andreped commited on
Commit
352d280
·
unverified ·
2 Parent(s): 255ed2d db13e57

Merge pull request #1 from andreped/huggingface

Browse files
.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
- # dsa4hf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
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"