Skip to content

Commit 1e80e9f

Browse files
authored
Add an uninstall command (#19)
This command removes the install and data directories, deletes the "brainframe" group, and removes all BrainFrame Docker images from the machine. It's effectively meant as the opposite of the "install" command. However, the command does not remove Docker or Docker Compose even if the CLI was originally used to install them. It also does not uninstall the BrainFrame CLI itself.
1 parent d7cb483 commit 1e80e9f

10 files changed

Lines changed: 146 additions & 16 deletions

File tree

.circleci/config.yml

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,10 @@ jobs:
118118
- run: sudo brainframe compose up -d
119119
- assert-core-container-running
120120
- assert-core-responds-to-http
121-
- run: sudo brainframe compose down
121+
- run: sudo brainframe uninstall --noninteractive
122+
- assert-uninstalled-data-preserved:
123+
install_path: /usr/share/brainframe
124+
data_path: /var/lib/brainframe
122125

123126
upload-to-pypi:
124127
docker:
@@ -143,7 +146,10 @@ jobs:
143146
- run: sudo $(which poetry) run brainframe compose up -d
144147
- assert-core-container-running
145148
- assert-core-responds-to-http
146-
- run: sudo $(which poetry) run brainframe compose down
149+
- run: sudo $(which poetry) run brainframe uninstall --noninteractive
150+
- assert-uninstalled-data-preserved:
151+
install_path: /usr/local/share/brainframe
152+
data_path: /var/local/brainframe
147153

148154
build-binary:
149155
docker:
@@ -188,7 +194,10 @@ jobs:
188194
- run: sudo brainframe compose up -d
189195
- assert-core-container-running
190196
- assert-core-responds-to-http
191-
- run: sudo brainframe compose down
197+
- run: sudo brainframe uninstall --noninteractive
198+
- assert-uninstalled-data-preserved:
199+
install_path: /usr/local/share/brainframe
200+
data_path: /var/local/brainframe
192201

193202
commands:
194203
install-poetry:
@@ -218,3 +227,17 @@ commands:
218227
done;
219228
echo "BrainFrame core service is up."
220229
no_output_timeout: 1m
230+
assert-uninstalled-data-preserved:
231+
parameters:
232+
install_path:
233+
type: string
234+
data_path:
235+
type: string
236+
steps:
237+
- run:
238+
name: Check BrainFrame is uninstalled, but data is preserved
239+
command: |
240+
stat << parameters.data_path >>
241+
! stat << parameters.install_path >>
242+
# Check that the images have been removed
243+
! docker image ls | grep aotuai/brainframe_core

brainframe/cli/commands/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@
33
from .info import info
44
from .install import install
55
from .self_update import self_update
6+
from .uninstall import uninstall
67
from .update import update
78
from .utils import by_name

brainframe/cli/commands/backup.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,23 +12,19 @@
1212
print_utils,
1313
)
1414

15-
from .utils import command, subcommand_parse_args
15+
from .utils import command, requires_root, subcommand_parse_args
1616

1717
BACKUP_DIR_FORMAT = "%Y-%m-%d_%H-%M-%S"
1818

1919

2020
@command("backup")
21+
@requires_root # Some BrainFrame services write files as root
2122
def backup():
2223
install_path = config.install_path.value
2324
data_path = config.data_path.value
2425

2526
args = _parse_args(data_path)
2627

27-
# This command has to be run as root for now because some BrainFrame
28-
# services write files as the root user.
29-
if not os_utils.is_root():
30-
print_utils.fail_translate("general.user-not-root")
31-
3228
docker_compose.assert_installed(install_path)
3329

3430
dependencies.rsync.ensure(args.noninteractive, args.install_rsync)

brainframe/cli/commands/install.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,14 @@
1212
print_utils,
1313
)
1414

15-
from .utils import command, subcommand_parse_args
15+
from .utils import command, requires_root, subcommand_parse_args
1616

1717

1818
@command("install")
19+
@requires_root
1920
def install():
2021
args = _parse_args()
2122

22-
if not os_utils.is_root():
23-
print_utils.fail_translate("general.user-not-root")
24-
2523
# Print some introductory text
2624
if not args.noninteractive:
2725
print_utils.art()
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import shutil
2+
from argparse import ArgumentParser
3+
4+
import i18n
5+
from brainframe.cli import config, docker_compose, os_utils, print_utils
6+
7+
from .utils import command, requires_root, subcommand_parse_args
8+
9+
10+
@command("uninstall")
11+
@requires_root
12+
def uninstall():
13+
install_path = config.install_path.value
14+
data_path = config.data_path.value
15+
16+
args = _parse_args()
17+
18+
docker_compose.assert_installed(install_path)
19+
20+
if args.noninteractive:
21+
delete_data = args.delete_data
22+
else:
23+
delete_data = print_utils.ask_yes_no("uninstall.ask-delete-data")
24+
25+
if not args.noninteractive:
26+
directories_to_delete = [str(install_path)]
27+
if delete_data:
28+
print_utils.warning_translate("uninstall.warning-with-delete-data")
29+
directories_to_delete.append(str(data_path))
30+
else:
31+
print_utils.warning_translate("uninstall.warning")
32+
33+
print_utils.warning_translate(
34+
"uninstall.directories-deleted", directories=directories_to_delete,
35+
)
36+
confirmed = print_utils.ask_yes_no("uninstall.ask-confirm")
37+
if not confirmed:
38+
print_utils.fail_translate("uninstall.abort")
39+
40+
print_utils.translate("uninstall.deleting-images")
41+
docker_compose.run(install_path, ["down", "--rmi", "all"])
42+
print_utils.translate("uninstall.deleting-install-path")
43+
shutil.rmtree(install_path)
44+
if delete_data:
45+
print_utils.translate("uninstall.deleting-data-path")
46+
shutil.rmtree(data_path)
47+
48+
print()
49+
print_utils.translate("uninstall.complete", color=print_utils.Color.GREEN)
50+
51+
52+
def _parse_args():
53+
parser = ArgumentParser(
54+
description=i18n.t("uninstall.description"),
55+
usage=i18n.t("uninstall.usage"),
56+
)
57+
58+
parser.add_argument(
59+
"--noninteractive",
60+
action="store_true",
61+
help=i18n.t("general.noninteractive-help"),
62+
)
63+
64+
parser.add_argument(
65+
"--delete-data",
66+
action="store_true",
67+
help=i18n.t("uninstall.delete-data-help"),
68+
)
69+
70+
return subcommand_parse_args(parser)

brainframe/cli/commands/utils.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1+
import functools
12
import sys
23
from argparse import ArgumentParser
4+
from typing import Any, Callable
5+
6+
from brainframe.cli import os_utils, print_utils
37

48
by_name = {}
59
"""A dict that maps command names to their corresponding function"""
@@ -23,3 +27,16 @@ def subcommand_parse_args(parser: ArgumentParser):
2327
args.noninteractive = True
2428

2529
return args
30+
31+
32+
def requires_root(function: Callable) -> Callable:
33+
"""A decorator that checks if the user is root before running a function"""
34+
35+
@functools.wraps(function)
36+
def wrapper(*args, **kwargs) -> Any:
37+
if not os_utils.is_root():
38+
print_utils.fail_translate("general.user-not-root")
39+
40+
return function(*args, **kwargs)
41+
42+
return wrapper

brainframe/cli/os_utils.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ def create_group(group_name: str, group_id: int):
2828
# Create the group
2929
result = run(["groupadd", group_name, "--gid", str(group_id)])
3030
if result.returncode != 0:
31-
message = i18n.t("install.create-group-failure")
32-
message = message.format(error=str(result.stderr))
33-
print_utils.fail(message)
31+
print_utils.fail_translate(
32+
"install.create-group-failure", error=str(result.stderr)
33+
)
3434

3535

3636
def added_to_group(group_name):

brainframe/cli/translations/install.en.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,4 @@ en:
7272
group? Doing so will allow the BrainFrame CLI to interact with Docker without
7373
root permissions."
7474
install-version: "BrainFrame:%{version} will be installed."
75+
create-group-failure: "Failed to create the \"brainframe\" group: %{error}"

brainframe/cli/translations/portal.en.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ en:
1010
update Updates the BrainFrame server to a new version
1111
info Provides information about the BrainFrame server
1212
compose Runs all following commands and flags through docker-compose
13+
uninstall Uninstalls the BrainFrame server
1314
1415
Examples:
1516
brainframe install Installs BrainFrame interactively
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
en:
2+
description: "Uninstalls the BrainFrame server. This command does not
3+
uninstall the BrainFrame CLI itself."
4+
usage: "brainframe uninstall [<args>]"
5+
delete-data-help: "If provided, data created by the BrainFrame server will be
6+
deleted as well"
7+
8+
ask-delete-data: "Would you like to delete all data created by the BrainFrame
9+
server as well?"
10+
warning: "WARNING: This command will uninstall the BrainFrame server
11+
and delete the server's Docker images!"
12+
warning-with-delete-data: "WARNING: This command will uninstall the
13+
BrainFrame server, permanently delete all data the server has produced
14+
including backups, and delete the server's Docker images! This data cannot
15+
be recovered."
16+
directories-deleted: "The following directories will be deleted:
17+
%{directories}"
18+
ask-confirm: "Would you like to continue?"
19+
abort: "Uninstallation has been cancelled. No data has been lost."
20+
deleting-images: "Bringing down the BrainFrame server and deleting images..."
21+
deleting-install-path: "Deleting the install path..."
22+
deleting-data-path: "Deleting the data path..."
23+
complete: "The BrainFrame server has been uninstalled."

0 commit comments

Comments
 (0)