Merge pull request #1 from andreped/huggingface
Browse files- .github/workflows/deploy.yml +20 -0
- Dockerfile +29 -0
- README.md +36 -1
- dsa/.dockerignore +3 -0
- dsa/.gitignore +4 -0
- dsa/README.rst +146 -0
- dsa/docker-compose.yml +168 -0
- dsa/girder.cfg +45 -0
- dsa/provision.py +649 -0
- dsa/provision.yaml +100 -0
- dsa/rabbitmq.advanced.config +1 -0
- dsa/start_girder.sh +50 -0
- dsa/start_worker.sh +34 -0
- dsa/utils/.vimrc +39 -0
- dsa/utils/cli_test.py +313 -0
- dsa/utils/rebuild_and_restart_girder.sh +11 -0
- dsa/utils/restart_girder.sh +8 -0
.github/workflows/deploy.yml
ADDED
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
name: Deploy HF
|
2 |
+
on:
|
3 |
+
push:
|
4 |
+
branches: [ main ]
|
5 |
+
|
6 |
+
# to run this workflow manually from the Actions tab
|
7 |
+
workflow_dispatch:
|
8 |
+
|
9 |
+
jobs:
|
10 |
+
sync-to-hub:
|
11 |
+
runs-on: ubuntu-latest
|
12 |
+
steps:
|
13 |
+
- uses: actions/checkout@v3
|
14 |
+
with:
|
15 |
+
fetch-depth: 0
|
16 |
+
lfs: true
|
17 |
+
- name: Push to hub
|
18 |
+
env:
|
19 |
+
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
20 |
+
run: git push https://andreped:[email protected]/spaces/andreped/dsa4hf main
|
Dockerfile
ADDED
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
FROM docker/compose
|
2 |
+
|
3 |
+
# Switch to root user
|
4 |
+
#USER root
|
5 |
+
|
6 |
+
WORKDIR /code
|
7 |
+
|
8 |
+
COPY ./dsa /code/dsa
|
9 |
+
|
10 |
+
#CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
|
11 |
+
|
12 |
+
#RUN docker run -itd -v /var/run/docker.sock:/var/run/docker.sock -v /code/dsa/:/var/tmp/ docker/compose:1.24.1 -f /var/tmp/docker-compose.yml up -d
|
13 |
+
|
14 |
+
RUN ls
|
15 |
+
|
16 |
+
#RUN sudo chmod 757 /var/run/docker.sock
|
17 |
+
|
18 |
+
# CMD ["/bin/bash"]
|
19 |
+
# CMD ["ls"]
|
20 |
+
#CMD docker run --privileged=true -it -v /var/run/docker.sock:/var/run/docker.sock -v /code/dsa/:/var/tmp/ docker/compose:1.24.1 -f /var/tmp/dsa/docker-compose.yml up -d
|
21 |
+
#CMD ["ls"]
|
22 |
+
|
23 |
+
ENTRYPOINT [ "/bin/sh" ]
|
24 |
+
|
25 |
+
#RUN docker-compose pull
|
26 |
+
|
27 |
+
#CMD ["docker", "run", "-it", "-v", "/var/run/docker.sock:/var/run/docker.sock", ]
|
28 |
+
|
29 |
+
#docker run -itd -v /var/run/docker.sock:/var/run/docker.sock -v /root/test/:/var/tmp/ docker/compose:1.24.1 -f /var/tmp/docker-compose.yaml up -d
|
README.md
CHANGED
@@ -1 +1,36 @@
|
|
1 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
title: 'Digital Slide Archive: Platform for working with large microscopy images.'
|
3 |
+
colorFrom: indigo
|
4 |
+
colorTo: indigo
|
5 |
+
sdk: docker
|
6 |
+
app_port: 8080
|
7 |
+
emoji: 🔬
|
8 |
+
pinned: false
|
9 |
+
license: mit
|
10 |
+
---
|
11 |
+
|
12 |
+
# dsa4hf
|
13 |
+
|
14 |
+
This project was made to showcase developed plugins for Digital Slide Archive through on Hugging Face spaces.
|
15 |
+
|
16 |
+
The project is a work-in-progress. I will make a release when I have it working. Stay tuned!
|
17 |
+
|
18 |
+
## Getting started
|
19 |
+
|
20 |
+
#### Deployment
|
21 |
+
|
22 |
+
When the solution is ready, the website should be accessible on [Hugging Face](https://huggingface.co/spaces/andreped/dsa4hf).
|
23 |
+
|
24 |
+
#### Development
|
25 |
+
|
26 |
+
```
|
27 |
+
docker build -t dsa4hf .
|
28 |
+
docker run -it -p 8080:8080 dsa4hf
|
29 |
+
```
|
30 |
+
|
31 |
+
To go inside docker image and debug, at the bottom of the Dockerfile, add `ENTRYPOINT [ "/bin/sh" ]` before running.
|
32 |
+
|
33 |
+
## Credit
|
34 |
+
|
35 |
+
I did not develop Digital Slide Archive, only implemented some plugins and showcased deployment on Hugging Face space.
|
36 |
+
Credit should be given to the developers of DSA for making such an amazing open software solution!
|
dsa/.dockerignore
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
assetstore
|
2 |
+
db
|
3 |
+
logs
|
dsa/.gitignore
ADDED
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
1 |
+
assetstore/
|
2 |
+
db/
|
3 |
+
logs/
|
4 |
+
*.local.*
|
dsa/README.rst
ADDED
@@ -0,0 +1,146 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
========================================
|
2 |
+
Digital Slide Archive via Docker Compose
|
3 |
+
========================================
|
4 |
+
|
5 |
+
This directory contains a complete docker-compose set up for the Digital Slide Archive.
|
6 |
+
|
7 |
+
Edit the docker-compose.yml file (or add a docker-compose override file) to add mount points for additional data or for exposing additional ports.
|
8 |
+
|
9 |
+
Prerequisites
|
10 |
+
-------------
|
11 |
+
|
12 |
+
Before using this, you need both Docker and docker-compose. See the `official installation instructions <https://docs.docker.com/compose/install>`_.
|
13 |
+
|
14 |
+
The docker-compose file assumes certain file paths. This has been tested on Ubuntu 20.04. It will probably work on other Linux variants.
|
15 |
+
|
16 |
+
Get the Digital Slide Archive repository::
|
17 |
+
|
18 |
+
git clone https://github.com/DigitalSlideArchive/digital_slide_archive
|
19 |
+
|
20 |
+
Start
|
21 |
+
-----
|
22 |
+
|
23 |
+
Change to the appropriate directory::
|
24 |
+
|
25 |
+
cd digital_slide_archive/devops/dsa/
|
26 |
+
|
27 |
+
To get the most recent built docker images, do::
|
28 |
+
|
29 |
+
docker-compose pull
|
30 |
+
|
31 |
+
If you don't pull the images, the main image will be built in preference to pulling.
|
32 |
+
|
33 |
+
To start the Digital Slide Archive::
|
34 |
+
|
35 |
+
DSA_USER=$(id -u):$(id -g) docker-compose up
|
36 |
+
|
37 |
+
This uses your current user id so that database files, logs, assetstore files, and temporary files are owned by the current user. If you omit setting ``DSA_USER``, files may be created owned by root.
|
38 |
+
|
39 |
+
The girder instance can now be accessed at http://localhost:8080. By default, it creates an ``admin`` user with a password of ``password``. Note that this example does not add any default tasks or sample files. You can log in with the admin user and use the Slicer CLI Web plugin settings to add default tasks (e.g., ``dsarchive/histomicstk:latest``).
|
40 |
+
|
41 |
+
Stop
|
42 |
+
----
|
43 |
+
|
44 |
+
To stop the Digital Slide Archive::
|
45 |
+
|
46 |
+
docker-compose down -v
|
47 |
+
|
48 |
+
The ``-v`` option removes unneeded temporary docker volumes.
|
49 |
+
|
50 |
+
Sample Data
|
51 |
+
-----------
|
52 |
+
|
53 |
+
Sample data can be added after performing ``docker-compose up`` by running::
|
54 |
+
|
55 |
+
python utils/cli_test.py dsarchive/histomicstk:latest --test
|
56 |
+
|
57 |
+
This downloads the HistomicsTK analysis tools, some sample data, and runs nuclei detection on some of the sample data. You need Python 3.6 or later available and may need to ``pip install girder-client`` before you can run this command.
|
58 |
+
|
59 |
+
|
60 |
+
Development
|
61 |
+
-----------
|
62 |
+
|
63 |
+
You can log into the running ``girder`` or ``worker`` containers by typing::
|
64 |
+
|
65 |
+
docker-compose exec girder bash
|
66 |
+
|
67 |
+
There are two convenience scripts ``restart_girder.sh`` and ``rebuild_and_restart_girder.sh`` that can be run in the container.
|
68 |
+
|
69 |
+
You can develop source code by mounting the source directory into the container. See the ``docker-compose.yml`` file for details.
|
70 |
+
|
71 |
+
If you need to log into the container as the Girder user, type::
|
72 |
+
|
73 |
+
docker-compose exec --user $(id -u) girder bash
|
74 |
+
|
75 |
+
Technical Details
|
76 |
+
-----------------
|
77 |
+
|
78 |
+
The Digital Slider Archive is built in Girder and Girder Worker. Here, these are coordinated using docker-compose. There are five containers that are started:
|
79 |
+
|
80 |
+
- `Girder <https://girder.readthedocs.io/>`_. Girder is an asset and user management system. It handles permissions and serves data via http.
|
81 |
+
|
82 |
+
- `MongoDB <https://www.mongodb.com/>`_. Girder stores settings and information about users and assets in a MongoDB database.
|
83 |
+
|
84 |
+
- `Girder Worker <https://girder-worker.readthedocs.io/>`_. Girder Worker is a task runner based on `Celery <https://celery.readthedocs.io/>`_ that has specific features to get authenticated data from Girder.
|
85 |
+
|
86 |
+
- `RabbitMQ <https://www.rabbitmq.com/>`_. Girder communicates to Girder Worker through a broker. In this configuration it is RabbitMQ. Girder Worker can be run on multiple computers communicating with a single broker to distribute processing.
|
87 |
+
|
88 |
+
- `Memcached <https://memcached.org/>`_. Memcached is used to cache data for faster access. This is used for large tiled images.
|
89 |
+
|
90 |
+
The Digital Slide Archive relies on several Girder plugins:
|
91 |
+
|
92 |
+
- `large_image <https://github.com/girder/large_image>`_. This provides a standardized way to access a wide range of image formats. Images can be handled as multi-resolution tiles. large_image has numerous tile sources to handle different formats.
|
93 |
+
|
94 |
+
- `HistomicUI <https://github.com/DigitalSlideArchive/HistomicsUI>`_. This provides a user interface to examine and annotate large images.
|
95 |
+
|
96 |
+
- `Slicer CLI Web <https://github.com/girder/slicer_cli_web>`_. This can run processing tasks in Docker containers. Tasks report their capabilities via the Slicer CLI standard, listing required and optional inputs and outputs. These tasks can be selected and configured via Girder and HistomicsUI and then run in a distributed fashion via Girder Worker.
|
97 |
+
|
98 |
+
Slicer CLI Web runs tasks in Docker containers and is itself running in a Docker container (in Girder for determining options and Girder Worker to run the task). In order to allow a process in a docker container to create another docker container, the paths the docker executable and communications sockets are mounted from the host to the docker container.
|
99 |
+
|
100 |
+
Permissions
|
101 |
+
-----------
|
102 |
+
|
103 |
+
By default, the girder container is run in Docker privileged mode. This can be reduced to a small set of permissions (see the docker-compose.yml file for details), but these may vary depending on the host system. If no extra permissions are granted, or if the docker daemon is started with --no-new-privileges, or if libfuse is not installed on the host system, the internal fuse mount will not be started. This may prevent full functionality with non-filesystem assestores and with some multiple-file image formats.
|
104 |
+
|
105 |
+
Customizing
|
106 |
+
-----------
|
107 |
+
|
108 |
+
Since this uses standard docker-compose, you can customize the process by creating a ``docker-compose.override.yml`` file in the same directory (or a yaml file of any name and use appropriate ``docker-compose -f docker-compose.yml -f <my yaml file> <command>`` command). Further, if you mount a provisioning yaml file into the docker image, you can customize settings, plugins, resources, and other options.
|
109 |
+
|
110 |
+
See the ``docker-compose.yml`` and ``provision.yaml`` files for details.
|
111 |
+
|
112 |
+
Example
|
113 |
+
~~~~~~~
|
114 |
+
|
115 |
+
To add some additional girder plugins and mount additional directories for assetstores, you can do something like this:
|
116 |
+
|
117 |
+
``docker-compose.override.yml``::
|
118 |
+
|
119 |
+
---
|
120 |
+
version: '3'
|
121 |
+
services:
|
122 |
+
girder:
|
123 |
+
environment:
|
124 |
+
# Specify that we want to use the provisioning file
|
125 |
+
DSA_PROVISION_YAML: ${DSA_PROVISION_YAML:-/opt/digital_slide_archive/devops/dsa/provision.yaml}
|
126 |
+
volumes:
|
127 |
+
# Mount the local provisioning file into the container
|
128 |
+
- ./provision.local.yaml:/opt/digital_slide_archive/devops/dsa/provision.yaml
|
129 |
+
# Also expose a local data mount into the container
|
130 |
+
- /mnt/data:/mnt/data
|
131 |
+
|
132 |
+
``provision.local.yaml``::
|
133 |
+
|
134 |
+
---
|
135 |
+
# Load some sample data
|
136 |
+
samples: True
|
137 |
+
# A list of additional pip modules to install
|
138 |
+
pip:
|
139 |
+
- girder-oauth
|
140 |
+
- girder-ldap
|
141 |
+
# rebuild the girder web client since we install some additional plugins
|
142 |
+
rebuild-client: True
|
143 |
+
# List slicer-cli-images to pull and load
|
144 |
+
slicer-cli-image:
|
145 |
+
- dsarchive/histomicstk:latest
|
146 |
+
- girder/slicer_cli_web:small
|
dsa/docker-compose.yml
ADDED
@@ -0,0 +1,168 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
version: '3'
|
3 |
+
services:
|
4 |
+
girder:
|
5 |
+
image: dsarchive/dsa_common
|
6 |
+
build: ../..
|
7 |
+
# Instead of privileged mode, fuse can use:
|
8 |
+
# devices:
|
9 |
+
# - /dev/fuse:/dev/fuse
|
10 |
+
# security_opt:
|
11 |
+
# - apparmor:unconfined
|
12 |
+
# cap_add:
|
13 |
+
# - SYS_ADMIN
|
14 |
+
# but these may be somewhat host specific, so we default to privileged. If
|
15 |
+
# the docker daemon is being run with --no-new-privileges, fuse may not
|
16 |
+
# work.
|
17 |
+
# See also https://github.com/docker/for-linux/issues/321 for possible
|
18 |
+
# methods to avoid both privileged mode and cap_add SYS_ADMIN.
|
19 |
+
privileged: true
|
20 |
+
# Set DSA_USER to a user id that is part of the docker group (e.g.,
|
21 |
+
# `DSA_USER=$(id -u):$(id -g)`). This makes files in assetstores and logs
|
22 |
+
# owned by that user and provides permissions to manage docker
|
23 |
+
environment:
|
24 |
+
DSA_USER: ${DSA_USER:-}
|
25 |
+
DSA_PROVISION_YAML: ${DSA_PROVISION_YAML:-/opt/digital_slide_archive/devops/dsa/provision.yaml}
|
26 |
+
restart: unless-stopped
|
27 |
+
# Set DSA_PORT to expose the interface on another port (default 8080).
|
28 |
+
ports:
|
29 |
+
- "${DSA_PORT:-8080}:8080"
|
30 |
+
volumes:
|
31 |
+
# Needed to use slicer_cli_web to run docker containers
|
32 |
+
- /var/run/docker.sock:/var/run/docker.sock
|
33 |
+
# Default assetstore
|
34 |
+
- ./assetstore:/assetstore
|
35 |
+
# Location of girder.cfg
|
36 |
+
- ./girder.cfg:/etc/girder.cfg
|
37 |
+
# Location of provision.py
|
38 |
+
- ./provision.py:/opt/digital_slide_archive/devops/dsa/provision.py
|
39 |
+
- ./provision.yaml:/opt/digital_slide_archive/devops/dsa/provision.yaml
|
40 |
+
- ./start_girder.sh:/opt/digital_slide_archive/devops/dsa/start_girder.sh
|
41 |
+
# Location to store logs
|
42 |
+
- ./logs:/logs
|
43 |
+
|
44 |
+
# For local development, uncomment the set of mounts associated with the
|
45 |
+
# local source files. Adding the editable egg directories first allows
|
46 |
+
# allow mounting source files from the host without breaking the internal
|
47 |
+
# data.
|
48 |
+
|
49 |
+
# - /opt/girder/girder.egg-info
|
50 |
+
# - /opt/girder/clients/python/girder_client.egg-info
|
51 |
+
# - ../../../girder:/opt/girder
|
52 |
+
|
53 |
+
# - /opt/girder_worker/girder_worker.egg-info
|
54 |
+
# - ../../../../girder_worker:/opt/girder_worker
|
55 |
+
|
56 |
+
# - /opt/girder_worker_utils/girder_worker_utils.egg-info
|
57 |
+
# - ../../../../girder_worker_utils:/opt/girder_worker_utils
|
58 |
+
|
59 |
+
# - /opt/HistomicsUI/histomicsui.egg-info
|
60 |
+
# - ../../../HistomicsUI:/opt/HistomicsUI
|
61 |
+
|
62 |
+
# - /opt/slicer_cli_web/girder_slicer_cli_web.egg-info
|
63 |
+
# - ../../../slicer_cli_web:/opt/slicer_cli_web
|
64 |
+
|
65 |
+
# - /opt/large_image/girder_annotation/girder_large_image_annotation.egg-info
|
66 |
+
# - /opt/large_image/girder/girder_large_image.egg-info
|
67 |
+
# - /opt/large_image/sources/bioformats/large_image_source_bioformats.egg-info
|
68 |
+
# - /opt/large_image/sources/openslide/large_image_source_openslide.egg-info
|
69 |
+
# - /opt/large_image/sources/ometiff/large_image_source_ometiff.egg-info
|
70 |
+
# - /opt/large_image/sources/pil/large_image_source_pil.egg-info
|
71 |
+
# - /opt/large_image/sources/test/large_image_source_test.egg-info
|
72 |
+
# - /opt/large_image/sources/dummy/large_image_source_dummy.egg-info
|
73 |
+
# - /opt/large_image/sources/tiff/large_image_source_tiff.egg-info
|
74 |
+
# - /opt/large_image/sources/mapnik/large_image_source_mapnik.egg-info
|
75 |
+
# - /opt/large_image/sources/openjpeg/large_image_source_openjpeg.egg-info
|
76 |
+
# - /opt/large_image/sources/gdal/large_image_source_gdal.egg-info
|
77 |
+
# - /opt/large_image/sources/nd2/large_image_source_nd2.egg-info
|
78 |
+
# - /opt/large_image/large_image.egg-info
|
79 |
+
# - /opt/large_image/utilities/converter/large_image_converter.egg-info
|
80 |
+
# - /opt/large_image/utilities/tasks/large_image_tasks.egg-info
|
81 |
+
# - ../../../large_image:/opt/large_image
|
82 |
+
|
83 |
+
# Add additional mounts here to get access to existing files on your
|
84 |
+
# system. Also add them to the worker container to reduce copying.
|
85 |
+
depends_on:
|
86 |
+
- mongodb
|
87 |
+
- memcached
|
88 |
+
- rabbitmq
|
89 |
+
command: /opt/digital_slide_archive/devops/dsa/start_girder.sh
|
90 |
+
mongodb:
|
91 |
+
image: "mongo:latest"
|
92 |
+
# Set DSA_USER to your user id (e.g., `DSA_USER=$(id -u):$(id -g)`)
|
93 |
+
# so that database files are owned by yourself.
|
94 |
+
user: ${DSA_USER:-PLEASE SET DSA_USER}
|
95 |
+
restart: unless-stopped
|
96 |
+
# Limiting maxConns reduces the amount of shared memory demanded by
|
97 |
+
# mongo. Remove this limit or increase the host vm.max_map_count value.
|
98 |
+
command: --maxConns 1000
|
99 |
+
volumes:
|
100 |
+
# Location to store database files
|
101 |
+
- ./db:/data/db
|
102 |
+
# Uncomment to allow access to the database from outside of the docker
|
103 |
+
# network.
|
104 |
+
# ports:
|
105 |
+
# - "27017"
|
106 |
+
logging:
|
107 |
+
options:
|
108 |
+
max-size: "10M"
|
109 |
+
max-file: "5"
|
110 |
+
memcached:
|
111 |
+
image: memcached
|
112 |
+
command: -m 4096 --max-item-size 8M
|
113 |
+
restart: unless-stopped
|
114 |
+
# Uncomment to allow access to memcached from outside of the docker network
|
115 |
+
# ports:
|
116 |
+
# - "11211"
|
117 |
+
logging:
|
118 |
+
options:
|
119 |
+
max-size: "10M"
|
120 |
+
max-file: "5"
|
121 |
+
rabbitmq:
|
122 |
+
image: "rabbitmq:latest"
|
123 |
+
restart: unless-stopped
|
124 |
+
# Uncomment to allow access to rabbitmq from outside of the docker network
|
125 |
+
# ports:
|
126 |
+
# - "5672"
|
127 |
+
environment:
|
128 |
+
RABBITMQ_DEFAULT_USER: ${RABBITMQ_DEFAULT_USER:-}
|
129 |
+
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_DEFAULT_PASS:-}
|
130 |
+
volumes:
|
131 |
+
- ./rabbitmq.advanced.config:/etc/rabbitmq/advanced.config:ro
|
132 |
+
logging:
|
133 |
+
options:
|
134 |
+
max-size: "10M"
|
135 |
+
max-file: "5"
|
136 |
+
worker:
|
137 |
+
image: dsarchive/dsa_common
|
138 |
+
build: ../..
|
139 |
+
# Set DSA_USER to a user id that is part of the docker group (e.g.,
|
140 |
+
# `DSA_USER=$(id -u):$(id -g)`). This provides permissions to manage
|
141 |
+
# docker
|
142 |
+
environment:
|
143 |
+
DSA_USER: ${DSA_USER:-}
|
144 |
+
DSA_WORKER_CONCURRENCY: ${DSA_WORKER_CONCURRENCY:-2}
|
145 |
+
DSA_PROVISION_YAML: ${DSA_PROVISION_YAML:-/opt/digital_slide_archive/devops/dsa/provision.yaml}
|
146 |
+
TMPDIR:
|
147 |
+
restart: unless-stopped
|
148 |
+
volumes:
|
149 |
+
# Needed to use slicer_cli_web to run docker containers
|
150 |
+
- /var/run/docker.sock:/var/run/docker.sock
|
151 |
+
# Modify the worker.local.cfg to specify a different rabbitmq server and
|
152 |
+
# then enable this mount. On the rabbitmq server, make sure you add a
|
153 |
+
# non-guest default user and use that both in the worker and in the main
|
154 |
+
# girder settings.
|
155 |
+
# - ./worker.local.cfg:/opt/girder_worker/girder_worker/worker.local.cfg
|
156 |
+
# Allow overriding the start command
|
157 |
+
- ./start_worker.sh:/opt/digital_slide_archive/devops/dsa/start_worker.sh
|
158 |
+
# Needed to allow transferring data to slicer_cli_web docker containers
|
159 |
+
- ${TMPDIR:-/tmp}:${TMPDIR:-/tmp}
|
160 |
+
# Add additional mounts here to get access to existing files on your
|
161 |
+
# system if they have the same path as on the girder container.
|
162 |
+
depends_on:
|
163 |
+
- rabbitmq
|
164 |
+
command: /opt/digital_slide_archive/devops/dsa/start_worker.sh
|
165 |
+
logging:
|
166 |
+
options:
|
167 |
+
max-size: "10M"
|
168 |
+
max-file: "5"
|
dsa/girder.cfg
ADDED
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
[global]
|
2 |
+
server.socket_host = "0.0.0.0"
|
3 |
+
server.max_request_body_size = 1073741824
|
4 |
+
|
5 |
+
[database]
|
6 |
+
uri = "mongodb://mongodb:27017/girder?socketTimeoutMS=3600000"
|
7 |
+
|
8 |
+
[server]
|
9 |
+
# Set to "production" or "development"
|
10 |
+
mode = "development"
|
11 |
+
# Disable the event daemon if you do not wish to run event handlers in a background thread.
|
12 |
+
# This may be necessary in certain deployment modes.
|
13 |
+
disable_event_daemon = False
|
14 |
+
|
15 |
+
[logging]
|
16 |
+
log_root = "/logs"
|
17 |
+
log_access = ["screen", "info"]
|
18 |
+
# Log everything to the info log (errors also go to the error log)
|
19 |
+
log_max_info_level = "CRITICAL"
|
20 |
+
# Increase maximum size of log file
|
21 |
+
log_max_size = "10 Mb"
|
22 |
+
|
23 |
+
[large_image]
|
24 |
+
# cache_backend is either "memcached" (default) or "python"
|
25 |
+
cache_backend = "memcached"
|
26 |
+
cache_memcached_url = "memcached"
|
27 |
+
cache_memcached_username = None
|
28 |
+
cache_memcached_password = None
|
29 |
+
# cache_python_memory_portion affects memory use when using python caching.
|
30 |
+
# Higher numbers use less memory.
|
31 |
+
# cache_python_memory_portion = 8
|
32 |
+
# These can be used to reduce the amount of memory used for caching tile
|
33 |
+
# sources
|
34 |
+
# cache_tilesource_memory_portion = 16
|
35 |
+
cache_tilesource_maximum = 64
|
36 |
+
|
37 |
+
[cache]
|
38 |
+
enabled = True
|
39 |
+
|
40 |
+
[histomicsui]
|
41 |
+
# If restrict_downloads is True, only logged-in users can access download
|
42 |
+
# and tiles/images endpoints. If this is a number, file and item download
|
43 |
+
# endpoints can be used by anonymous users for files up to the specified
|
44 |
+
# size in bytes. This setting does not affect logged-in users.
|
45 |
+
restrict_downloads = 100000
|
dsa/provision.py
ADDED
@@ -0,0 +1,649 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/usr/bin/env python3
|
2 |
+
|
3 |
+
import argparse
|
4 |
+
import configparser
|
5 |
+
import logging
|
6 |
+
import os
|
7 |
+
import subprocess
|
8 |
+
import sys
|
9 |
+
import tempfile
|
10 |
+
import time
|
11 |
+
|
12 |
+
import yaml
|
13 |
+
|
14 |
+
logger = logging.getLogger(__name__)
|
15 |
+
# See http://docs.python.org/3.3/howto/logging.html#configuring-logging-for-a-library
|
16 |
+
logging.getLogger(__name__).addHandler(logging.NullHandler())
|
17 |
+
|
18 |
+
|
19 |
+
def get_collection_folder(adminUser, collName, folderName):
|
20 |
+
from girder.models.collection import Collection
|
21 |
+
from girder.models.folder import Folder
|
22 |
+
|
23 |
+
if Collection().findOne({'lowerName': collName.lower()}) is None:
|
24 |
+
logger.info('Create collection %s', collName)
|
25 |
+
Collection().createCollection(collName, adminUser)
|
26 |
+
collection = Collection().findOne({'lowerName': collName.lower()})
|
27 |
+
if Folder().findOne({
|
28 |
+
'parentId': collection['_id'], 'lowerName': folderName.lower()}) is None:
|
29 |
+
logger.info('Create folder %s in %s', folderName, collName)
|
30 |
+
Folder().createFolder(collection, folderName, parentType='collection',
|
31 |
+
public=True, creator=adminUser)
|
32 |
+
folder = Folder().findOne({'parentId': collection['_id'], 'lowerName': folderName.lower()})
|
33 |
+
return folder
|
34 |
+
|
35 |
+
|
36 |
+
def get_sample_data(adminUser, collName='Sample Images', folderName='Images'):
|
37 |
+
"""
|
38 |
+
As needed, download sample data.
|
39 |
+
|
40 |
+
:param adminUser: a user to create and modify collections and folders.
|
41 |
+
:param collName: the collection name where the data will be added.
|
42 |
+
:param folderName: the folder name where the data will be added.
|
43 |
+
:returns: the folder where the sample data is located.
|
44 |
+
"""
|
45 |
+
try:
|
46 |
+
import girder_client
|
47 |
+
except ImportError:
|
48 |
+
logger.error('girder_client is unavailable. Cannot get sample data.')
|
49 |
+
return
|
50 |
+
from girder.models.item import Item
|
51 |
+
from girder.models.upload import Upload
|
52 |
+
from girder_large_image.models.image_item import ImageItem
|
53 |
+
|
54 |
+
folder = get_collection_folder(adminUser, collName, folderName)
|
55 |
+
remote = girder_client.GirderClient(apiUrl='https://data.kitware.com/api/v1')
|
56 |
+
remoteFolder = remote.resourceLookup('/collection/HistomicsTK/Deployment test images')
|
57 |
+
sampleItems = []
|
58 |
+
for remoteItem in remote.listItem(remoteFolder['_id']):
|
59 |
+
item = Item().findOne({'folderId': folder['_id'], 'name': remoteItem['name']})
|
60 |
+
if item and len(list(Item().childFiles(item, limit=1))):
|
61 |
+
sampleItems.append(item)
|
62 |
+
continue
|
63 |
+
if not item:
|
64 |
+
item = Item().createItem(remoteItem['name'], creator=adminUser, folder=folder)
|
65 |
+
for remoteFile in remote.listFile(remoteItem['_id']):
|
66 |
+
with tempfile.NamedTemporaryFile() as tf:
|
67 |
+
fileName = tf.name
|
68 |
+
tf.close()
|
69 |
+
logger.info('Downloading %s', remoteFile['name'])
|
70 |
+
remote.downloadFile(remoteFile['_id'], fileName)
|
71 |
+
Upload().uploadFromFile(
|
72 |
+
open(fileName, 'rb'), os.path.getsize(fileName),
|
73 |
+
name=remoteItem['name'], parentType='item',
|
74 |
+
parent=item, user=adminUser)
|
75 |
+
sampleItems.append(item)
|
76 |
+
for item in sampleItems:
|
77 |
+
if 'largeImage' not in item:
|
78 |
+
logger.info('Making large_item %s', item['name'])
|
79 |
+
try:
|
80 |
+
ImageItem().createImageItem(item, createJob=False)
|
81 |
+
except Exception:
|
82 |
+
pass
|
83 |
+
logger.info('done')
|
84 |
+
return folder
|
85 |
+
|
86 |
+
|
87 |
+
def value_from_resource(value, adminUser):
|
88 |
+
"""
|
89 |
+
If a value is a string that startwith 'resource:', it is a path to an
|
90 |
+
existing resource. Fetch it an return the string of the _id.
|
91 |
+
|
92 |
+
:param value: a value
|
93 |
+
:returns: the original value it is not a resource, or the string id of the
|
94 |
+
resource.
|
95 |
+
"""
|
96 |
+
import girder.utility.path as path_util
|
97 |
+
|
98 |
+
if str(value) == 'resourceid:admin':
|
99 |
+
value = str(adminUser['_id'])
|
100 |
+
elif str(value).startswith('resourceid:'):
|
101 |
+
resource = path_util.lookUpPath(value.split(':', 1)[1], force=True)['document']
|
102 |
+
value = str(resource['_id'])
|
103 |
+
elif str(value) == 'resource:admin':
|
104 |
+
value = adminUser
|
105 |
+
elif str(value).startswith('resource:'):
|
106 |
+
value = path_util.lookUpPath(value.split(':', 1)[1], force=True)['document']
|
107 |
+
return value
|
108 |
+
|
109 |
+
|
110 |
+
def provision_resources(resources, adminUser):
|
111 |
+
"""
|
112 |
+
Given a dictionary of resources, add them to the system. The resource is
|
113 |
+
only added if a resource of that name with the same parent object does not
|
114 |
+
exist.
|
115 |
+
|
116 |
+
:param resources: a list of resources to add.
|
117 |
+
:param adminUser: the admin user to use for provisioning.
|
118 |
+
"""
|
119 |
+
from girder.utility.model_importer import ModelImporter
|
120 |
+
|
121 |
+
for entry in resources:
|
122 |
+
entry = {k: value_from_resource(v, adminUser) for k, v in entry.items()}
|
123 |
+
modelName = entry.pop('model')
|
124 |
+
metadata = entry.pop('metadata', None)
|
125 |
+
metadata_update = entry.pop('metadata_update', True)
|
126 |
+
metadata_key = entry.pop('metadata_key', 'meta')
|
127 |
+
model = ModelImporter.model(modelName)
|
128 |
+
key = 'name' if model != 'user' else 'login'
|
129 |
+
query = {}
|
130 |
+
if key in entry:
|
131 |
+
query[key] = entry[key]
|
132 |
+
owners = {'folder': 'parent', 'item': 'folder', 'file': 'item'}
|
133 |
+
ownerKey = owners.get(modelName)
|
134 |
+
if ownerKey and ownerKey in entry and isinstance(
|
135 |
+
entry[ownerKey], dict) and '_id' in entry[ownerKey]:
|
136 |
+
query[ownerKey + 'Id'] = entry[ownerKey]['_id']
|
137 |
+
if query and model.findOne(query):
|
138 |
+
result = model.findOne(query)
|
139 |
+
logger.debug('Has %s (%r)', modelName, entry)
|
140 |
+
else:
|
141 |
+
createFunc = getattr(model, 'create%s' % modelName.capitalize())
|
142 |
+
logger.info('Creating %s (%r)', modelName, entry)
|
143 |
+
result = createFunc(**entry)
|
144 |
+
if isinstance(metadata, dict) and hasattr(model, 'setMetadata'):
|
145 |
+
if metadata_key not in metadata or metadata_update:
|
146 |
+
if metadata_key not in result:
|
147 |
+
result[metadata_key] = {}
|
148 |
+
result[metadata_key].update(metadata.items())
|
149 |
+
for key in metadata:
|
150 |
+
if metadata[key] is None:
|
151 |
+
del result[metadata_key][key]
|
152 |
+
model.validateKeys(result[metadata_key])
|
153 |
+
result = model.save(result)
|
154 |
+
|
155 |
+
|
156 |
+
def get_slicer_images(imageList, adminUser):
|
157 |
+
"""
|
158 |
+
Load a list of cli docker images into the system.
|
159 |
+
|
160 |
+
:param imageList: a list of docker images.
|
161 |
+
:param adminUser: an admin user for permissions.
|
162 |
+
"""
|
163 |
+
import threading
|
164 |
+
|
165 |
+
from girder import logger
|
166 |
+
from girder.models.setting import Setting
|
167 |
+
from girder_jobs.constants import JobStatus
|
168 |
+
from girder_jobs.models.job import Job
|
169 |
+
from slicer_cli_web.config import PluginSettings
|
170 |
+
from slicer_cli_web.docker_resource import DockerResource
|
171 |
+
from slicer_cli_web.image_job import jobPullAndLoad
|
172 |
+
|
173 |
+
imageList = [entry for entry in imageList if entry and len(entry)]
|
174 |
+
if not len(imageList):
|
175 |
+
return
|
176 |
+
logger.info('Pulling and installing slicer_cli images: %r', imageList)
|
177 |
+
job = Job().createLocalJob(
|
178 |
+
module='slicer_cli_web.image_job',
|
179 |
+
function='jobPullAndLoad',
|
180 |
+
kwargs={
|
181 |
+
'nameList': imageList,
|
182 |
+
'folder': Setting().get(PluginSettings.SLICER_CLI_WEB_TASK_FOLDER),
|
183 |
+
},
|
184 |
+
title='Pulling and caching docker images',
|
185 |
+
type=DockerResource.jobType,
|
186 |
+
user=adminUser,
|
187 |
+
public=True,
|
188 |
+
asynchronous=True
|
189 |
+
)
|
190 |
+
job = Job().save(job)
|
191 |
+
t = threading.Thread(target=jobPullAndLoad, args=(job, ))
|
192 |
+
t.start()
|
193 |
+
logpos = 0
|
194 |
+
logger.info('Result:\n')
|
195 |
+
while job['status'] not in {JobStatus.SUCCESS, JobStatus.ERROR, JobStatus.CANCELED}:
|
196 |
+
time.sleep(0.1)
|
197 |
+
job = Job().load(id=job['_id'], user=adminUser, includeLog=True)
|
198 |
+
if 'log' in job:
|
199 |
+
while logpos < len(job['log']):
|
200 |
+
logger.info(job['log'][logpos].rstrip())
|
201 |
+
logpos += 1
|
202 |
+
t.join()
|
203 |
+
if 'log' not in job:
|
204 |
+
logger.warning('Job record: %r', job)
|
205 |
+
if job['status'] != JobStatus.SUCCESS:
|
206 |
+
raise Exception('Failed to pull and load images')
|
207 |
+
|
208 |
+
|
209 |
+
def preprovision(opts):
|
210 |
+
"""
|
211 |
+
Preprovision the instance. This includes installing python modules with
|
212 |
+
pip and rebuilding the girder client if desired.
|
213 |
+
|
214 |
+
:param opts: the argparse options.
|
215 |
+
"""
|
216 |
+
if getattr(opts, 'pip', None) and len(opts.pip):
|
217 |
+
for entry in opts.pip:
|
218 |
+
cmd = 'pip install %s' % entry
|
219 |
+
logger.info('Installing: %s', cmd)
|
220 |
+
subprocess.check_call(cmd, shell=True)
|
221 |
+
if getattr(opts, 'shell', None) and len(opts.shell):
|
222 |
+
for entry in opts.shell:
|
223 |
+
cmd = entry
|
224 |
+
logger.info('Running: %s', cmd)
|
225 |
+
subprocess.check_call(cmd, shell=True)
|
226 |
+
if getattr(opts, 'rebuild-client', None):
|
227 |
+
cmd = 'girder build'
|
228 |
+
if str(getattr(opts, 'rebuild-client', None)).lower().startswith('dev'):
|
229 |
+
cmd += ' --dev'
|
230 |
+
logger.info('Rebuilding girder client: %s', cmd)
|
231 |
+
cmd = ('NPM_CONFIG_FUND=false NPM_CONFIG_AUDIT=false '
|
232 |
+
'NPM_CONFIG_AUDIT_LEVEL=high NPM_CONFIG_LOGLEVEL=error '
|
233 |
+
'NPM_CONFIG_PROGRESS=false NPM_CONFIG_PREFER_OFFLINE=true ' + cmd)
|
234 |
+
subprocess.check_call(cmd, shell=True)
|
235 |
+
|
236 |
+
|
237 |
+
def provision(opts): # noqa
|
238 |
+
"""
|
239 |
+
Provision the instance.
|
240 |
+
|
241 |
+
:param opts: the argparse options.
|
242 |
+
"""
|
243 |
+
from girder.models.assetstore import Assetstore
|
244 |
+
from girder.models.setting import Setting
|
245 |
+
from girder.models.user import User
|
246 |
+
|
247 |
+
# If there is are no admin users, create an admin user
|
248 |
+
if User().findOne({'admin': True}) is None:
|
249 |
+
adminParams = dict({
|
250 |
+
'login': 'admin',
|
251 |
+
'password': 'password',
|
252 |
+
'firstName': 'Admin',
|
253 |
+
'lastName': 'Admin',
|
254 |
+
'email': '[email protected]',
|
255 |
+
'public': True,
|
256 |
+
}, **(opts.admin if opts.admin else {}))
|
257 |
+
User().createUser(admin=True, **adminParams)
|
258 |
+
adminUser = User().findOne({'admin': True})
|
259 |
+
|
260 |
+
# Make sure we have an assetstore
|
261 |
+
assetstoreParams = opts.assetstore or {'name': 'Assetstore', 'root': '/assetstore'}
|
262 |
+
if not isinstance(assetstoreParams, list):
|
263 |
+
assetstoreParams = [assetstoreParams]
|
264 |
+
if Assetstore().findOne() is None:
|
265 |
+
for params in assetstoreParams:
|
266 |
+
method = params.pop('method', 'createFilesystemAssetstore')
|
267 |
+
getattr(Assetstore(), method)(**params)
|
268 |
+
|
269 |
+
# Make sure we have a demo collection and download some demo files
|
270 |
+
if getattr(opts, 'samples', None):
|
271 |
+
get_sample_data(
|
272 |
+
adminUser,
|
273 |
+
getattr(opts, 'sample-collection', 'Samples'),
|
274 |
+
getattr(opts, 'sample-folder', 'Images'))
|
275 |
+
if opts.resources:
|
276 |
+
provision_resources(opts.resources, adminUser)
|
277 |
+
settings = dict({}, **(opts.settings or {}))
|
278 |
+
force = getattr(opts, 'force', None) or []
|
279 |
+
for key, value in settings.items():
|
280 |
+
if (value != '__SKIP__' and (
|
281 |
+
force is True or key in force or
|
282 |
+
Setting().get(key) is None or
|
283 |
+
Setting().get(key) == Setting().getDefault(key))):
|
284 |
+
value = value_from_resource(value, adminUser)
|
285 |
+
logger.info('Setting %s to %r', key, value)
|
286 |
+
Setting().set(key, value)
|
287 |
+
if getattr(opts, 'slicer-cli-image', None):
|
288 |
+
try:
|
289 |
+
get_slicer_images(getattr(opts, 'slicer-cli-image', None), adminUser)
|
290 |
+
except Exception:
|
291 |
+
logger.info('Cannot fetch slicer-cli-images.')
|
292 |
+
|
293 |
+
|
294 |
+
def preprovision_worker(opts):
|
295 |
+
"""
|
296 |
+
Preprovision the worker.
|
297 |
+
"""
|
298 |
+
settings = dict({}, **(opts.worker or {}))
|
299 |
+
if settings.get('pip') and len(settings['pip']):
|
300 |
+
for entry in settings['pip']:
|
301 |
+
cmd = 'pip install %s' % entry
|
302 |
+
logger.info('Installing: %s', cmd)
|
303 |
+
subprocess.check_call(cmd, shell=True)
|
304 |
+
if settings.get('shell') and len(settings['shell']):
|
305 |
+
for entry in settings['shell']:
|
306 |
+
cmd = entry
|
307 |
+
logger.info('Running: %s', cmd)
|
308 |
+
subprocess.check_call(cmd, shell=True)
|
309 |
+
|
310 |
+
|
311 |
+
def provision_worker(opts):
|
312 |
+
"""
|
313 |
+
Provision the worker. There are a few top-level settings, but others
|
314 |
+
should be in the worker sub-field.
|
315 |
+
"""
|
316 |
+
settings = dict({}, **(opts.worker or {}))
|
317 |
+
for key in dir(opts):
|
318 |
+
if key.startswith('worker-'):
|
319 |
+
mainkey = key.split('worker-', 1)[1]
|
320 |
+
if settings.get(mainkey) is None:
|
321 |
+
settings[mainkey] = getattr(opts, key)
|
322 |
+
if not settings.get('rabbitmq-host'):
|
323 |
+
return
|
324 |
+
conf = configparser.ConfigParser()
|
325 |
+
conf.read([settings['config']])
|
326 |
+
conf.set('celery', 'broker', 'amqp://%s:%s@%s/' % (
|
327 |
+
settings['rabbitmq-user'], settings['rabbitmq-pass'], settings['host']))
|
328 |
+
conf.set('celery', 'backend', 'rpc://%s:%s@%s/' % (
|
329 |
+
settings['rabbitmq-user'], settings['rabbitmq-pass'], settings['host']))
|
330 |
+
with open(settings['config'], 'w') as fptr:
|
331 |
+
conf.write(fptr)
|
332 |
+
|
333 |
+
|
334 |
+
def merge_environ_opts(opts):
|
335 |
+
"""
|
336 |
+
Merge environment options, overriding other settings.
|
337 |
+
|
338 |
+
:param opts: the options parsed from the command line.
|
339 |
+
:return opts: the modified options.
|
340 |
+
"""
|
341 |
+
keyDict = {
|
342 |
+
'RABBITMQ_USER': 'worker_rabbitmq_user',
|
343 |
+
'RABBITMQ_PASS': 'worker_rabbitmq_pass',
|
344 |
+
'DSA_RABBITMQ_HOST': 'worker_rabbitmq_host',
|
345 |
+
}
|
346 |
+
for key, value in os.environ.items():
|
347 |
+
if not value or not value.strip():
|
348 |
+
continue
|
349 |
+
if key == 'DSA_WORKER_API_URL':
|
350 |
+
key = 'worker.api_url'
|
351 |
+
elif key.startswith('DSA_SETTING_'):
|
352 |
+
key = key.split('DSA_SETTING_', 1)[1]
|
353 |
+
elif key in keyDict:
|
354 |
+
key = keyDict[key]
|
355 |
+
else:
|
356 |
+
continue
|
357 |
+
opts.settings[key] = value
|
358 |
+
if not opts.force:
|
359 |
+
opts.force = {key}
|
360 |
+
elif opts.force is not True:
|
361 |
+
opts.force.add(key)
|
362 |
+
return opts
|
363 |
+
|
364 |
+
|
365 |
+
def merge_yaml_opts(opts, parser):
|
366 |
+
"""
|
367 |
+
Parse a yaml file of provisioning options. Modify the options used for
|
368 |
+
provisioning.
|
369 |
+
|
370 |
+
:param opts: the options parsed from the command line.
|
371 |
+
:param parser: command line parser used to check if the options are the
|
372 |
+
default values.
|
373 |
+
:return opts: the modified options.
|
374 |
+
"""
|
375 |
+
yamlfile = os.environ.get('DSA_PROVISION_YAML') if getattr(
|
376 |
+
opts, 'yaml', None) is None else opts.yaml
|
377 |
+
if yamlfile:
|
378 |
+
logger.debug('Parse yaml file: %r', yamlfile)
|
379 |
+
if not yamlfile or not os.path.exists(yamlfile):
|
380 |
+
return opts
|
381 |
+
defaults = parser.parse_args(args=[])
|
382 |
+
if getattr(opts, 'use-defaults', None) is not False:
|
383 |
+
defaults = merge_default_opts(defaults)
|
384 |
+
yamlopts = yaml.safe_load(open(yamlfile).read())
|
385 |
+
for key, value in yamlopts.items():
|
386 |
+
key = key.replace('_', '-')
|
387 |
+
if getattr(opts, key, None) is None or getattr(
|
388 |
+
opts, key, None) == getattr(defaults, key, None):
|
389 |
+
if key == 'settings' and getattr(opts, key, None) and isinstance(value, dict):
|
390 |
+
getattr(opts, key).update(value)
|
391 |
+
else:
|
392 |
+
setattr(opts, key, value)
|
393 |
+
logger.debug('Arguments after adding yaml: %r', opts)
|
394 |
+
return opts
|
395 |
+
|
396 |
+
|
397 |
+
def merge_default_opts(opts):
|
398 |
+
"""
|
399 |
+
Add the defaults to the options.
|
400 |
+
|
401 |
+
:param opts: the options parsed from the command line.
|
402 |
+
:return opts: the modified options.
|
403 |
+
"""
|
404 |
+
settings = dict({}, **(opts.settings or {}))
|
405 |
+
settings.update({
|
406 |
+
'worker.broker': 'amqp://guest:guest@rabbitmq',
|
407 |
+
'worker.backend': 'rpc://guest:guest@rabbitmq',
|
408 |
+
'worker.api_url': 'http://girder:8080/api/v1',
|
409 |
+
'worker.direct_path': True,
|
410 |
+
'core.brand_name': 'Digital Slide Archive',
|
411 |
+
'histomicsui.webroot_path': 'histomics',
|
412 |
+
'histomicsui.alternate_webroot_path': 'histomicstk',
|
413 |
+
'histomicsui.delete_annotations_after_ingest': True,
|
414 |
+
'homepage.markdown': """# Digital Slide Archive
|
415 |
+
---
|
416 |
+
## Bioinformatics Platform
|
417 |
+
|
418 |
+
Welcome to the **Digital Slide Archive**.
|
419 |
+
|
420 |
+
Developers who want to use the Girder REST API should check out the
|
421 |
+
[interactive web API docs](api/v1).
|
422 |
+
|
423 |
+
The [HistomicsUI](histomics) application is enabled.""",
|
424 |
+
'slicer_cli_web.task_folder': 'resourceid:collection/Tasks/Slicer CLI Web Tasks',
|
425 |
+
})
|
426 |
+
opts.settings = settings
|
427 |
+
if getattr(opts, 'slicer-cli-image', None) is None:
|
428 |
+
setattr(opts, 'slicer-cli-image', ['dsarchive/histomicstk:latest'])
|
429 |
+
if getattr(opts, 'assetstore', None) is None:
|
430 |
+
opts.assetstore = {
|
431 |
+
'name': 'Assetstore',
|
432 |
+
'root': '/assetstore',
|
433 |
+
'method': 'createFilesystemAssetstore',
|
434 |
+
}
|
435 |
+
if getattr(opts, 'admin', None) is None:
|
436 |
+
opts.admin = {
|
437 |
+
'login': 'admin',
|
438 |
+
'password': 'password',
|
439 |
+
'firstName': 'Admin',
|
440 |
+
'lastName': 'Admin',
|
441 |
+
'email': '[email protected]',
|
442 |
+
'public': True,
|
443 |
+
}
|
444 |
+
resources = opts.resources or []
|
445 |
+
resources.extend([{
|
446 |
+
'model': 'collection',
|
447 |
+
'name': 'Tasks',
|
448 |
+
'creator': 'resource:admin',
|
449 |
+
'public': True,
|
450 |
+
}, {
|
451 |
+
'model': 'folder',
|
452 |
+
'parent': 'resource:collection/Tasks',
|
453 |
+
'parentType': 'collection',
|
454 |
+
'name': 'Slicer CLI Web Tasks',
|
455 |
+
'creator': 'resource:admin',
|
456 |
+
'public': True,
|
457 |
+
}])
|
458 |
+
opts.resources = resources
|
459 |
+
return opts
|
460 |
+
|
461 |
+
|
462 |
+
class YamlAction(argparse.Action):
|
463 |
+
def __init__(self, option_strings, dest, nargs=None, **kwargs):
|
464 |
+
"""Parse a yaml entry"""
|
465 |
+
if nargs is not None:
|
466 |
+
raise ValueError('nargs not allowed')
|
467 |
+
super().__init__(option_strings, dest, **kwargs)
|
468 |
+
|
469 |
+
def __call__(self, parser, namespace, values, option_string=None):
|
470 |
+
setattr(namespace, self.dest, yaml.safe_load(values))
|
471 |
+
|
472 |
+
|
473 |
+
if __name__ == '__main__': # noqa
|
474 |
+
parser = argparse.ArgumentParser(description='Provision a Digital Slide Archive instance')
|
475 |
+
parser.add_argument(
|
476 |
+
'--force', action='store_true',
|
477 |
+
help='Reset all settings. This does not change the admin user or the '
|
478 |
+
'default assetstore if those already exist. Otherwise, settings are '
|
479 |
+
'only added or modified if they do not exist or are the default '
|
480 |
+
'value.')
|
481 |
+
parser.add_argument(
|
482 |
+
'--samples', '--data', '--sample-data',
|
483 |
+
action='store_true', help='Download sample data')
|
484 |
+
parser.add_argument(
|
485 |
+
'--sample-collection', dest='sample-collection', default='Samples',
|
486 |
+
help='Sample data collection name')
|
487 |
+
parser.add_argument(
|
488 |
+
'--sample-folder', dest='sample-folder', default='Images',
|
489 |
+
help='Sample data folder name')
|
490 |
+
parser.add_argument(
|
491 |
+
'--admin', action=YamlAction,
|
492 |
+
help='A yaml dictionary of parameters used to create a default admin '
|
493 |
+
'user. If any of login, password, firstName, lastName, email, or '
|
494 |
+
'public are not specified, some default values are used. If any '
|
495 |
+
'admin user already exists, no modifications are made.')
|
496 |
+
parser.add_argument(
|
497 |
+
'--assetstore', action=YamlAction,
|
498 |
+
help='A yaml dictionary (or list of dictionaries) of parameters used '
|
499 |
+
'to create a default assetstore. This can include "method" which '
|
500 |
+
'includes the creation method, such as "createFilesystemAssetstore" '
|
501 |
+
'or "createS3Assetstore". Otherwise, this is a list of parameters '
|
502 |
+
'passed to the creation method. For filesystem assetstores, these '
|
503 |
+
'parameters are name, root, and perms. For S3 assetstores, these are '
|
504 |
+
'name, bucket, accessKeyId, secret, prefix, service, readOnly, '
|
505 |
+
'region, inferCredentials, and serverSideEncryption. If unspecified, '
|
506 |
+
'a filesystem assetstore is created.')
|
507 |
+
parser.add_argument(
|
508 |
+
'--settings', action=YamlAction,
|
509 |
+
help='A yaml dictionary of settings to change in the Girder '
|
510 |
+
'database. This is merged with the default settings dictionary. '
|
511 |
+
'Settings are only changed if they are their default values, the '
|
512 |
+
'force option is used, or they are specified by an environment '
|
513 |
+
'variable. If a setting has a value of "__SKIP__", it will not be '
|
514 |
+
'changed (this can prevent setting a default setting '
|
515 |
+
'option to any value).')
|
516 |
+
parser.add_argument(
|
517 |
+
'--resources', action=YamlAction,
|
518 |
+
help='A yaml list of resources to add by name to the Girder '
|
519 |
+
'database. Each entry is a dictionary including "model" with the '
|
520 |
+
'resource model and a dictionary of values to pass to the '
|
521 |
+
'appropriate create(resource) function. A value of '
|
522 |
+
'"resource:<path>" is converted to the resource document with that '
|
523 |
+
'resource path. "resource:admin" uses the default admin, '
|
524 |
+
'"resourceid:<path>" is the string id for the resource path.')
|
525 |
+
parser.add_argument(
|
526 |
+
'--yaml',
|
527 |
+
help='Specify parameters for this script in a yaml file. If no value '
|
528 |
+
'is specified, this defaults to the environment variable of '
|
529 |
+
'DSA_PROVISION_YAML. No error is thrown if the file does not exist. '
|
530 |
+
'The yaml file is a dictionary of keys as would be passed to the '
|
531 |
+
'command line.')
|
532 |
+
parser.add_argument(
|
533 |
+
'--no-mongo-compat', action='store_false', dest='mongo-compat',
|
534 |
+
default=True, help='Do not automatically set the mongo feature '
|
535 |
+
'compatibility version to the current server version.')
|
536 |
+
parser.add_argument(
|
537 |
+
'--no-defaults', action='store_false', dest='use-defaults',
|
538 |
+
default=None, help='Do not use default settings; start with a minimal '
|
539 |
+
'number of parameters.')
|
540 |
+
parser.add_argument(
|
541 |
+
'--pip', action='append', help='A list of modules to pip install. If '
|
542 |
+
'any are specified that include girder client plugins, also specify '
|
543 |
+
'--rebuild-client. Each specified value is passed to pip install '
|
544 |
+
'directly, so additional options are needed, these can be added (such '
|
545 |
+
'as --find-links). The actual values need to be escaped '
|
546 |
+
'appropriately for a bash shell.')
|
547 |
+
parser.add_argument(
|
548 |
+
'--rebuild-client', dest='rebuild-client', action='store_true',
|
549 |
+
default=False, help='Rebuild the girder client.')
|
550 |
+
parser.add_argument(
|
551 |
+
'--slicer-cli-image', dest='slicer-cli-image', action='append',
|
552 |
+
help='Install slicer_cli images.')
|
553 |
+
|
554 |
+
parser.add_argument(
|
555 |
+
'--rabbitmq-user', default='guest', dest='worker-rabbitmq-user',
|
556 |
+
help='Worker: RabbitMQ user name.')
|
557 |
+
parser.add_argument(
|
558 |
+
'--rabbitmq-pass', default='guest', dest='worker-rabbitmq-pass',
|
559 |
+
help='Worker: RabbitMQ password.')
|
560 |
+
parser.add_argument(
|
561 |
+
'--rabbitmq-host', dest='worker-rabbitmq-host',
|
562 |
+
help='Worker: RabbitMQ host.')
|
563 |
+
parser.add_argument(
|
564 |
+
'--config', dest='worker-config',
|
565 |
+
default='/opt/girder_worker/girder_worker/worker.local.cfg',
|
566 |
+
help='Worker: Path to the worker config file.')
|
567 |
+
parser.add_argument(
|
568 |
+
'--worker', action=YamlAction,
|
569 |
+
help='A yaml dictionary of worker settings.')
|
570 |
+
parser.add_argument(
|
571 |
+
'--worker-main', dest='portion', action='store_const',
|
572 |
+
const='worker-main',
|
573 |
+
help='Provision a worker, not the main process.')
|
574 |
+
parser.add_argument(
|
575 |
+
'--worker-pre', dest='portion', action='store_const',
|
576 |
+
const='worker-pre',
|
577 |
+
help='Pre-provision a worker, not the main process.')
|
578 |
+
parser.add_argument(
|
579 |
+
'--pre', dest='portion', action='store_const', const='pre',
|
580 |
+
help='Only do preprovisioning (install optional python modules and '
|
581 |
+
'optionally build the girder client).')
|
582 |
+
parser.add_argument(
|
583 |
+
'--main', dest='portion', action='store_const', const='main',
|
584 |
+
help='Only do main provisioning.')
|
585 |
+
parser.add_argument(
|
586 |
+
'--verbose', '-v', action='count', default=0, help='Increase verbosity')
|
587 |
+
parser.add_argument(
|
588 |
+
'--dry-run', '-n', dest='dry-run', action='store_true',
|
589 |
+
help='Report merged options but do not actually apply them')
|
590 |
+
opts = parser.parse_args(args=sys.argv[1:])
|
591 |
+
logger.addHandler(logging.StreamHandler(sys.stderr))
|
592 |
+
logger.setLevel(max(1, logging.WARNING - 10 * opts.verbose))
|
593 |
+
try:
|
594 |
+
logger.info('Provision file date: %s; size: %d',
|
595 |
+
time.ctime(os.path.getmtime(__file__)),
|
596 |
+
os.path.getsize(__file__))
|
597 |
+
except Exception:
|
598 |
+
pass
|
599 |
+
logger.debug('Parsed arguments: %r', opts)
|
600 |
+
if getattr(opts, 'use-defaults', None) is not False:
|
601 |
+
opts = merge_default_opts(opts)
|
602 |
+
opts = merge_yaml_opts(opts, parser)
|
603 |
+
opts = merge_environ_opts(opts)
|
604 |
+
logger.debug('Merged arguments: %r', opts)
|
605 |
+
if getattr(opts, 'dry-run'):
|
606 |
+
print(yaml.dump({k: v for k, v in vars(opts).items() if v is not None}))
|
607 |
+
sys.exit(0)
|
608 |
+
# Worker provisioning
|
609 |
+
if getattr(opts, 'portion', None) == 'worker-pre':
|
610 |
+
preprovision_worker(opts)
|
611 |
+
sys.exit(0)
|
612 |
+
if getattr(opts, 'portion', None) == 'worker-main':
|
613 |
+
provision_worker(opts)
|
614 |
+
sys.exit(0)
|
615 |
+
if getattr(opts, 'portion', None) in {'pre', None}:
|
616 |
+
# Run provisioning that has to happen before configuring the server.
|
617 |
+
preprovision(opts)
|
618 |
+
if getattr(opts, 'portion', None) == 'pre':
|
619 |
+
sys.exit(0)
|
620 |
+
if getattr(opts, 'portion', None) in {'main', None}:
|
621 |
+
# This loads plugins, allowing setting validation. We want the import
|
622 |
+
# to be after the preprovision step.
|
623 |
+
from girder.utility.server import configureServer
|
624 |
+
|
625 |
+
configureServer()
|
626 |
+
if getattr(opts, 'mongo-compat', None) is not False:
|
627 |
+
from girder.models import getDbConnection
|
628 |
+
|
629 |
+
try:
|
630 |
+
db = getDbConnection()
|
631 |
+
except Exception:
|
632 |
+
logger.warning('Could not connect to mongo.')
|
633 |
+
try:
|
634 |
+
db.admin.command({'setFeatureCompatibilityVersion': '.'.join(
|
635 |
+
db.server_info()['version'].split('.')[:2]), 'confirm': True})
|
636 |
+
except Exception:
|
637 |
+
try:
|
638 |
+
db.admin.command({'setFeatureCompatibilityVersion': '.'.join(
|
639 |
+
db.server_info()['version'].split('.')[:2])})
|
640 |
+
except Exception:
|
641 |
+
logger.warning('Could not set mongo feature compatibility version.')
|
642 |
+
try:
|
643 |
+
# Also attempt to upgrade old version 2 image sources
|
644 |
+
db.girder.item.update_many(
|
645 |
+
{'largeImage.sourceName': 'svs'},
|
646 |
+
{'$set': {'largeImage.sourceName': 'openslide'}})
|
647 |
+
except Exception:
|
648 |
+
logger.warning('Could not update old source names.')
|
649 |
+
provision(opts)
|
dsa/provision.yaml
ADDED
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
# The provision script can take a yaml file with provision options
|
3 |
+
# This is a dictionary of command-line arguments for the provisioning script
|
4 |
+
force: False
|
5 |
+
samples: False
|
6 |
+
sample-collection: Samples
|
7 |
+
sample-folder: Images
|
8 |
+
# Set use-defaults to False to skip default settings
|
9 |
+
use-defaults: True
|
10 |
+
# Set mongo_compat to False to not automatically set the mongo feature
|
11 |
+
# compatibility version to the current server version.
|
12 |
+
mongo-compat: True
|
13 |
+
# A list of additional pip modules to install; if any are girder plugins with
|
14 |
+
# client-side code, also specify rebuild-client.
|
15 |
+
# pip:
|
16 |
+
# - girder-oauth
|
17 |
+
# - girder-ldap
|
18 |
+
# rebuild-client may be False, True (for production mode), or "development"
|
19 |
+
rebuild-client: False
|
20 |
+
# Run additional shell commands before start
|
21 |
+
# shell:
|
22 |
+
# - ls
|
23 |
+
# Default admin user if there are no admin users
|
24 |
+
admin:
|
25 |
+
login: admin
|
26 |
+
password: password
|
27 |
+
firstName: Admin
|
28 |
+
lastName: Admin
|
29 |
+
email: [email protected]
|
30 |
+
public: True
|
31 |
+
# Default assetstore if there are no assetstores
|
32 |
+
assetstore:
|
33 |
+
method: createFilesystemAssetstore
|
34 |
+
name: Assetstore
|
35 |
+
root: /assetstore
|
36 |
+
# Any resources to ensure exist. A model must be specified. This creates the
|
37 |
+
# resource if there is no match for all specified values. A value of
|
38 |
+
# "resource:<path>" is converted to the resource document with that resource
|
39 |
+
# path. "resource:admin" uses the default admin, "resourceid:<path>" is the
|
40 |
+
# string id for the resource path, and "resourceid:admin" is the string if for
|
41 |
+
# default admin.
|
42 |
+
# You can add metadata to a resource. The default key is meta. If
|
43 |
+
# metadata_update is False, metadata will not be set if any metadata
|
44 |
+
# already exists.
|
45 |
+
resources:
|
46 |
+
- model: collection
|
47 |
+
name: Tasks
|
48 |
+
creator: resource:admin
|
49 |
+
public: True
|
50 |
+
- model: folder
|
51 |
+
parent: resource:collection/Tasks
|
52 |
+
parentType: collection
|
53 |
+
name: "Slicer CLI Web Tasks"
|
54 |
+
creator: resource:admin
|
55 |
+
public: True
|
56 |
+
# metadata:
|
57 |
+
# sample_key: sample_value
|
58 |
+
# metadata_key: meta
|
59 |
+
# metadata_update: True
|
60 |
+
settings:
|
61 |
+
worker.broker: "amqp://guest:guest@rabbitmq"
|
62 |
+
worker.backend: "rpc://guest:guest@rabbitmq"
|
63 |
+
worker.api_url: "http://girder:8080/api/v1"
|
64 |
+
worker.direct_path: True
|
65 |
+
core.brand_name: "Digital Slide Archive"
|
66 |
+
histomicsui.webroot_path: "histomics"
|
67 |
+
histomicsui.alternate_webroot_path: "histomicstk"
|
68 |
+
histomicsui.delete_annotations_after_ingest: True
|
69 |
+
homepage.markdown: |-
|
70 |
+
# Digital Slide Archive
|
71 |
+
---
|
72 |
+
## Bioinformatics Platform
|
73 |
+
|
74 |
+
Welcome to the **Digital Slide Archive**.
|
75 |
+
|
76 |
+
Developers who want to use the Girder REST API should check out the
|
77 |
+
[interactive web API docs](api/v1).
|
78 |
+
|
79 |
+
The [HistomicsUI](histomics) application is enabled.
|
80 |
+
slicer_cli_web.task_folder: "resourceid:collection/Tasks/Slicer CLI Web Tasks"
|
81 |
+
# List slicer-cli-images to pull and load
|
82 |
+
slicer-cli-image:
|
83 |
+
- dsarchive/histomicstk:latest
|
84 |
+
# The worker can specify parameters for provisioning
|
85 |
+
# worker-rabbitmq-host: girder:8080
|
86 |
+
worker-rabbitmq-user: guest
|
87 |
+
worker-rabbitmq-pass: guest
|
88 |
+
worker-config: /opt/girder_worker/girder_worker/worker.local.cfg
|
89 |
+
# These have precedence over the top level values
|
90 |
+
worker:
|
91 |
+
# rabbitmq-host: girder:8080
|
92 |
+
# rabbitmq-user: guest
|
93 |
+
# rabbitmq-pass: guest
|
94 |
+
# config: /opt/girder_worker/girder_worker/worker.local.cfg
|
95 |
+
# Install additional pip packages in the worker
|
96 |
+
# pip:
|
97 |
+
# - package_one
|
98 |
+
# Run additional shell commands in the worker before start
|
99 |
+
# shell:
|
100 |
+
# - ls
|
dsa/rabbitmq.advanced.config
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
[ {rabbit, [ {consumer_timeout, undefined} ]} ].
|
dsa/start_girder.sh
ADDED
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/bin/bash
|
2 |
+
# Ensures that the main process runs as the DSA_USER and is part of both that
|
3 |
+
# group and the docker group. Fail if DSA_USER is not specified.
|
4 |
+
if [[ -z "$DSA_USER" ]]; then
|
5 |
+
echo "Set the DSA_USER before starting (e.g, DSA_USER=\$$(id -u):\$$(id -g) <up command>"
|
6 |
+
exit 1
|
7 |
+
fi
|
8 |
+
# add a user with the DSA_USER's id; this user is named ubuntu if it doesn't
|
9 |
+
# exist.
|
10 |
+
adduser --uid ${DSA_USER%%:*} --disabled-password --gecos "" ubuntu 2>/dev/null
|
11 |
+
# add a group with the DSA_USER's group id.
|
12 |
+
addgroup --gid ${DSA_USER#*:} $(id -ng ${DSA_USER#*:}) 2>/dev/null
|
13 |
+
# add the user to the user group.
|
14 |
+
adduser $(id -nu ${DSA_USER%%:*}) $(getent group ${DSA_USER#*:} | cut "-d:" -f1) 2>/dev/null
|
15 |
+
# add a group with the docker group id.
|
16 |
+
addgroup --gid $(stat -c "%g" /var/run/docker.sock) docker 2>/dev/null
|
17 |
+
# add the user to the docker group.
|
18 |
+
adduser $(id -nu ${DSA_USER%%:*}) $(getent group $(stat -c "%g" /var/run/docker.sock) | cut "-d:" -f1) 2>/dev/null
|
19 |
+
# Try to increase permissions for the docker socket; this helps this work on
|
20 |
+
# OSX where the users don't translate
|
21 |
+
chmod 777 /var/run/docker.sock 2>/dev/null || true
|
22 |
+
# Use iptables to make some services appear as if they are on localhost (as
|
23 |
+
# well as on the docker network). This is done to allow tox tests to run.
|
24 |
+
sysctl -w net.ipv4.conf.eth0.route_localnet=1
|
25 |
+
iptables -t nat -A OUTPUT -o lo -p tcp -m tcp --dport 27017 -j DNAT --to-destination `dig +short mongodb`:27017
|
26 |
+
iptables -t nat -A OUTPUT -o lo -p tcp -m tcp --dport 11211 -j DNAT --to-destination `dig +short memcached`:11211
|
27 |
+
iptables -t nat -A POSTROUTING -o eth0 -m addrtype --src-type LOCAL --dst-type UNICAST -j MASQUERADE
|
28 |
+
echo 'PATH="/opt/digital_slide_archive/devops/dsa/utils:/opt/venv/bin:/.pyenv/bin:/.pyenv/shims:$PATH"' >> /home/$(id -nu ${DSA_USER%%:*})/.bashrc
|
29 |
+
echo ==== Pre-Provisioning ===
|
30 |
+
PATH="/opt/venv/bin:/.pyenv/bin:/.pyenv/shims:$PATH" \
|
31 |
+
python /opt/digital_slide_archive/devops/dsa/provision.py -v --pre
|
32 |
+
# Run subsequent commands as the DSA_USER. This sets some paths based on what
|
33 |
+
# is expected in the Docker so that the current python environment and the
|
34 |
+
# devops/dsa/utils are available. Then:
|
35 |
+
# - Provision the Girder instance. This sets values in the database, such as
|
36 |
+
# creating an admin user if there isn't one. See the provision.py script for
|
37 |
+
# the details.
|
38 |
+
# - If possible, set up a girder mount. This allows file-like access of girder
|
39 |
+
# resources. It requires the host to have fuse installed and the docker
|
40 |
+
# container to be run with enough permissions to use fuse.
|
41 |
+
# - Start the main girder process.
|
42 |
+
su $(id -nu ${DSA_USER%%:*}) -c "
|
43 |
+
PATH=\"/opt/digital_slide_archive/devops/dsa/utils:/opt/venv/bin:/.pyenv/bin:/.pyenv/shims:$PATH\";
|
44 |
+
echo ==== Provisioning === &&
|
45 |
+
python /opt/digital_slide_archive/devops/dsa/provision.py -v --main &&
|
46 |
+
echo ==== Creating FUSE mount === &&
|
47 |
+
(girder mount /fuse || true) &&
|
48 |
+
echo ==== Starting Girder === &&
|
49 |
+
girder serve --dev
|
50 |
+
"
|
dsa/start_worker.sh
ADDED
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/bin/bash
|
2 |
+
# Ensures that the main process runs as the DSA_USER and is part of both that
|
3 |
+
# group and the docker group. Fail if DSA_USER is not specified.
|
4 |
+
if [[ -z "$DSA_USER" ]]; then
|
5 |
+
echo "Set the DSA_USER before starting (e.g, DSA_USER=\$$(id -u):\$$(id -g) <up command>"
|
6 |
+
exit 1
|
7 |
+
fi
|
8 |
+
# add a user with the DSA_USER's id; this user is named ubuntu if it doesn't
|
9 |
+
# exist.
|
10 |
+
adduser --uid ${DSA_USER%%:*} --disabled-password --gecos "" ubuntu 2>/dev/null
|
11 |
+
# add a group with the DSA_USER's group id.
|
12 |
+
addgroup --gid ${DSA_USER#*:} $(id -ng ${DSA_USER#*:}) 2>/dev/null
|
13 |
+
# add the user to the user group.
|
14 |
+
adduser $(id -nu ${DSA_USER%%:*}) $(getent group ${DSA_USER#*:} | cut "-d:" -f1) 2>/dev/null
|
15 |
+
# add a group with the docker group id.
|
16 |
+
addgroup --gid $(stat -c "%g" /var/run/docker.sock) docker 2>/dev/null
|
17 |
+
# add the user to the docker group.
|
18 |
+
adduser $(id -nu ${DSA_USER%%:*}) $(getent group $(stat -c "%g" /var/run/docker.sock) | cut "-d:" -f1) 2>/dev/null
|
19 |
+
# Try to increase permissions for the docker socket; this helps this work on
|
20 |
+
# OSX where the users don't translate
|
21 |
+
chmod 777 /var/run/docker.sock 2>/dev/null || true
|
22 |
+
chmod 777 ${TMPDIR:-/tmp} || true
|
23 |
+
echo ==== Pre-Provisioning ===
|
24 |
+
python3 /opt/digital_slide_archive/devops/dsa/provision.py --worker-pre
|
25 |
+
echo ==== Provisioning === &&
|
26 |
+
python3 /opt/digital_slide_archive/devops/dsa/provision.py --worker-main
|
27 |
+
echo ==== Starting Worker === &&
|
28 |
+
# Run subsequent commands as the DSA_USER. This sets some paths based on what
|
29 |
+
# is expected in the Docker so that the current python environment and the
|
30 |
+
# devops/dsa/utils are available. Then it runs girder_worker
|
31 |
+
su $(id -nu ${DSA_USER%%:*}) -c "
|
32 |
+
PATH=\"/opt/digital_slide_archive/devops/dsa/utils:/opt/venv/bin:/.pyenv/bin:/.pyenv/shims:$PATH\";
|
33 |
+
DOCKER_CLIENT_TIMEOUT=86400 TMPDIR=${TMPDIR:-/tmp} GW_DIRECT_PATHS=true python -m girder_worker --concurrency=${DSA_WORKER_CONCURRENCY:-2} -Ofair --prefetch-multiplier=1
|
34 |
+
"
|
dsa/utils/.vimrc
ADDED
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
" see :options
|
2 |
+
" expandtabs
|
3 |
+
set et
|
4 |
+
set tabstop=4
|
5 |
+
" shiftwidth
|
6 |
+
set sw=4
|
7 |
+
set nocindent
|
8 |
+
" autoindent
|
9 |
+
set ai
|
10 |
+
" tell indenting programs that we already indented the buffer
|
11 |
+
let b:did_indent = 1
|
12 |
+
" don't do an incremental search (don't search before we finish typing)
|
13 |
+
set nois
|
14 |
+
" don't ignore case by default
|
15 |
+
set noic
|
16 |
+
" don't break at 80 characters
|
17 |
+
set wrap
|
18 |
+
" don't add linebreaks at 80 characters
|
19 |
+
set nolbr
|
20 |
+
" highlight all search matches
|
21 |
+
set hls
|
22 |
+
" default to utf-8
|
23 |
+
set enc=utf-8
|
24 |
+
" show the cursor position
|
25 |
+
set ruler
|
26 |
+
" allow backspace to go to the previous line
|
27 |
+
set bs=2
|
28 |
+
" keep this much history
|
29 |
+
set history=50
|
30 |
+
" don't try to maintain vi compatibility
|
31 |
+
set nocompatible
|
32 |
+
|
33 |
+
" syntax highlighting is on
|
34 |
+
syntax on
|
35 |
+
" save information for 100 files, with up to 50 lines for each register
|
36 |
+
set viminfo='100,\"50
|
37 |
+
if v:lang =~ "utf8$" || v:lang =~ "UTF-8$"
|
38 |
+
set fileencodings=utf-8,latin1
|
39 |
+
endif
|
dsa/utils/cli_test.py
ADDED
@@ -0,0 +1,313 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/usr/bin/env python
|
2 |
+
|
3 |
+
import argparse
|
4 |
+
import getpass
|
5 |
+
import random
|
6 |
+
import sys
|
7 |
+
import tempfile
|
8 |
+
import time
|
9 |
+
|
10 |
+
import girder_client
|
11 |
+
|
12 |
+
|
13 |
+
def get_girder_client(opts):
|
14 |
+
"""
|
15 |
+
Log in to Girder and return a reference to the client.
|
16 |
+
|
17 |
+
:param opts: options that include the username, password, and girder api
|
18 |
+
url.
|
19 |
+
:returns: the girder client.
|
20 |
+
"""
|
21 |
+
token = opts.get('token')
|
22 |
+
username = opts.get('username')
|
23 |
+
password = opts.get('password')
|
24 |
+
if not username and not token:
|
25 |
+
username = input('Admin login: ')
|
26 |
+
if not password and not token:
|
27 |
+
password = getpass.getpass('Password for %s: ' % (
|
28 |
+
username if username else 'default admin user'))
|
29 |
+
client = girder_client.GirderClient(apiUrl=opts['apiurl'])
|
30 |
+
if token:
|
31 |
+
client.setToken(token)
|
32 |
+
else:
|
33 |
+
client.authenticate(username, password)
|
34 |
+
return client
|
35 |
+
|
36 |
+
|
37 |
+
def get_test_data(client, opts): # noqa
|
38 |
+
"""
|
39 |
+
Make sure we have a test collection with a folder with test data.
|
40 |
+
|
41 |
+
:param client: girder client.
|
42 |
+
:param opts: command line options.
|
43 |
+
"""
|
44 |
+
collName = 'HistomicsTK Tests'
|
45 |
+
try:
|
46 |
+
collection = client.resourceLookup('/collection/' + collName)
|
47 |
+
except Exception:
|
48 |
+
collection = None
|
49 |
+
if not collection:
|
50 |
+
collection = client.createCollection(collName, public=True)
|
51 |
+
folderName = 'Images'
|
52 |
+
try:
|
53 |
+
folder = client.resourceLookup('/collection/%s/%s' % (collName, folderName))
|
54 |
+
except Exception:
|
55 |
+
folder = None
|
56 |
+
if not folder:
|
57 |
+
folder = client.createFolder(collection['_id'], folderName, parentType='collection')
|
58 |
+
remote = girder_client.GirderClient(apiUrl='https://data.kitware.com/api/v1')
|
59 |
+
remoteFolder = remote.resourceLookup('/collection/HistomicsTK/Deployment test images')
|
60 |
+
for item in remote.listItem(remoteFolder['_id']):
|
61 |
+
localPath = '/collection/%s/%s/%s' % (collName, folderName, item['name'])
|
62 |
+
try:
|
63 |
+
localItem = client.resourceLookup(localPath)
|
64 |
+
except Exception:
|
65 |
+
localItem = None
|
66 |
+
if localItem:
|
67 |
+
if opts.get('test') == 'local':
|
68 |
+
continue
|
69 |
+
client.delete('item/%s' % localItem['_id'])
|
70 |
+
localItem = client.createItem(folder['_id'], item['name'])
|
71 |
+
for remoteFile in remote.listFile(item['_id']):
|
72 |
+
with tempfile.NamedTemporaryFile() as tf:
|
73 |
+
fileName = tf.name
|
74 |
+
tf.close()
|
75 |
+
sys.stdout.write('Downloading %s' % remoteFile['name'])
|
76 |
+
sys.stdout.flush()
|
77 |
+
remote.downloadFile(remoteFile['_id'], fileName)
|
78 |
+
sys.stdout.write(' .')
|
79 |
+
sys.stdout.flush()
|
80 |
+
client.uploadFileToItem(
|
81 |
+
localItem['_id'], fileName, filename=remoteFile['name'],
|
82 |
+
mimeType=remoteFile['mimeType'])
|
83 |
+
sys.stdout.write('.\n')
|
84 |
+
sys.stdout.flush()
|
85 |
+
for item in list(client.listItem(folder['_id'])):
|
86 |
+
if '.anot' in item['name']:
|
87 |
+
sys.stdout.write('Deleting %s\n' % item['name'])
|
88 |
+
sys.stdout.flush()
|
89 |
+
client.delete('item/%s' % item['_id'])
|
90 |
+
continue
|
91 |
+
if 'largeImage' not in item:
|
92 |
+
sys.stdout.write('Making large_item %s ' % item['name'])
|
93 |
+
sys.stdout.flush()
|
94 |
+
job = client.post('item/%s/tiles' % item['_id'])
|
95 |
+
if job is not None:
|
96 |
+
job, peak_memory = wait_for_job(client, job)
|
97 |
+
else:
|
98 |
+
print('done')
|
99 |
+
return folder
|
100 |
+
|
101 |
+
|
102 |
+
def install_cli(client, imageName):
|
103 |
+
"""
|
104 |
+
Make sure the specified CLI is installed.
|
105 |
+
|
106 |
+
:param client: girder client.
|
107 |
+
:param imageName: name of the CLI docker image
|
108 |
+
"""
|
109 |
+
client.put('slicer_cli_web/docker_image', data={'name': '["%s"]' % imageName})
|
110 |
+
job = client.get('job/all', parameters={
|
111 |
+
'sort': 'created', 'sortdir': -1,
|
112 |
+
'types': '["slicer_cli_web_job"]',
|
113 |
+
'limit': 1})[0]
|
114 |
+
sys.stdout.write('Adding %s ' % imageName)
|
115 |
+
wait_for_job(client, job)
|
116 |
+
|
117 |
+
|
118 |
+
def get_memory_use(client):
|
119 |
+
"""
|
120 |
+
Get the memory use as reported by the system.
|
121 |
+
|
122 |
+
:return: the system/check virtualMemory['used'] information.
|
123 |
+
"""
|
124 |
+
info = client.get('system/check?mode=quick')
|
125 |
+
return info['virtualMemory']['used']
|
126 |
+
|
127 |
+
|
128 |
+
def test_cli(client, folder, opts):
|
129 |
+
"""
|
130 |
+
Run the CLI on an image and make sure we get an annotation out of it.
|
131 |
+
|
132 |
+
:param client: girder client.
|
133 |
+
:param folder: the parent folder of the test images.
|
134 |
+
:param opts: command line options.
|
135 |
+
"""
|
136 |
+
testItem = None
|
137 |
+
if not opts.get('testid'):
|
138 |
+
for item in client.listItem(folder['_id']):
|
139 |
+
if item['name'].startswith('TCGA-02'):
|
140 |
+
testItem = item
|
141 |
+
break
|
142 |
+
else:
|
143 |
+
testItem = {'_id': opts.get('testid')}
|
144 |
+
localFile = next(client.listFile(testItem['_id']))
|
145 |
+
path = 'slicer_cli_web/%s/NucleiDetection/run' % (
|
146 |
+
opts['cli'].replace('/', '_').replace(':', '_'), )
|
147 |
+
sys.stdout.write('Running %s ' % opts['cli'])
|
148 |
+
sys.stdout.flush()
|
149 |
+
anList = client.get('annotation', parameters={
|
150 |
+
'itemId': testItem['_id'], 'sort': '_id', 'sortdir': -1, 'limit': 1})
|
151 |
+
lastOldAnnotId = None
|
152 |
+
if len(anList):
|
153 |
+
lastOldAnnotId = anList[0]['_id']
|
154 |
+
memory_use = get_memory_use(client)
|
155 |
+
starttime = time.time()
|
156 |
+
region = '[15000,15000,1000,1000]'
|
157 |
+
if opts.get('randomregion'):
|
158 |
+
metadata = client.get('item/%s/tiles' % testItem['_id'])
|
159 |
+
w = metadata['sizeX']
|
160 |
+
h = metadata['sizeY']
|
161 |
+
rw = random.randint(500, 5000)
|
162 |
+
rh = random.randint(500, 5000)
|
163 |
+
region = '[%d,%d,%d,%d]' % (random.randint(0, w - rw), random.randint(0, h - rh), rw, rh)
|
164 |
+
if opts.get('noregion'):
|
165 |
+
region = '[-1,-1,-1,-1]'
|
166 |
+
data = {
|
167 |
+
'inputImageFile': localFile['_id'],
|
168 |
+
'outputNucleiAnnotationFile_folder': folder['_id'],
|
169 |
+
'outputNucleiAnnotationFile': 'cli_test.anot',
|
170 |
+
'analysis_roi': region,
|
171 |
+
'foreground_threshold': '60',
|
172 |
+
'min_fgnd_frac': '0.05',
|
173 |
+
|
174 |
+
'analysis_tile_size': '4096',
|
175 |
+
'nuclei_annotation_format': 'bbox',
|
176 |
+
'max_radius': '30',
|
177 |
+
'min_radius': '20',
|
178 |
+
}
|
179 |
+
if opts.get('testarg') and len(opts.get('testarg')):
|
180 |
+
testarg = {val.split('=', 1)[0]: val.split('=', 1)[1] for val in opts['testarg']}
|
181 |
+
data.update(testarg)
|
182 |
+
if opts.get('verbose', 0) >= 1:
|
183 |
+
sys.stdout.write('%r\n' % data)
|
184 |
+
job = client.post(path, data=data)
|
185 |
+
job, peak_memory = wait_for_job(client, job)
|
186 |
+
runtime = time.time() - starttime
|
187 |
+
# Wait for the annotation to be processed after the job finishes.
|
188 |
+
maxWait = time.time() + 60
|
189 |
+
annot = None
|
190 |
+
while not annot and time.time() < maxWait:
|
191 |
+
anList = client.get('annotation', parameters={
|
192 |
+
'itemId': testItem['_id'], 'sort': '_id', 'sortdir': -1, 'limit': 1})
|
193 |
+
if len(anList) and anList[0]['_id'] != lastOldAnnotId:
|
194 |
+
annot = client.get('annotation/%s' % anList[0]['_id'])
|
195 |
+
break
|
196 |
+
time.sleep(1)
|
197 |
+
sys.stdout.write('Total time: %5.3f, Max memory delta: %d bytes, Elements: %d\n' % (
|
198 |
+
runtime, peak_memory - memory_use, len(annot['annotation']['elements'])))
|
199 |
+
sys.stdout.flush()
|
200 |
+
if len(annot['annotation']['elements']) < 100:
|
201 |
+
raise Exception('Got less than 100 annotation elements (%d) from annotation %s' % (
|
202 |
+
len(annot['annotation']['elements']), anList[0]['_id']))
|
203 |
+
|
204 |
+
|
205 |
+
def test_tiles(client, folder, opts):
|
206 |
+
"""
|
207 |
+
Make sure we have a test collection with a folder with test data.
|
208 |
+
|
209 |
+
:param client: girder client.
|
210 |
+
:param folder: the parent folder of the test images.
|
211 |
+
:param opts: command line options.
|
212 |
+
"""
|
213 |
+
for item in client.listItem(folder['_id']):
|
214 |
+
if 'largeImage' not in item:
|
215 |
+
raise Exception('No large image in item')
|
216 |
+
result = client.get('item/%s/tiles/region' % item['_id'], parameters={
|
217 |
+
'left': 100, 'top': 150, 'right': 400, 'bottom': 450,
|
218 |
+
'encoding': 'PNG',
|
219 |
+
}, jsonResp=False)
|
220 |
+
region = result.content
|
221 |
+
if region[1:4] != b'PNG' or len(region) < 6000:
|
222 |
+
raise Exception('Region did not give expected results')
|
223 |
+
|
224 |
+
|
225 |
+
def wait_for_job(client, job):
|
226 |
+
"""
|
227 |
+
Wait for a job to complete.
|
228 |
+
|
229 |
+
:param client: the girder client.
|
230 |
+
:param job: a girder job.
|
231 |
+
:return: the updated girder job.
|
232 |
+
"""
|
233 |
+
peak_memory_use = get_memory_use(client)
|
234 |
+
lastdot = 0
|
235 |
+
jobId = job['_id']
|
236 |
+
while job['status'] not in (3, 4, 5):
|
237 |
+
if time.time() - lastdot >= 3:
|
238 |
+
sys.stdout.write('.')
|
239 |
+
sys.stdout.flush()
|
240 |
+
lastdot = time.time()
|
241 |
+
time.sleep(0.25)
|
242 |
+
peak_memory_use = max(peak_memory_use, get_memory_use(client))
|
243 |
+
job = client.get('job/%s' % jobId)
|
244 |
+
if job['status'] == 3:
|
245 |
+
print(' ready')
|
246 |
+
else:
|
247 |
+
print(' failed')
|
248 |
+
return job, peak_memory_use
|
249 |
+
|
250 |
+
|
251 |
+
if __name__ == '__main__':
|
252 |
+
parser = argparse.ArgumentParser(
|
253 |
+
description='Download test data for HistomicsTK, and test that basic functions work.')
|
254 |
+
parser.add_argument(
|
255 |
+
'cli',
|
256 |
+
help='A cli docker image name. This is pulled and used in tests.')
|
257 |
+
parser.add_argument(
|
258 |
+
'--apiurl', '--api', '--url', '-a',
|
259 |
+
default='http://127.0.0.1:8080/api/v1', help='The Girder api url.')
|
260 |
+
parser.add_argument(
|
261 |
+
'--password', '--pass', '--passwd', '--pw',
|
262 |
+
help='The Girder admin password. If not specified, a prompt is given.')
|
263 |
+
parser.add_argument(
|
264 |
+
'--username', '--user',
|
265 |
+
help='The Girder admin username. If not specified, a prompt is given.')
|
266 |
+
parser.add_argument(
|
267 |
+
'--token',
|
268 |
+
help='A Girder admin authentication token. If specified, username '
|
269 |
+
'and password are ignored')
|
270 |
+
parser.add_argument(
|
271 |
+
'--no-cli', '--nocli', action='store_true', dest='nocli',
|
272 |
+
help='Do not pull and upload the cli; assume it is already present.')
|
273 |
+
parser.add_argument(
|
274 |
+
'--no-region', '--noregion', '--whole', action='store_true',
|
275 |
+
dest='noregion',
|
276 |
+
help='Run the cli against the whole image (this is slow).')
|
277 |
+
parser.add_argument(
|
278 |
+
'--random-region', '--randomregion', '--random', action='store_true',
|
279 |
+
dest='randomregion',
|
280 |
+
help='Run the cli against a random region on the image (this may be slow).')
|
281 |
+
parser.add_argument(
|
282 |
+
'--test', action='store_true', default=False,
|
283 |
+
help='Download test data and check that basic functions work.')
|
284 |
+
parser.add_argument(
|
285 |
+
'--test-local', '--local-test', '--local', action='store_const',
|
286 |
+
dest='test', const='local',
|
287 |
+
help='Use local test data and check that basic functions work. If '
|
288 |
+
'local data is not present, it is downloaded.')
|
289 |
+
parser.add_argument(
|
290 |
+
'--no-test', action='store_false', dest='test',
|
291 |
+
help='Do not download test data and do not run checks.')
|
292 |
+
parser.add_argument(
|
293 |
+
'--test-id', dest='testid', help='The ID of the item to test.')
|
294 |
+
parser.add_argument(
|
295 |
+
'--test-arg', '--arg', '--testarg', dest='testarg', action='append',
|
296 |
+
help='Test arguments. These should be of the form <key>=<value>.')
|
297 |
+
parser.add_argument(
|
298 |
+
'--only-data', '--data', action='store_const', dest='test',
|
299 |
+
const='data',
|
300 |
+
help='Download test data, but do not run CLI.')
|
301 |
+
parser.add_argument('--verbose', '-v', action='count', default=0)
|
302 |
+
|
303 |
+
args = parser.parse_args()
|
304 |
+
if args.verbose >= 2:
|
305 |
+
print('Parsed arguments: %r' % args)
|
306 |
+
client = get_girder_client(vars(args))
|
307 |
+
if not args.nocli:
|
308 |
+
install_cli(client, args.cli)
|
309 |
+
if args.test:
|
310 |
+
folder = get_test_data(client, vars(args))
|
311 |
+
test_tiles(client, folder, vars(args))
|
312 |
+
if args.test != 'data':
|
313 |
+
test_cli(client, folder, vars(args))
|
dsa/utils/rebuild_and_restart_girder.sh
ADDED
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/usr/bin/env bash
|
2 |
+
|
3 |
+
set -e
|
4 |
+
|
5 |
+
OLDSTART=$(curl --silent 'http://127.0.0.1:8080/api/v1/system/version')
|
6 |
+
girder build --dev
|
7 |
+
touch /etc/girder.cfg
|
8 |
+
echo "Girder has been rebuilt and will now restart"
|
9 |
+
while true; do NEWSTART=$(curl --silent 'http://127.0.0.1:8080/api/v1/system/version' || true); if [ "${OLDSTART}" != "${NEWSTART}" ]; then echo ${NEWSTART} | grep -q 'release' && break || true; fi; sleep 1; echo -n "."; done
|
10 |
+
echo ""
|
11 |
+
echo "Girder has restarted"
|
dsa/utils/restart_girder.sh
ADDED
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/usr/bin/env bash
|
2 |
+
|
3 |
+
OLDSTART=$(curl --silent 'http://127.0.0.1:8080/api/v1/system/version')
|
4 |
+
touch /etc/girder.cfg
|
5 |
+
echo "Girder will now restart"
|
6 |
+
while true; do NEWSTART=$(curl --silent 'http://127.0.0.1:8080/api/v1/system/version' || true); if [ "${OLDSTART}" != "${NEWSTART}" ]; then echo ${NEWSTART} | grep -q 'release' && break || true; fi; sleep 1; echo -n "."; done
|
7 |
+
echo ""
|
8 |
+
echo "Girder has restarted"
|