Skip to content
This repository has been archived by the owner on Jan 22, 2025. It is now read-only.

Add Singularity support #241

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
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
10 changes: 7 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,10 @@ def prerelease_local_scheme(version):
# perform the install
setup(
name='girder-slicer-cli-web',
use_scm_version={
'local_scheme': prerelease_local_scheme,
'fallback_version': '0.0.0'},
# use_scm_version={
# 'local_scheme': prerelease_local_scheme,
# 'fallback_version': '0.0.0'},
version='1.5.5',
setup_requires=[
'setuptools-scm',
],
Expand Down Expand Up @@ -71,6 +72,9 @@ def prerelease_local_scheme(version):
'worker': [
'docker>=2.6.0',
'girder-worker[worker]>=0.6.0',
],
'singularity': [
'slicer-cli-web-singularity',
]
},
entry_points={
Expand Down
11 changes: 8 additions & 3 deletions slicer_cli_web/docker_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,14 @@
from girder_jobs.models.job import Job

from .config import PluginSettings
from .models import CLIItem, DockerImageItem, DockerImageNotFoundError
from .models import CLIItem, DockerImageNotFoundError
from .rest_slicer_cli import genRESTEndPointsForSlicerCLIsForItem

try:
from slicer_cli_web_singularity.singularity_image import SingularityImageItem as ImageItem
except ImportError:
from .models import DockerImageItem as ImageItem


class DockerResource(Resource):
"""
Expand Down Expand Up @@ -72,7 +77,7 @@ def __init__(self, name):
def getDockerImages(self, params):
data = {}
if self.getCurrentUser():
for image in DockerImageItem.findAllImages(self.getCurrentUser()):
for image in ImageItem.findAllImages(self.getCurrentUser()):
imgData = {}
for cli in image.getCLIs():
basePath = '/%s/cli/%s' % (self.resourceName, cli._id)
Expand Down Expand Up @@ -119,7 +124,7 @@ def _deleteImage(self, names, deleteImage):
docker rmi -f <image> )
:type name: boolean
"""
removed = DockerImageItem.removeImages(names, self.getCurrentUser())
removed = ImageItem.removeImages(names, self.getCurrentUser())
if removed != names:
rest = [name for name in names if name not in removed]
raise RestException('Some docker images could not be removed. %s' % (rest))
Expand Down
138 changes: 101 additions & 37 deletions slicer_cli_web/image_job.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
###############################################################################

import json
import os
import time

import docker
Expand All @@ -29,6 +30,19 @@

from .models import DockerImageError, DockerImageItem, DockerImageNotFoundError

try:
from slicer_cli_web_singularity.job import (find_and_remove_local_sif_files,
is_singularity_installed,
load_meta_data_for_singularity,
pull_image_and_convert_to_sif)
from slicer_cli_web_singularity.singularity_image import SingularityImage, SingularityImageItem
from slicer_cli_web_singularity.utils import (generate_image_name_for_singularity,
switch_to_sif_image_folder)
USE_SINGULARITY = True # TODO: do this better
except ImportError as e:
USE_SINGULARITY = False
logger.info(f'Failed to import singularity modules: {e}')


def deleteImage(job):
"""
Expand All @@ -40,32 +54,42 @@ def deleteImage(job):
"""
job = Job().updateJob(
job,
log='Started to Delete Docker images\n',
log=f'Started to Delete {"Singularity" if USE_SINGULARITY else "Docker"} images\n',
status=JobStatus.RUNNING,
)

docker_client = None

try:
deleteList = job['kwargs']['deleteList']
error = False

try:
docker_client = docker.from_env(version='auto')
if USE_SINGULARITY:
sif_folder = os.getenv('SIF_IMAGE_PATH')
else:
try:
docker_client = docker.from_env(version='auto')

except docker.errors.DockerException as err:
logger.exception('Could not create the docker client')
job = Job().updateJob(
job,
log='Failed to create the Docker Client\n' + str(err) + '\n',
status=JobStatus.ERROR,
)
raise DockerImageError('Could not create the docker client')
except docker.errors.DockerException as err:
logger.exception('Could not create the docker client')
job = Job().updateJob(
job,
log='Failed to create the Docker Client\n' + str(err) + '\n',
status=JobStatus.ERROR,
)
raise DockerImageError('Could not create the docker client')

for name in deleteList:
try:
docker_client.images.remove(name, force=True)
if USE_SINGULARITY:
name = generate_image_name_for_singularity(name)
filename = os.path.join(sif_folder, name)
os.remove(filename)
else:
docker_client.images.remove(name, force=True)

except Exception as err:
logger.exception('Failed to remove image')
logger.exception('Failed to remove image ', name)
job = Job().updateJob(
job,
log='Failed to remove image \n' + str(err) + '\n',
Expand Down Expand Up @@ -129,38 +153,68 @@ def jobPullAndLoad(job):
try:
job = Job().updateJob(
job,
log='Started to Load Docker images\n',
log=f'Started to Load {"Singularity" if USE_SINGULARITY else "Docker"} images\n',
status=JobStatus.RUNNING,
)
user = User().load(job['userId'], level=AccessType.READ)
baseFolder = Folder().load(
job['kwargs']['folder'], user=user, level=AccessType.WRITE, exc=True)

logger.info(f"names = {job['kwargs']['nameList']}")
loadList = job['kwargs']['nameList']
logger.info(f'loadList = {loadList}')
job = Job().updateJob(
job,
log=f'loadList = {loadList}\n',
)

job = Job().updateJob(
job,
log=f'singularity = {USE_SINGULARITY}\n',
)

errorState = False

notExistSet = set()
try:
docker_client = docker.from_env(version='auto')
if USE_SINGULARITY:
is_singularity_installed()

except docker.errors.DockerException as err:
logger.exception('Could not create the docker client')
job = Job().updateJob(
job,
log='Failed to create the Docker Client\n' + str(err) + '\n',
)
raise DockerImageError('Could not create the docker client')
# Singularity doesn't use layers and uses caching so if we have a new version of the
# image with the same tag, it will not be pulled but instead the same is used from
# singularity cache. Therefore, we need to remove local images if new version with
# same tag has to be pulled.
# MAKE SURE YOU'RE' NOT CONSTANTLY PULLING THE SAME VERSION OF THE IMAGE
# UNTIL THE ACTUAL IMAGE IS UPDATED
for name in loadList:
find_and_remove_local_sif_files(name)

pullList = loadList
loadList = []

else:
try:
docker_client = docker.from_env(version='auto')

except docker.errors.DockerException as err:
logger.exception('Could not create the docker client')
job = Job().updateJob(
job,
log='Failed to create the Docker Client\n' + str(err) + '\n',
)
raise DockerImageError('Could not create the docker client')

pullList = [
name for name in loadList
if not findLocalImage(docker_client, name) or
str(job['kwargs'].get('pull')).lower() == 'true']
loadList = [name for name in loadList if name not in pullList]
pullList = [
name for name in loadList
if not findLocalImage(docker_client, name) or
str(job['kwargs'].get('pull')).lower() == 'true']
loadList = [name for name in loadList if name not in pullList]

try:
stage = 'pulling'
pullDockerImage(docker_client, pullList, job)
if USE_SINGULARITY:
switch_to_sif_image_folder()
pull_image_and_convert_to_sif(pullList)
else:
pullDockerImage(docker_client, pullList, job)
except DockerImageNotFoundError as err:
errorState = True
notExistSet = set(err.imageName)
Expand All @@ -170,12 +224,23 @@ def jobPullAndLoad(job):
notExistSet) + '\n',
)
stage = 'metadata'
images, loadingError = loadMetadata(job, docker_client, pullList,
loadList, notExistSet)
for name, cli_dict in images:
docker_image = docker_client.images.get(name)
stage = 'parsing'
DockerImageItem.saveImage(name, cli_dict, docker_image, user, baseFolder)

if USE_SINGULARITY:
images, loadingError = load_meta_data_for_singularity(job, pullList,
loadList, notExistSet)
for name, cli_dict in images:
singularity_image_object = SingularityImage(name)
stage = 'parsing'
SingularityImageItem.saveImage(
name, cli_dict, singularity_image_object, user, baseFolder)
else:
images, loadingError = loadMetadata(job, docker_client, pullList,
loadList, notExistSet)
for name, cli_dict in images:
docker_image = docker_client.images.get(name)
stage = 'parsing'
DockerImageItem.saveImage(name, cli_dict, docker_image, user, baseFolder)

if errorState is False and loadingError is False:
newStatus = JobStatus.SUCCESS
else:
Expand Down Expand Up @@ -222,7 +287,6 @@ def loadMetadata(job, docker_client, pullList, loadList, notExistSet):
job = Job().updateJob(
job,
log='Image %s was pulled successfully \n' % name,

)

try:
Expand Down
7 changes: 5 additions & 2 deletions slicer_cli_web/rest_slicer_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,10 @@ def cliSubHandler(currentItem, params, user, token, datalist=None):
:param datalist: if not None, an object with keys that override
parameters. No outputs are used.
"""
from .girder_worker_plugin.direct_docker_run import run
try:
from slicer_cli_web_singularity.girder_worker_plugin.direct_singularity_run import run
except ImportError:
from .girder_worker_plugin.direct_docker_run import run

original_params = copy.deepcopy(params)
if hasattr(getCurrentToken, 'set'):
Expand Down Expand Up @@ -461,7 +464,7 @@ def cliSubHandler(currentItem, params, user, token, datalist=None):
girder_job_type=jobType,
girder_job_title=jobTitle,
girder_result_hooks=result_hooks,
image=cliItem.digest,
image=cliItem.image, # TODO: check this shouldnt be .digest for Docker
pull_image='if-not-present',
container_args=container_args,
**job_kwargs
Expand Down
3 changes: 3 additions & 0 deletions slicer_cli_web/singularity/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[build-system]
requires = ["setuptools", "setuptools-scm"]
build-backend = "setuptools.build_meta"
30 changes: 30 additions & 0 deletions slicer_cli_web/singularity/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from setuptools import find_packages, setup

setup(
name='slicer-cli-web-singularity',
version='0.0.0',
description='A girder plugin adding singularity support to slicer-cli-web',
author='Kitware, Inc.',
author_email='[email protected]',
license='Apache Software License 2.0',
classifiers=[
'Development Status :: 4 - Beta',
'Environment :: Console',
'License :: OSI Approved :: Apache Software License',
'Operating System :: OS Independent',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
],
install_requires=['girder-slicer-cli-web', 'girder-worker-singularity'],
entry_points={
'girder_worker_plugins': [
'slicer_cli_web_singularity = slicer_cli_web_singularity:SlicerCLISingularityWebWorkerPlugin',
]
},
packages=find_packages(),
zip_safe=False
)
25 changes: 25 additions & 0 deletions slicer_cli_web/singularity/slicer_cli_web_singularity/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
###############################################################################
# Copyright Kitware Inc.
#
# Licensed under the Apache License, Version 2.0 ( the "License" );
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
###############################################################################

from girder_worker import GirderWorkerPluginABC


class SlicerCLISingularityWebWorkerPlugin(GirderWorkerPluginABC):
def __init__(self, app, *args, **kwargs):
self.app = app

def task_imports(self):
return ['slicer_cli_web_singularity.girder_worker_plugin.direct_singularity_run']
Loading