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

SHPC New Functionality - Reinstall #674

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions shpc/client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,34 @@ def get_parser():
action="store_true",
)

# Reinstall an installed software
reinstall = subparsers.add_parser(
"reinstall",
description=help.reinstall_description,
formatter_class=argparse.RawTextHelpFormatter,
)
reinstall.add_argument(
"reinstall_recipe",
help="recipe to reinstall",
nargs="?",
)

reinstall.add_argument(
"--update-containers",
"-u",
dest="update_containers",
help="Also reinstall the containers (by default, containers are preserved)",
default=False,
action="store_true",
)

reinstall.add_argument(
"--all",
"-a",
help="reinstall all installed software",
action="store_true",
)

# List installed modules
listing = subparsers.add_parser(
"list",
Expand Down Expand Up @@ -397,6 +425,7 @@ def get_parser():
install,
listing,
remove,
reinstall,
shell,
test,
uninstall,
Expand Down Expand Up @@ -535,6 +564,8 @@ def help(return_code=0):
from .pull import main
elif args.command == "remove":
from .remove import main
elif args.command == "reinstall":
from .reinstall import main
elif args.command == "shell":
from .shell import main
elif args.command == "show":
Expand Down
39 changes: 39 additions & 0 deletions shpc/client/help.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,45 @@
$ shpc install python:3.9.5-alpine
"""

reinstall_description = """Reinstall a software. Containers are kept by default

# Reinstall a specific version of a software
$ shpc reinstall quay.io/biocontainers/samtools:1.20--h50ea8bc_1

# Reinstall all versions of a software
$ shpc reinstall quay.io/biocontainers/samtools

# Reinstall all installed software
$ shpc reinstall --all
OR
$ shpc reinstall -a

# Completely reinstall a specific version of a software without keeping the container
$ shpc reinstall quay.io/biocontainers/samtools:1.20--h50ea8bc_1 --update_container
OR
$ shpc reinstall quay.io/biocontainers/samtools:1.20--h50ea8bc_1 -u

# Completely reinstall all versions of a software without keeping the container
$ shpc reinstall quay.io/biocontainers/samtools --update_container
OR
$ shpc reinstall quay.io/biocontainers/samtools -u

# Completely reinstall all software without keeping the container
$ shpc reinstall --all --update_container
OR
$ shpc reinstall -a -u

# Invalid arguement combinations:
$ shpc reinstall quay.io/biocontainers/samtools:1.20--h50ea8bc_1 --all
OR
$ shpc reinstall quay.io/biocontainers/samtools:1.20--h50ea8bc_1 -a

$ shpc reinstall quay.io/biocontainers/samtools --all
OR
$ shpc reinstall quay.io/biocontainers/samtools -a
"""


listing_description = """List installed modules.

# Show installed modules
Expand Down
129 changes: 129 additions & 0 deletions shpc/client/reinstall.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
__author__ = "Vanessa Sochat"
__copyright__ = "Copyright 2021-2024, Vanessa Sochat"
__license__ = "MPL 2.0"

import shpc.utils
from shpc.logger import logger


def main(args, parser, extra, subparser):
from shpc.main import get_client

shpc.utils.ensure_no_extra(extra)

cli = get_client(
quiet=args.quiet,
settings_file=args.settings_file,
module_sys=args.module_sys,
container_tech=args.container_tech,
)

# Update config settings on the fly
cli.settings.update_params(args.config_params)

# Check if user entered an incomplete command
if not args.reinstall_recipe and not args.all:
subparser.error(
"You must specify a recipe to reinstall or use --all to reinstall all installed software."
)

# Reinstall all software
if args.all:
# Check if the user typed an invalid argument combination
if args.reinstall_recipe:
logger.exit(
"You cannot specify a recipe with --all. Use shpc reinstall --all to reinstall all installed software."
)

# Check if the user has any software installed
installed_software = cli.list(return_modules=True)
if not installed_software:
logger.exit("You currently don't have any installed software to reinstall.")

# Reinstall all installed software
print("Reinstalling all installed software...")
for software in installed_software.keys():
reinstall(software, cli, args, update_containers=args.update_containers)
logger.info("All software reinstalled.")

# Reinstall a specific software
else:
# Add namespace
name = cli.add_namespace(args.reinstall_recipe)

# Reinstall the software
reinstall(name, cli, args, update_containers=args.update_containers)


def reinstall(name, cli, args, update_containers=False):
"""
Reinstall a specific version or all versions of a software.
"""
# Check if the provided recipe is known in any registry
try:
cli._load_container(name)
except SystemExit:
# Give additional messages relating to shpc reinstall, to the original exit message in _load_container function
logger.exit(
"This means it cannot be reinstalled because it is not installed, and cannot be installed because it is not known in any registry.\nPlease check the name or try a different recipe."
)

# Check if the software or version is installed
installed_versions = cli.list(return_modules=True).get(name.split(":")[0], [])
if not installed_versions:
logger.exit(
f"You currently don't have '{name}' installed.\nTry: shpc install", 0
)

# Determine if a specific version is requested
specific_version = ":" in name
if specific_version and name.split(":")[1] not in installed_versions:
logger.exit(
f"You currently don't have '{name}' installed.\nTry: shpc install", 0
)

# Handle reinstallation logic
if specific_version:
print(f"Reinstalling {name}...")
reinstall_version(name, cli, args, update_containers)
logger.info(f"Successfully reinstalled {name}.")
else:
print(f"Reinstalling all versions of {name}...")
for version in installed_versions:
version_name = f"{name}:{version}"
reinstall_version(version_name, cli, args, update_containers)
logger.info(f"Successfully reinstalled all versions of {name}.")


def reinstall_version(name, cli, args, update_containers):
"""
Sub-function to handle the actual reinstallation
"""
# Get the list of views the software was in
views_with_module = set()
views_dir = cli.new_module(name).module_dir
for view_name, entry in cli.views.items():
if entry.exists(views_dir):
views_with_module.add(view_name)

# Uninstallation process. By default, uninstall without prompting the user and keep the container except the user wants a complete reinstall
cli.uninstall(name, force=True, keep_container=not update_containers)

# Display a helpful message to the user about the state of the container during reinstall process
if not update_containers:
print(
"Container was successfully preserved, module files and wrapper scripts will be overwritten..."
)
else:
print("No container was preserved, all files will be overwritten...")

# Installation process
cli.install(name)

# Restore the software to the captured views
print(
f"Restoring {name} to the views it was uninstalled from during reinstallation"
)
for view_name in views_with_module:
cli.view_install(view_name, name)
logger.info(f"Restored {name} to view: {view_name}")
10 changes: 10 additions & 0 deletions shpc/main/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,16 @@ def install(self, name, tag=None, **kwargs):
"""
raise NotImplementedError

def reinstall(self, name, update_containers=False):
"""
Reinstall an installed software
"""
from shpc.client.reinstall import reinstall

cli = self
args = {}
reinstall(name, cli, args, update_containers=update_containers)

def uninstall(self, name, tag=None):
"""
Uninstall must also implemented by the subclass (e.g., lmod)
Expand Down
19 changes: 11 additions & 8 deletions shpc/main/modules/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ def view_uninstall(self, view, name, force=False):
logger.exit("View %s does not exist, cannot uninstall." % view)
return self.views[view].uninstall(module.module_dir)

def uninstall(self, name, force=False):
def uninstall(self, name, force=False, keep_container=False):
"""
Given a unique resource identifier, uninstall a module.
Set "force" to True to bypass the confirmation prompt.
Expand All @@ -100,7 +100,7 @@ def uninstall(self, name, force=False):

# Ask before deleting anything!
if not force:
msg = name + "?"
msg = "Do you wish to uninstall " + name + "?"
if views_with_module:
msg += (
"\nThis will uninstall the module from views:\n %s\nAre you sure?"
Expand All @@ -110,14 +110,17 @@ def uninstall(self, name, force=False):
return

# Podman needs image deletion
self.container.delete(module.name)
if not keep_container: # For reinstall
self.container.delete(module.name)

if module.container_dir != module.module_dir:
self._uninstall(
module.container_dir,
self.container_base,
"$container_base/%s" % module.name,
)
if not keep_container:
self._uninstall(
module.container_dir,
self.container_base,
"$container_base/%s" % module.name,
)

self._uninstall(
module.module_dir,
self.settings.module_base,
Expand Down
Loading