├── .gitignore ├── .gitmodules ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── ansible.cfg ├── deploy.yml ├── docs ├── Makefile ├── environment.yml ├── make.bat ├── requirements.txt └── source │ ├── acknowledgment.rst │ ├── conf.py │ ├── configure-nbgrader.rst │ ├── design.rst │ ├── index.rst │ ├── installation.rst │ ├── repo_contents.md │ ├── spelling_wordlist.txt │ ├── teaching-checklist.md │ ├── use-nbgrader.md │ └── use-nbgrader.rst ├── group_vars └── jupyterhub_hosts ├── host_vars └── hostname.example ├── hosts.example ├── readthedocs.yml ├── roles ├── bash │ └── tasks │ │ └── main.yml ├── common │ ├── handlers │ │ └── main.yml │ └── tasks │ │ ├── hostname.yml │ │ ├── main.yml │ │ ├── mounts.yml │ │ ├── ntp.yml │ │ ├── packages.yml │ │ └── ssh.yml ├── cull_idle │ ├── files │ │ └── cull_idle_servers.py │ └── tasks │ │ └── main.yml ├── jupyterhub │ ├── tasks │ │ ├── config.yml │ │ ├── directories.yml │ │ ├── googleanalytics.yml │ │ ├── main.yml │ │ ├── packages.yml │ │ └── supervisor.yml │ └── templates │ │ ├── jupyterhub.conf.j2 │ │ ├── jupyterhub_config.py.j2 │ │ ├── page.html.j2 │ │ ├── start-jupyter-labhub.sh.j2 │ │ └── start-jupyterhub.sh.j2 ├── nbgrader │ └── tasks │ │ └── main.yml ├── newrelic │ └── tasks │ │ └── main.yml ├── nginx │ ├── files │ │ └── letsencrypt-renew │ ├── handlers │ │ └── main.yml │ ├── tasks │ │ └── main.yml │ └── templates │ │ └── nginx.conf.j2 ├── python │ ├── tasks │ │ ├── conda.yml │ │ ├── jupyter.yml │ │ ├── jupyterlab.yml │ │ ├── main.yml │ │ └── python3.yml │ └── vars │ │ └── main.yml ├── r │ └── tasks │ │ └── main.yml ├── saveusers │ ├── files │ │ ├── create_users.py │ │ └── save_users.py │ └── tasks │ │ └── main.yml ├── start_jupyterhub │ └── tasks │ │ └── main.yml └── supervisor │ ├── handlers │ └── main.yml │ └── tasks │ └── main.yml ├── saveusers.yml └── security └── .gitignore /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | 59 | customize.yml 60 | security/ 61 | hosts 62 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "ansible-conda"] 2 | path = ansible-conda 3 | url = https://github.com/UDST/ansible-conda.git 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Welcome! As a [Jupyter](https://jupyter.org) project, we follow the [Jupyter contributor guide](https://jupyter.readthedocs.io/en/latest/contributor/content-contributor.html). 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2017, Project Jupyter Contributors 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deploy JupyterHub for teaching 2 | 3 | [![Google Group](https://img.shields.io/badge/-Google%20Group-lightgrey.svg)](https://groups.google.com/forum/#!forum/jupyter) 4 | [![Documentation Status](http://readthedocs.org/projects/jupyterhub-deploy-teaching/badge/?version=latest)](http://jupyterhub-deploy-teaching.readthedocs.org/en/latest/?badge=latest) 5 | 6 | The goal of this repository is to produce a reference deployment of JupyterHub 7 | for teaching with nbgrader. 8 | 9 | The repository started from [this deployment](https://github.com/calpolydatascience/jupyterhub-deploy-data301) of JupyterHub 10 | for "Introduction to Data Science" at Cal Poly. 11 | It is designed to be a simple and reusable JupyterHub deployment, while 12 | following best practices. 13 | 14 | The main use case targeted is **small to medium groups of trusted users 15 | working on a single server**. 16 | 17 | ## Design goal of this reference deployment 18 | 19 | Create a JupyterHub teaching reference deployment that is simple yet 20 | functional: 21 | 22 | - Use a single server. 23 | - Use Nginx as a frontend proxy, serving static assets, and a termination 24 | point for SSL/TLS. 25 | - Configure using Ansible scripts. 26 | - Use (optionally) https://letsencrypt.org/ for generating SSL certificates. 27 | - Does not use Docker or containers 28 | 29 | ## Prerequisites 30 | 31 | To *deploy* this JupyterHub reference deployment, you should have: 32 | 33 | - An empty Ubuntu server running the latest stable release 34 | - Local drives to be mounted 35 | - A formatted and mounted directory to store user home directories 36 | - A valid DNS name 37 | - SSL certificate 38 | - Ansible 2.1+ installed for JupyterHub configuration (`pip install "ansible>=2.1"`) 39 | - [Verified](https://github.com/jupyterhub/jupyterhub-deploy-teaching/issues/48#issuecomment-277407265) Ansible 2.2.1.0 works with Ubuntu 16.04 and Python3 40 | 41 | For *administration* of the server, you should also: 42 | 43 | - Specify the admin users of JupyterHub. 44 | - Allow SSH key based access to server and add the public SSH keys of GitHub 45 | users who need to be able to SSH to the server as `root` for administration. 46 | 47 | For *managing users and services* on the server, you will: 48 | 49 | - Create "Trusted" users on the system, meaning that you would give them a 50 | user-level shell account on the server 51 | - Authenticate and manage users with either: 52 | * Regular Unix users and PAM. 53 | * GitHub OAuth 54 | - Manage the running of jupyterhub and nbgrader using supervisor. 55 | - Monitor the state of the server (optional feature) using NewRelic or your 56 | cloud provider. 57 | 58 | ## Installation 59 | 60 | Follow the detailed instructions in the [Installation Guide](http://jupyterhub-deploy-teaching.readthedocs.org/en/latest/installation.html). 61 | 62 | The basic steps are: 63 | - Create the hosts group with Fully Qualified Domain Names (FQDNs) of the hosts 64 | - Secure your deployment with SSL 65 | - Deploy with Ansible ``ansible-playbook -i hosts deploy.yml`` 66 | - Verify your deployment and reboot the Hub ``supervisorctl reload`` 67 | 68 | ## Configuring nbgrader 69 | 70 | The nbgrader package is installed when JupyterHub is installed using the 71 | steps in the Installation Guide. 72 | 73 | View the [documentation for detailed configuration steps](http://jupyterhub-deploy-teaching.readthedocs.org/en/latest/configure-nbgrader.html). The basic steps to 74 | configure formgrade or nbgrader's notebook extensions are: 75 | 76 | - activate the extension ``nbgrader extension activate`` 77 | - log into JupyterHub 78 | - run ansible script ``ansible-playbook -i hosts deploy_formgrade.yml`` 79 | - SSH into JupyterHub 80 | - reboot the Hub and nbgrader ``supervisorctl reload`` 81 | 82 | ## Using nbgrader 83 | 84 | With this reference deployment, instructors can start to use nbgrader. 85 | The [Using nbgrader section](http://jupyterhub-deploy-teaching.readthedocs.org/en/latest/use-nbgrader.html) 86 | of the reference deployment documentation gives brief instructions about 87 | creating course assignments, releasing them to students, and grading student 88 | submissions. 89 | 90 | For full details about nbgrader and its features, see the [nbgrader documentation](http://nbgrader.readthedocs.org/en/latest/). 91 | 92 | ## Notes 93 | 94 | ### Ansible configuration and deployment 95 | 96 | Change the ansible configuration by editing ``./ansible_cfg``. 97 | 98 | To limit the deployment to certain hosts, add ``-l hostname`` to the 99 | Ansible deploy commands: 100 | 101 | ``ansible-playbook -i hosts -l hostname deploy.yml`` 102 | 103 | ### Authentication 104 | If you are not using GitHub OAuth, you will need to manually create users 105 | using adduser: ``adduser --gecos "" username``. 106 | 107 | ### Logs 108 | The logs for jupyterhub are in ``/var/log/jupyterhub``. 109 | 110 | The logs for nbgrader are in ``/var/log/nbgrader``. 111 | 112 | ### Starting, stopping, and restarting the Hub 113 | To manage the jupyterhub and nbgrader services by SSH to the server 114 | and run: ``supervisorctl jupyterhub [start|stop|restart]`` 115 | -------------------------------------------------------------------------------- /ansible.cfg: -------------------------------------------------------------------------------- 1 | # Custom configuration settings for Ansible application 2 | 3 | [defaults] 4 | remote_user=ubuntu 5 | library = ./ansible-conda 6 | log_path = ./ansible.log 7 | 8 | [privilege_escalation] 9 | become=True 10 | become_method=sudo 11 | become_user=root 12 | 13 | [ssh_connection] 14 | ssh_args = -o ServerAliveInterval=60 -o ControlMaster=auto -o ControlPersist=10m 15 | -------------------------------------------------------------------------------- /deploy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # The playbook deploys JupyterHub. 3 | # This file would be equivalent to a `sites.yml` file in Ansible terms. 4 | 5 | 6 | - hosts: jupyterhub_hosts 7 | 8 | tasks: 9 | - assert: 10 | that: (ansible_version.major, ansible_version.minor) >= (2, 1) 11 | msg: require ansible >= 2.1, found {{ansible_version.full}} 12 | 13 | - command: echo "$PATH" 14 | register: default_path 15 | 16 | 17 | - hosts: jupyterhub_hosts 18 | 19 | tasks: 20 | - name: Make sure we are running on Ubuntu 21 | fail: msg="This ansible setup only work on Ubuntu" 22 | when: ansible_distribution != 'Ubuntu' 23 | 24 | 25 | - hosts: jupyterhub_hosts 26 | 27 | roles: 28 | - common 29 | - python 30 | - role: r 31 | when: install_r_kernel is defined and install_r_kernel 32 | - role: newrelic 33 | when: newrelic_license_key is defined and newrelic_license_key != '' 34 | - nginx 35 | - supervisor 36 | - saveusers 37 | - role: bash 38 | when: install_bash_kernel is defined and install_bash_kernel 39 | - jupyterhub 40 | - role: cull_idle 41 | when: use_cull_idle_servers is defined and use_cull_idle_servers 42 | - role: nbgrader 43 | when: use_nbgrader is defined and use_nbgrader 44 | - role: start_jupyterhub 45 | environment: 46 | PATH: "/opt/conda/bin:{{ default_path.stdout }}" 47 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don\'t have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) ./source 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help 23 | help: 24 | @echo "Please use \`make ' where is one of" 25 | @echo " html to make standalone HTML files" 26 | @echo " dirhtml to make HTML files named index.html in directories" 27 | @echo " singlehtml to make a single large HTML file" 28 | @echo " pickle to make pickle files" 29 | @echo " json to make JSON files" 30 | @echo " htmlhelp to make HTML files and a HTML help project" 31 | @echo " qthelp to make HTML files and a qthelp project" 32 | @echo " applehelp to make an Apple Help Book" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " epub3 to make an epub3" 36 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 37 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 38 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 39 | @echo " text to make text files" 40 | @echo " man to make manual pages" 41 | @echo " texinfo to make Texinfo files" 42 | @echo " info to make Texinfo files and run them through makeinfo" 43 | @echo " gettext to make PO message catalogs" 44 | @echo " changes to make an overview of all changed/added/deprecated items" 45 | @echo " xml to make Docutils-native XML files" 46 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 47 | @echo " linkcheck to check all external links for integrity" 48 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 49 | @echo " coverage to run coverage check of the documentation (if enabled)" 50 | @echo " dummy to check syntax errors of document sources" 51 | @echo " spelling to run the spell checker" 52 | 53 | .PHONY: clean 54 | clean: 55 | rm -rf $(BUILDDIR)/* 56 | 57 | .PHONY: html 58 | html: 59 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 60 | @echo 61 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 62 | 63 | .PHONY: dirhtml 64 | dirhtml: 65 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 66 | @echo 67 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 68 | 69 | .PHONY: singlehtml 70 | singlehtml: 71 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 72 | @echo 73 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 74 | 75 | .PHONY: pickle 76 | pickle: 77 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 78 | @echo 79 | @echo "Build finished; now you can process the pickle files." 80 | 81 | .PHONY: json 82 | json: 83 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 84 | @echo 85 | @echo "Build finished; now you can process the JSON files." 86 | 87 | .PHONY: htmlhelp 88 | htmlhelp: 89 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 90 | @echo 91 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 92 | ".hhp project file in $(BUILDDIR)/htmlhelp." 93 | 94 | .PHONY: qthelp 95 | qthelp: 96 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 97 | @echo 98 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 99 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 100 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/CalPolyDataScienceDATA301.qhcp" 101 | @echo "To view the help file:" 102 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/CalPolyDataScienceDATA301.qhc" 103 | 104 | .PHONY: applehelp 105 | applehelp: 106 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 107 | @echo 108 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 109 | @echo "N.B. You won't be able to view it unless you put it in" \ 110 | "~/Library/Documentation/Help or install it in your application" \ 111 | "bundle." 112 | 113 | .PHONY: devhelp 114 | devhelp: 115 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 116 | @echo 117 | @echo "Build finished." 118 | @echo "To view the help file:" 119 | @echo "# mkdir -p $$HOME/.local/share/devhelp/CalPolyDataScienceDATA301" 120 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/CalPolyDataScienceDATA301" 121 | @echo "# devhelp" 122 | 123 | .PHONY: epub 124 | epub: 125 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 126 | @echo 127 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 128 | 129 | .PHONY: epub3 130 | epub3: 131 | $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 132 | @echo 133 | @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." 134 | 135 | .PHONY: latex 136 | latex: 137 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 138 | @echo 139 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 140 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 141 | "(use \`make latexpdf' here to do that automatically)." 142 | 143 | .PHONY: latexpdf 144 | latexpdf: 145 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 146 | @echo "Running LaTeX files through pdflatex..." 147 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 148 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 149 | 150 | .PHONY: latexpdfja 151 | latexpdfja: 152 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 153 | @echo "Running LaTeX files through platex and dvipdfmx..." 154 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 155 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 156 | 157 | .PHONY: text 158 | text: 159 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 160 | @echo 161 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 162 | 163 | .PHONY: man 164 | man: 165 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 166 | @echo 167 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 168 | 169 | .PHONY: texinfo 170 | texinfo: 171 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 172 | @echo 173 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 174 | @echo "Run \`make' in that directory to run these through makeinfo" \ 175 | "(use \`make info' here to do that automatically)." 176 | 177 | .PHONY: info 178 | info: 179 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 180 | @echo "Running Texinfo files through makeinfo..." 181 | make -C $(BUILDDIR)/texinfo info 182 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 183 | 184 | .PHONY: gettext 185 | gettext: 186 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 187 | @echo 188 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 189 | 190 | .PHONY: changes 191 | changes: 192 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 193 | @echo 194 | @echo "The overview file is in $(BUILDDIR)/changes." 195 | 196 | .PHONY: linkcheck 197 | linkcheck: 198 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 199 | @echo 200 | @echo "Link check complete; look for any errors in the above output " \ 201 | "or in $(BUILDDIR)/linkcheck/output.txt." 202 | 203 | .PHONY: doctest 204 | doctest: 205 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 206 | @echo "Testing of doctests in the sources finished, look at the " \ 207 | "results in $(BUILDDIR)/doctest/output.txt." 208 | 209 | .PHONY: coverage 210 | coverage: 211 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 212 | @echo "Testing of coverage in the sources finished, look at the " \ 213 | "results in $(BUILDDIR)/coverage/python.txt." 214 | 215 | .PHONY: xml 216 | xml: 217 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 218 | @echo 219 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 220 | 221 | .PHONY: pseudoxml 222 | pseudoxml: 223 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 224 | @echo 225 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 226 | 227 | .PHONY: dummy 228 | dummy: 229 | $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy 230 | @echo 231 | @echo "Build finished. Dummy builder generates no files." 232 | 233 | .PHONE: spelling 234 | spelling: 235 | $(SPHINXBUILD) -b spelling $(ALLSPHINXOPTS) $(BUILDDIR)/spelling 236 | @echo 237 | @echo "Spell check complete; look for any errors in the above output " \ 238 | "or in $(BUILDDIR)/spelling/output.txt." 239 | -------------------------------------------------------------------------------- /docs/environment.yml: -------------------------------------------------------------------------------- 1 | name: jupyterhub_teaching 2 | channels: 3 | - conda-forge 4 | - defaults 5 | dependencies: 6 | - sphinx>=1.4.6 7 | - sphinx_rtd_theme 8 | - pip: 9 | - recommonmark==0.4.0 10 | - pyenchant 11 | - sphinxcontrib-spelling 12 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. epub3 to make an epub3 31 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 32 | echo. text to make text files 33 | echo. man to make manual pages 34 | echo. texinfo to make Texinfo files 35 | echo. gettext to make PO message catalogs 36 | echo. changes to make an overview over all changed/added/deprecated items 37 | echo. xml to make Docutils-native XML files 38 | echo. pseudoxml to make pseudoxml-XML files for display purposes 39 | echo. linkcheck to check all external links for integrity 40 | echo. doctest to run all doctests embedded in the documentation if enabled 41 | echo. coverage to run coverage check of the documentation if enabled 42 | echo. dummy to check syntax errors of document sources 43 | goto end 44 | ) 45 | 46 | if "%1" == "clean" ( 47 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 48 | del /q /s %BUILDDIR%\* 49 | goto end 50 | ) 51 | 52 | 53 | REM Check if sphinx-build is available and fallback to Python version if any 54 | %SPHINXBUILD% 1>NUL 2>NUL 55 | if errorlevel 9009 goto sphinx_python 56 | goto sphinx_ok 57 | 58 | :sphinx_python 59 | 60 | set SPHINXBUILD=python -m sphinx.__init__ 61 | %SPHINXBUILD% 2> nul 62 | if errorlevel 9009 ( 63 | echo. 64 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 65 | echo.installed, then set the SPHINXBUILD environment variable to point 66 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 67 | echo.may add the Sphinx directory to PATH. 68 | echo. 69 | echo.If you don't have Sphinx installed, grab it from 70 | echo.http://sphinx-doc.org/ 71 | exit /b 1 72 | ) 73 | 74 | :sphinx_ok 75 | 76 | 77 | if "%1" == "html" ( 78 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 79 | if errorlevel 1 exit /b 1 80 | echo. 81 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 82 | goto end 83 | ) 84 | 85 | if "%1" == "dirhtml" ( 86 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 87 | if errorlevel 1 exit /b 1 88 | echo. 89 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 90 | goto end 91 | ) 92 | 93 | if "%1" == "singlehtml" ( 94 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 95 | if errorlevel 1 exit /b 1 96 | echo. 97 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 98 | goto end 99 | ) 100 | 101 | if "%1" == "pickle" ( 102 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 103 | if errorlevel 1 exit /b 1 104 | echo. 105 | echo.Build finished; now you can process the pickle files. 106 | goto end 107 | ) 108 | 109 | if "%1" == "json" ( 110 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 111 | if errorlevel 1 exit /b 1 112 | echo. 113 | echo.Build finished; now you can process the JSON files. 114 | goto end 115 | ) 116 | 117 | if "%1" == "htmlhelp" ( 118 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 119 | if errorlevel 1 exit /b 1 120 | echo. 121 | echo.Build finished; now you can run HTML Help Workshop with the ^ 122 | .hhp project file in %BUILDDIR%/htmlhelp. 123 | goto end 124 | ) 125 | 126 | if "%1" == "qthelp" ( 127 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 128 | if errorlevel 1 exit /b 1 129 | echo. 130 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 131 | .qhcp project file in %BUILDDIR%/qthelp, like this: 132 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\CalPolyDataScienceDATA301.qhcp 133 | echo.To view the help file: 134 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\CalPolyDataScienceDATA301.ghc 135 | goto end 136 | ) 137 | 138 | if "%1" == "devhelp" ( 139 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 140 | if errorlevel 1 exit /b 1 141 | echo. 142 | echo.Build finished. 143 | goto end 144 | ) 145 | 146 | if "%1" == "epub" ( 147 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 148 | if errorlevel 1 exit /b 1 149 | echo. 150 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 151 | goto end 152 | ) 153 | 154 | if "%1" == "epub3" ( 155 | %SPHINXBUILD% -b epub3 %ALLSPHINXOPTS% %BUILDDIR%/epub3 156 | if errorlevel 1 exit /b 1 157 | echo. 158 | echo.Build finished. The epub3 file is in %BUILDDIR%/epub3. 159 | goto end 160 | ) 161 | 162 | if "%1" == "latex" ( 163 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 164 | if errorlevel 1 exit /b 1 165 | echo. 166 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 167 | goto end 168 | ) 169 | 170 | if "%1" == "latexpdf" ( 171 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 172 | cd %BUILDDIR%/latex 173 | make all-pdf 174 | cd %~dp0 175 | echo. 176 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 177 | goto end 178 | ) 179 | 180 | if "%1" == "latexpdfja" ( 181 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 182 | cd %BUILDDIR%/latex 183 | make all-pdf-ja 184 | cd %~dp0 185 | echo. 186 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 187 | goto end 188 | ) 189 | 190 | if "%1" == "text" ( 191 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 192 | if errorlevel 1 exit /b 1 193 | echo. 194 | echo.Build finished. The text files are in %BUILDDIR%/text. 195 | goto end 196 | ) 197 | 198 | if "%1" == "man" ( 199 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 200 | if errorlevel 1 exit /b 1 201 | echo. 202 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 203 | goto end 204 | ) 205 | 206 | if "%1" == "texinfo" ( 207 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 208 | if errorlevel 1 exit /b 1 209 | echo. 210 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 211 | goto end 212 | ) 213 | 214 | if "%1" == "gettext" ( 215 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 216 | if errorlevel 1 exit /b 1 217 | echo. 218 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 219 | goto end 220 | ) 221 | 222 | if "%1" == "changes" ( 223 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 224 | if errorlevel 1 exit /b 1 225 | echo. 226 | echo.The overview file is in %BUILDDIR%/changes. 227 | goto end 228 | ) 229 | 230 | if "%1" == "linkcheck" ( 231 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 232 | if errorlevel 1 exit /b 1 233 | echo. 234 | echo.Link check complete; look for any errors in the above output ^ 235 | or in %BUILDDIR%/linkcheck/output.txt. 236 | goto end 237 | ) 238 | 239 | if "%1" == "doctest" ( 240 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 241 | if errorlevel 1 exit /b 1 242 | echo. 243 | echo.Testing of doctests in the sources finished, look at the ^ 244 | results in %BUILDDIR%/doctest/output.txt. 245 | goto end 246 | ) 247 | 248 | if "%1" == "coverage" ( 249 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 250 | if errorlevel 1 exit /b 1 251 | echo. 252 | echo.Testing of coverage in the sources finished, look at the ^ 253 | results in %BUILDDIR%/coverage/python.txt. 254 | goto end 255 | ) 256 | 257 | if "%1" == "xml" ( 258 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 259 | if errorlevel 1 exit /b 1 260 | echo. 261 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 262 | goto end 263 | ) 264 | 265 | if "%1" == "pseudoxml" ( 266 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 267 | if errorlevel 1 exit /b 1 268 | echo. 269 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 270 | goto end 271 | ) 272 | 273 | if "%1" == "dummy" ( 274 | %SPHINXBUILD% -b dummy %ALLSPHINXOPTS% %BUILDDIR%/dummy 275 | if errorlevel 1 exit /b 1 276 | echo. 277 | echo.Build finished. Dummy builder generates no files. 278 | goto end 279 | ) 280 | 281 | :end 282 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx>=1.4.6 2 | sphinx_rtd_theme 3 | recommonmark 4 | pyenchant 5 | sphinxcontrib-spelling 6 | -------------------------------------------------------------------------------- /docs/source/acknowledgment.rst: -------------------------------------------------------------------------------- 1 | Acknowledgment 2 | ============== 3 | 4 | Prof. Brian Granger, Cal Poly San Luis Obispo, authored this repository's 5 | code to deploy JupyterHub for the course, DATA 301, "Introduction to Data 6 | Science." 7 | 8 | Thank you Brian Granger and Jonathan Fredric, co-author of an earlier code 9 | prototype, for sharing their work. -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | import sys 5 | import os 6 | import shlex 7 | 8 | # For conversion from markdown to html 9 | import recommonmark.parser 10 | 11 | # Set paths 12 | #sys.path.insert(0, os.path.abspath('.')) 13 | 14 | # -- General configuration ------------------------------------------------ 15 | 16 | # Minimal Sphinx version 17 | needs_sphinx = '1.4' 18 | 19 | # Sphinx extension modules 20 | extensions = [ 21 | 'sphinx.ext.autodoc', 22 | 'sphinx.ext.intersphinx', 23 | 'sphinx.ext.mathjax', 24 | ] 25 | 26 | # The master toctree document. 27 | master_doc = 'index' 28 | 29 | # General information about the project. 30 | project = 'JupyterHub for Teaching' 31 | copyright = '2016, Project Jupyter' 32 | author = 'Project Jupyter' 33 | 34 | # The version info for the project 35 | # The short X.Y version. 36 | version = '1.0' 37 | # The full version, including alpha/beta/rc tags. 38 | release = '1.0' 39 | 40 | language = None 41 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', 'ansible-conda'] 42 | pygments_style = 'sphinx' 43 | todo_include_todos = False 44 | 45 | # -- Source ------------------------------------------------------------- 46 | 47 | source_parsers = { 48 | '.md': 'recommonmark.parser.CommonMarkParser', 49 | } 50 | 51 | source_suffix = ['.rst', '.md'] 52 | #source_encoding = 'utf-8-sig' 53 | 54 | # -- Options for HTML output ---------------------------------------------- 55 | 56 | # The theme to use for HTML and HTML Help pages. 57 | html_theme = 'sphinx_rtd_theme' 58 | 59 | #html_theme_options = {} 60 | #html_theme_path = [] 61 | #html_title = None 62 | #html_short_title = None 63 | #html_logo = None 64 | #html_favicon = None 65 | 66 | # Paths that contain custom static files (such as style sheets) 67 | #html_static_path = ['_static'] 68 | 69 | #html_extra_path = [] 70 | #html_last_updated_fmt = None 71 | #html_use_smartypants = True 72 | #html_sidebars = {} 73 | #html_additional_pages = {} 74 | #html_domain_indices = True 75 | #html_use_index = True 76 | #html_split_index = False 77 | #html_show_sourcelink = True 78 | 79 | #Output file base name for HTML help builder. 80 | htmlhelp_basename = 'JupyterHubTeaching' 81 | 82 | # -- Options for LaTeX output --------------------------------------------- 83 | 84 | latex_elements = { 85 | #'papersize': 'letterpaper', 86 | #'pointsize': '10pt', 87 | #'preamble': '', 88 | #'figure_align': 'htbp', 89 | } 90 | 91 | # Grouping the document tree into LaTeX files. List of tuples 92 | # (source start file, target name, title, 93 | # author, documentclass [howto, manual, or own class]). 94 | latex_documents = [ 95 | (master_doc, 'JupyterHubTeaching.tex', 'JupyterHub for Teaching', 96 | 'Project Jupyter', 'manual'), 97 | ] 98 | 99 | #latex_logo = None 100 | #latex_use_parts = False 101 | #latex_show_pagerefs = False 102 | #latex_show_urls = False 103 | #latex_appendices = [] 104 | #latex_domain_indices = True 105 | 106 | 107 | # -- Options for manual page output --------------------------------------- 108 | 109 | # One entry per manual page. List of tuples 110 | # (source start file, name, description, authors, manual section). 111 | man_pages = [ 112 | (master_doc, 'JupyterHubTeaching', 'JupyterHub for Teaching', 113 | [author], 1) 114 | ] 115 | 116 | #man_show_urls = False 117 | 118 | 119 | # -- Options for Texinfo output ------------------------------------------- 120 | 121 | # Grouping the document tree into Texinfo files. List of tuples 122 | # (source start file, target name, title, author, 123 | # dir menu entry, description, category) 124 | texinfo_documents = [ 125 | (master_doc, 'JupyterHubTeaching', 'JupyterHub for Teaching Documentation', 126 | [author], 'JupyterHubTeaching', 'Reference deployment', 'Miscellaneous') 127 | ] 128 | 129 | #texinfo_appendices = [] 130 | #texinfo_domain_indices = True 131 | #texinfo_show_urls = 'footnote' 132 | #texinfo_no_detailmenu = False 133 | 134 | # -- Epub output -------------------------------------------------------- 135 | 136 | # Bibliographic Dublin Core info. 137 | epub_title = project 138 | epub_author = author 139 | epub_publisher = author 140 | epub_copyright = copyright 141 | 142 | # A list of files that should not be packed into the epub file. 143 | epub_exclude_files = ['search.html'] 144 | 145 | # -- Intersphinx ---------------------------------------------------------- 146 | 147 | # Example configuration for intersphinx: refer to the Python standard library. 148 | intersphinx_mapping = {'https://docs.python.org/': None} 149 | 150 | # -- Read The Docs -------------------------------------------------------- 151 | 152 | on_rtd = os.environ.get('READTHEDOCS', None) == 'True' 153 | 154 | if not on_rtd: 155 | # only import and set the theme if we're building docs locally 156 | import sphinx_rtd_theme 157 | html_theme = 'sphinx_rtd_theme' 158 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 159 | 160 | # readthedocs.org uses their theme by default, so no need to specify it 161 | 162 | # -- Spell checking ------------------------------------------------------- 163 | 164 | try: 165 | import sphinxcontrib.spelling 166 | except ImportError: 167 | pass 168 | else: 169 | extensions.append("sphinxcontrib.spelling") 170 | 171 | spelling_word_list_filename='spelling_wordlist.txt' 172 | -------------------------------------------------------------------------------- /docs/source/configure-nbgrader.rst: -------------------------------------------------------------------------------- 1 | Configuring nbgrader 2 | ==================== 3 | 4 | The nbgrader package will be installed with the reference deployment. 5 | 6 | To run nbgrader's formgrade application or use its notebook 7 | extensions, additional steps are needed. 8 | 9 | Deploy formgrade 10 | ---------------- 11 | 12 | First, edit the :file:`deploy_formgrade.yml` file with the information 13 | for each course you want to start formgrade for. Each course should have a 14 | unique `nbgrader_course_id` and `nbgrader_port`. 15 | 16 | Second, make sure that each main instructor (the `nbgrader_owner` for each 17 | course) has logged into JupyterHub at least once. This ensures that their 18 | home directory has been created. The home directory of the main instructor 19 | is used for the main nbgrader course files. It is assumed that the main 20 | instructor will be running the nbgrader command line programs. 21 | 22 | Third, run the ansible-playbook to deploy formgrade:: 23 | 24 | $ ansible-playbook -i hosts deploy_formgrade.yml 25 | 26 | Fourth, SSH into the JupyterHub server:: 27 | 28 | $ ssh {user}@{hostname} 29 | 30 | Finally, restart jupyterhub and nbgrader by doing:: 31 | 32 | $ supervisorctl reload 33 | 34 | 35 | Configuration notes 36 | ------------------- 37 | 38 | * To limit the deployment to certain hosts, add the ``-l hostname`` to the 39 | commands:: 40 | 41 | $ ansible-playbook -i hosts -l hostname deploy.yml 42 | 43 | * The logs for `jupyterhub` are in :file:`/var/log/jupyterhub`. 44 | * The logs for `nbgrader` are in :file:`/var/log/nbgrader`. 45 | * If you are not using GitHub OAuth, you will need to manually create users using 46 | `adduser`:: 47 | 48 | $ adduser --gecos "" username 49 | 50 | * Change the ansible configuration by editing :file:`./ansible_cfg`. 51 | * To manage the jupyterhub and nbgrader services by SSH to the server and run:: 52 | 53 | $ supervisorctl jupyterhub { start, stop, restart } 54 | 55 | Troubleshooting: Saving and restoring users 56 | ------------------------------------------- 57 | 58 | In some situations, you may remount your user's home directories into a new instance that 59 | doesn't have their user accounts, but has their home directories. When recreating the 60 | same users it is important that they all have the same uids so the new users have 61 | ownership of the home directories. 62 | 63 | **This is only relevant when using GitHub OAuth for users and authentication.** 64 | 65 | To save the list of usernames and uids in `{{homedir}}/saved_users.txt`:: 66 | 67 | $ ansible-playbook -i hosts saveusers.yml 68 | 69 | Then, when you run deploy.yml, it will look for this file and if it exists, will create 70 | those users with those exact uids and home directories. 71 | 72 | You can also manually create the users by running:: 73 | 74 | $ python3 create_users.py 75 | 76 | in the home directory. 77 | -------------------------------------------------------------------------------- /docs/source/design.rst: -------------------------------------------------------------------------------- 1 | Design goals 2 | ============ 3 | 4 | Instructors and maintainers 5 | --------------------------- 6 | 7 | When using this repository to deploy JupyterHub and nbgrader, individuals 8 | should be able to have a deployment that is as simple as possible: 9 | 10 | - No Docker use. 11 | - `NGINX `_ as a frontend proxy, serving static 12 | assets, and a termination point for SSL/TLS. 13 | - A single server. 14 | - `Ansible `_ for configuration. 15 | - Optionally, use `Let's Encrypt `_ for 16 | generating SSL certificates. 17 | 18 | JupyterHub 19 | ~~~~~~~~~~ 20 | 21 | * Start from: 22 | 23 | - An empty Ubuntu latest stable server with SSH key based access. 24 | - A valid DNS name. 25 | - A formatted and mounted directory to use for user home directories. 26 | - The assumption that all users of the system will be "trusted," meaning 27 | that you would given them a user-level shell account on the server. 28 | 29 | * Always have SSL/TLS enabled. 30 | * Specify local drives to be mounted. 31 | * Manage the running of jupyterhub and nbgrader using supervisor. 32 | * Optionally, monitor the state of the server and set email alerts using 33 | `NewRelic `_. The built-in monitoring of your cloud 34 | provider may also be used. 35 | * Specify admin users of JupyterHub. 36 | * Add the public SSH keys of GitHub users who need to be able to ``ssh`` to 37 | the server as ``root`` for administration. 38 | * Manage users and authentication using either: 39 | 40 | - Regular Unix users and `PAM (Pluggable authentication modules) `_ 41 | - `GitHub OAuth `_ 42 | 43 | nbgrader 44 | ~~~~~~~~ 45 | * Run nbgrader and configure: 46 | 47 | - The course name. 48 | - The instructors username. 49 | - Graders' usernames. 50 | - The location of the nbgrader config. 51 | 52 | Students 53 | -------- 54 | End users of this deployment should be able to: 55 | 56 | * Use the following Jupyter kernels. 57 | 58 | - `Python version 3 `_ using the IPython kernel 59 | with the main Python libraries for data science. 60 | - Bash kernel 61 | 62 | * Sign in using their GitHub or Unix credentials. 63 | * Have a persistent home directory. 64 | * Have outbound network access. 65 | 66 | .. _`Let's Encrypt `: 67 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | JupyterHub for Teaching 2 | ======================= 3 | 4 | Version: |version| 5 | 6 | Date: |today| 7 | 8 | .. _abstract-teaching: 9 | 10 | Abstract 11 | -------- 12 | This deployment is designed for teaching a small to medium 13 | group of trusted users. 14 | 15 | As a simple, reusable JupyterHub deployment for your reference, this 16 | repository enables installation and deployment of JupyterHub and nbgrader 17 | on a single server. The reference deployment follows best practices and 18 | has been used by Professor Brian Granger when teaching "Introduction to 19 | Data Science". 20 | 21 | .. _content: 22 | 23 | Contents 24 | -------- 25 | .. toctree:: 26 | :maxdepth: 2 27 | 28 | design.rst 29 | installation.rst 30 | configure-nbgrader.rst 31 | use-nbgrader.rst 32 | teaching-checklist.md 33 | acknowledgment.rst 34 | repo_contents.md 35 | -------------------------------------------------------------------------------- /docs/source/installation.rst: -------------------------------------------------------------------------------- 1 | Installation Guide 2 | ================== 3 | 4 | Prerequisites 5 | ------------- 6 | 7 | - Start a server running latest Ubuntu version. 8 | 9 | - Enable password-less SSH access for :command:`root` user. 10 | 11 | - Partition and format any local disks you want to mount. 12 | 13 | - Verify a valid DNS entry for the server. 14 | 15 | - Choose an SSL certificate source. Use either of these options: 16 | 17 | * `Let's Encrypt `_ 18 | * obtain a trusted SSL certificate and key for the server at that FQDN. 19 | 20 | - Checkout the latest version of the repository including the ``ansible-conda`` submodule:: 21 | 22 | $ git clone --recursive https://github.com/jupyterhub/jupyterhub-deploy-teaching.git 23 | 24 | Create the hosts group 25 | ---------------------- 26 | 27 | 1. Edit the :file:`./hosts` file to lists the FQDN's of the hosts in the 28 | ``jupyterhub_hosts`` group. 29 | 30 | 2. Create for each host a file in :file:`./host_vars` directory with the 31 | name of the host, starting from :file:`./host_vars/hostname.example`. 32 | 33 | Secure your deployment 34 | ---------------------- 35 | 36 | 1. Create a cookie secret file, :file:`./security/cookie_secret`, using:: 37 | 38 | $ openssl rand -hex 1024 > ./security/cookie_secret 39 | 40 | For additional information, see the `cookie secret file `_ section in the JupyterHub documentation. 41 | 42 | 2. If you are using `Let's Encrypt `_, skip this step. 43 | Otherwise, install your SSL private key :file:`./security/ssl.key` and 44 | certificate as :file:`./security/ssl.crt`. 45 | 46 | Deploy with Ansible 47 | ------------------- 48 | 49 | 1. Run :file:`ansible-playbook` for the main deployment:: 50 | 51 | $ ansible-playbook -i hosts deploy.yml 52 | 53 | Verify your deployment 54 | ---------------------- 55 | 56 | 1. SSH into the server:: 57 | 58 | $ ssh root@{hostname} 59 | 60 | substituting your hostname for {hostname}. For example, ``ssh root@jupyter.org``. 61 | 62 | 2. Reload supervisor:: 63 | 64 | $ supervisorctl reload 65 | -------------------------------------------------------------------------------- /docs/source/repo_contents.md: -------------------------------------------------------------------------------- 1 | # Repository Contents 2 | 3 | ## Ansible application 4 | 5 | ### ansible.cfg 6 | 7 | Custom configuration settings for the Ansible application 8 | - We use to customize root access, root privileges, and ssh connection length. 9 | 10 | ### ansible-conda 11 | 12 | Git submodule for `ansible-conda` application 13 | 14 | ## Inventory (Ansible) 15 | 16 | ### hosts.inventory 17 | 18 | Inventory file of servers (hosts) being managed by Ansible 19 | 20 | ## Playbooks (Ansible) 21 | 22 | ### deploy.yml (a.k.a. site.yml in Ansible jargon) 23 | 24 | ### deploy_formgrade.yml 25 | 26 | ### saveusers.yml 27 | 28 | ## Variables (Ansible) 29 | 30 | ### group_vars 31 | 32 | ### host_vars 33 | 34 | ## Roles (Ansible) 35 | 36 | ### bash 37 | 38 | ### common 39 | 40 | ### cull_idle 41 | 42 | ### formgrade 43 | 44 | ### jupyterhub 45 | 46 | ### nbgrader 47 | 48 | ### newrelic 49 | 50 | ### nginx 51 | 52 | ### python 53 | 54 | ### r 55 | 56 | ### saveusers 57 | 58 | ### supervisor 59 | 60 | 61 | ## Development 62 | 63 | ### .gitignore 64 | 65 | ### .gitmodules 66 | 67 | ### LICENSE 68 | 69 | ### README.md 70 | 71 | ## Documentation 72 | 73 | ### readthedocs.yml 74 | 75 | Settings for readthedocs services 76 | 77 | ### docs 78 | 79 | Directory containing sphinx documentation for the reference deployment. 80 | -------------------------------------------------------------------------------- /docs/source/spelling_wordlist.txt: -------------------------------------------------------------------------------- 1 | admin 2 | Afterwards 3 | alchemyst 4 | alope 5 | Ansible 6 | ansible 7 | api 8 | API 9 | apps 10 | args 11 | asctime 12 | auth 13 | authenticator 14 | Authenticator 15 | authenticators 16 | Authenticators 17 | Autograde 18 | autograde 19 | autogradeapp 20 | autograded 21 | Autograded 22 | autograder 23 | Autograder 24 | autograding 25 | backends 26 | Bitdiddle 27 | bugfix 28 | Bugfixes 29 | bugtracker 30 | Carreau 31 | Changelog 32 | changelog 33 | checksum 34 | checksums 35 | cmd 36 | cogsci 37 | conda 38 | config 39 | coroutine 40 | coroutines 41 | crt 42 | customizable 43 | datefmt 44 | decrypted 45 | dev 46 | DockerSpawner 47 | dockerspawner 48 | dropdown 49 | duedate 50 | Duedate 51 | ellachao 52 | ellisonbg 53 | entrypoint 54 | env 55 | Filenames 56 | filesystem 57 | formatters 58 | formdata 59 | formgrade 60 | frontend 61 | gif 62 | GitHub 63 | Gradebook 64 | gradebook 65 | Granger 66 | hardcoded 67 | hOlle 68 | Homebrew 69 | hostname 70 | html 71 | http 72 | https 73 | hubapi 74 | Indices 75 | index 76 | IFramed 77 | inline 78 | iopub 79 | ip 80 | ipynb 81 | IPython 82 | ischurov 83 | ivanslapnicar 84 | jdfreder 85 | jhamrick 86 | jklymak 87 | jonathanmorgan 88 | joschu 89 | JUPYTER 90 | Jupyter 91 | jupyter 92 | jupyterhub 93 | Kerberos 94 | kerberos 95 | letsencrypt 96 | lgpage 97 | linkcheck 98 | linux 99 | localhost 100 | logfile 101 | login 102 | logins 103 | logout 104 | lookup 105 | lphk 106 | mandli 107 | Marr 108 | mathjax 109 | matplotlib 110 | metadata 111 | mikebolt 112 | minrk 113 | Mitigations 114 | mixin 115 | Mixin 116 | multi 117 | multiuser 118 | namespace 119 | nbconvert 120 | nbgrader 121 | neuroscience 122 | nginx 123 | np 124 | npm 125 | oauth 126 | OAuth 127 | oauthenticator 128 | Obispo 129 | ok 130 | olgabot 131 | osx 132 | PAM 133 | phantomjs 134 | Phantomjs 135 | playbook 136 | plugin 137 | plugins 138 | Poly 139 | Popen 140 | positionally 141 | postgres 142 | pregenerated 143 | prepend 144 | prepopulate 145 | preprocessor 146 | Preprocessor 147 | prev 148 | Programmatically 149 | programmatically 150 | ps 151 | py 152 | Qualys 153 | quickstart 154 | readonly 155 | redSlug 156 | reinstall 157 | resize 158 | rst 159 | runtime 160 | rw 161 | sandboxed 162 | sansary 163 | singleuser 164 | smeylan 165 | spawner 166 | Spawner 167 | spawners 168 | Spawners 169 | spellcheck 170 | SQL 171 | sqlite 172 | startup 173 | statsd 174 | stdin 175 | stdout 176 | stoppped 177 | subclasses 178 | subcommand 179 | subdomain 180 | subdomains 181 | Subdomains 182 | suchow 183 | suprocesses 184 | svurens 185 | sys 186 | SystemUserSpawner 187 | systemwide 188 | tasilb 189 | teardown 190 | threadsafe 191 | timestamp 192 | timestamps 193 | TLD 194 | todo 195 | toolbar 196 | traitlets 197 | travis 198 | tuples 199 | Ubuntu 200 | uids 201 | undeletable 202 | unicode 203 | uninstall 204 | UNIX 205 | unix 206 | untracked 207 | untrusted 208 | url 209 | username 210 | usernames 211 | utcnow 212 | utils 213 | vinaykola 214 | virtualenv 215 | whitelist 216 | whitespace 217 | wildcard 218 | Wildcards 219 | willingc 220 | wordlist 221 | Workflow 222 | workflow 223 | yml 224 | -------------------------------------------------------------------------------- /docs/source/teaching-checklist.md: -------------------------------------------------------------------------------- 1 | # Checklist for a JupyterHub teaching deployment 2 | 3 | Documentation for teaching deployment: https://jupyterhub-deploy-teaching.readthedocs.io 4 | 5 | Documentation for JupyterHub: https://jupyterhub.readthedocs.io 6 | 7 | ## Notes 8 | 9 | - Does **not** use Docker. 10 | - [NGINX](https://www.nginx.com) as a frontend proxy, for serving static 11 | assets, and a termination point for SSL/TLS. 12 | - Single Ubuntu server 13 | - [Ansible](https://www.ansible.com/resources) for configuration. 14 | 15 | ## 1. Prepare the server 16 | 17 | - [ ] **Server:** running latest Ubuntu version 18 | - [ ] **SSH:** enable password-less SSH for `ubuntu` user 19 | - [ ] **Local disks:** partition and format 20 | - [ ] **DNS (domain name):** valid entry for server 21 | 22 | ## 2. Install JupyterHub source 23 | 24 | - [ ] **Source:** Clone latest `jupyterhub-deploy-teaching` repo using `--recursive` (needed for `ansible-conda`) submodule 25 | 26 | ```bash 27 | $ git clone --recursive https://github.com/jupyterhub/jupyterhub-deploy-teaching.git 28 | ``` 29 | 30 | ## 3. Secure before deployment 31 | 32 | - [ ] **cookie secret file:** Create `./security/cookie_secret` 33 | 34 | ```bash 35 | $ openssl rand -hex 1024 > ./security/cookie_secret 36 | ``` 37 | 38 | - [ ] **SSL:** 39 | * [Let's Encrypt](https://letsencrypt.org/): No additional steps as 40 | Ansible will install for you. 41 | * Third Party SSL trusted source: Install SSL private key 42 | `./security/ssl.key` and certificate as `./security/ssl.crt`. 43 | 44 | ## 4. Create JupyterHub hosts group 45 | 46 | - [ ] **`./hosts` file:** Edit file to lists the FQDN's of the hosts in the 47 | `jupyterhub_hosts` group. 48 | - [ ] **hostname files:** Use `./host_vars/hostname.example` as a 49 | template for creating and editing a hostname file for each host and 50 | place hostname files in `./host_vars` directory. 51 | 52 | ## 5. Configure admins 53 | 54 | - [ ] List of admins is configured in `jupyterhub_admin_users` in the config 55 | file. Public SSH keys will be retrieved from GitHub. 56 | 57 | ## 6. Configure users 58 | 59 | - [ ] If using [PAM (Pluggable authentication 60 | modules)](https://en.wikipedia.org/wiki/Linux_PAM), you will need to 61 | manually create users using adduser: `adduser --gecos "" username`. 62 | 63 | - [ ] If using [GitHub OAuth](https://developer.github.com/v3/oauth/), add 64 | usernames to `jupyterhub_users` list. 65 | 66 | ## 7. Add optional services 67 | 68 | - [ ] **Monitoring:** New Relic 69 | - [ ] **Analytics:** Google Analytics 70 | - [ ] **Assignment distribution and collection:** nbgrader 71 | - [ ] **Grading:** nbgrader 72 | 73 | ## 8. Deploy with Ansible 74 | 75 | - [ ] **Deploy:** Run `ansible-playbook` for the main deployment. 76 | 77 | ```bash 78 | $ ansible-playbook -i hosts deploy.yml 79 | ``` 80 | 81 | ## 9. Verify deployment and reload supervisor 82 | 83 | - [ ] **Verify:** SSH into the server: 84 | 85 | ```bash 86 | $ ssh root@{hostname} 87 | ``` 88 | substituting your hostname for {hostname}. For example, ``ssh root@jupyter.org``. 89 | 90 | ## 10. JupyterLab 91 | -------------------------------------------------------------------------------- /docs/source/use-nbgrader.md: -------------------------------------------------------------------------------- 1 | ## Using nbgrader 2 | 3 | With the reference deployment, instructors can start to use nbgrader. 4 | This section contains a rough sketch 5 | of what that looks like. For full details see the [nbgrader 6 | documentation](http://nbgrader.readthedocs.org/en/latest/). 7 | 8 | To use nbgrader, an instructor will primarily use the nbgrader command line 9 | program. Before doing this, the instructor will need to edit the 10 | `nbgrader_config.py` file with a list of students and assignments as follows: 11 | 12 | ```python 13 | c.NbGrader.db_assignments = [dict(name="ps1")] 14 | c.NbGrader.db_students = [ 15 | dict(id="bitdiddle", first_name="Ben", last_name="Bitdiddle"), 16 | dict(id="hacker", first_name="Alyssa", last_name="Hacker"), 17 | dict(id="reasoner", first_name="Louis", last_name="Reasoner") 18 | ] 19 | ``` 20 | 21 | You can also add an `email` field to each student and a `duedate` field to 22 | each assignment. Each time you create a new assignment add it to the config 23 | file. 24 | 25 | For each assignment, first create a directory for an assignment's source: 26 | 27 | cd ~/nbgrader/ 28 | mkdir source/ 29 | 30 | Next, copy notebooks into that directory: 31 | 32 | cp ~/Problem1.ipynb ~/nbgrader//source/ 33 | cp ~/Problem2.ipynb ~/nbgrader//source/ 34 | 35 | These notebooks should be prepared using the nbgrader "Create Assignment Celltoolbar". Now create 36 | the assignment: 37 | 38 | 39 | nbgrader assign 40 | 41 | That will create the student versions of the notebooks and put them into the 42 | `~/nbgrader//release/` directory with your solutions removed. 43 | 44 | Next, release the assignment to students: 45 | 46 | nbgrader release 47 | 48 | At this point, students can fetch the assignment by doing: 49 | 50 | nbgrader fetch --course 51 | 52 | That will give students a copy of the assignment directory with all of the notebooks. When students 53 | are done working the notebooks, they can submit the assignment by doing: 54 | 55 | nbgrader submit --course 56 | 57 | You can collect submitted assignments by doing: 58 | 59 | nbgrader collect 60 | 61 | This puts the students submitted work into the `~/nbgrader//submitted/` directory. To enter those notebooks into the nbgrader web grading system, run: 62 | 63 | nbgrader autograde 64 | 65 | By default, this will rerun all of the students notebooks. If you don't want to run them: 66 | 67 | nbgrader autograde --no-execute 68 | 69 | To see the full command line options for nbgrader, run: 70 | 71 | nbgrader --help 72 | 73 | Some other things you can do with nbgrader: 74 | 75 | * Run `collect` and `autograde` commands for a single student or notebook. 76 | * Collect a single assignment multiple times and regrade all or parts selectively. 77 | 78 | -------------------------------------------------------------------------------- /docs/source/use-nbgrader.rst: -------------------------------------------------------------------------------- 1 | Using nbgrader 2 | ============== 3 | 4 | With the reference deployment, instructors can start to use nbgrader. 5 | This section contains a rough sketch 6 | of what that looks like. For full details see the `nbgrader 7 | documentation `_. 8 | 9 | Preparing class assignments - Instructor 10 | ---------------------------------------- 11 | To use nbgrader, an instructor will primarily use the nbgrader command line 12 | program. 13 | 14 | Create a list of students and assignments 15 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 16 | Before doing this, the instructor will need to edit the 17 | :file:`nbgrader_config.py` file with a list of students and assignments as 18 | follows: 19 | 20 | .. code:: python 21 | 22 | c.NbGrader.db_assignments = [dict(name="ps1")] 23 | c.NbGrader.db_students = [ 24 | dict(id="bitdiddle", first_name="Ben", last_name="Bitdiddle"), 25 | dict(id="hacker", first_name="Alyssa", last_name="Hacker"), 26 | dict(id="reasoner", first_name="Louis", last_name="Reasoner") 27 | ] 28 | 29 | You can also add an ``email`` field to each student and a ``duedate`` field to 30 | each assignment. 31 | 32 | Remember to add new assignments to the :file:`nbgrader_config.py` file as the 33 | assignments are created. 34 | 35 | Create an assignment directory 36 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 37 | Create a directory for each assignment's source:: 38 | 39 | $ cd ~/nbgrader/ 40 | $ mkdir source/ 41 | 42 | Copy notebooks into assignment directory 43 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 44 | Copy notebooks into the assignment directory:: 45 | 46 | $ cp ~/Problem1.ipynb ~/nbgrader//source/ 47 | $ cp ~/Problem2.ipynb ~/nbgrader//source/ 48 | 49 | Create a student version of an assignment 50 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 51 | These notebooks should be prepared using the nbgrader 52 | "Create Assignment Cell toolbar". Now create the assignment:: 53 | 54 | $ nbgrader assign 55 | 56 | After creating the student versions of the notebooks, put them into the 57 | :file:`~/nbgrader//release/` directory, and remember 58 | to remove your solutions. 59 | 60 | Release the assignment 61 | ~~~~~~~~~~~~~~~~~~~~~~ 62 | Next, release the assignment to students:: 63 | 64 | $ nbgrader release 65 | 66 | 67 | Working with an assignment - Students 68 | ------------------------------------- 69 | 70 | Fetch the assignment 71 | ~~~~~~~~~~~~~~~~~~~~ 72 | At this point, students can fetch the assignment by doing:: 73 | 74 | $ nbgrader fetch --course 75 | 76 | That will give students a copy of the assignment directory with all of the 77 | notebooks. 78 | 79 | Submit an assignment solution 80 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 81 | When students are done working the notebooks, they can submit the assignment 82 | by doing:: 83 | 84 | $ nbgrader submit --course 85 | 86 | Grading the assignments - Instructor 87 | ------------------------------------ 88 | 89 | Collect student assignments 90 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 91 | 92 | You can collect submitted assignments by doing:: 93 | 94 | $ nbgrader collect 95 | 96 | This puts the students submitted work into the 97 | :file:`~/nbgrader//submitted/` directory. 98 | 99 | Grade the assignments 100 | ~~~~~~~~~~~~~~~~~~~~~ 101 | To enter those notebooks into the nbgrader web grading system, run:: 102 | 103 | $ nbgrader autograde 104 | 105 | By default, this will rerun all of the students notebooks. 106 | 107 | If you don't want to run them:: 108 | 109 | $ nbgrader autograde --no-execute 110 | 111 | Next steps 112 | ---------- 113 | To see the full command line options for nbgrader, run:: 114 | 115 | $ nbgrader --help 116 | 117 | Some other things you can do with nbgrader: 118 | 119 | * Run :command:`collect` and :command:`autograde` commands for a single 120 | student or notebook. 121 | * Collect a single assignment multiple times and regrade all or parts 122 | selectively. 123 | -------------------------------------------------------------------------------- /group_vars/jupyterhub_hosts: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | # These are global variables that are used across roles. 4 | # NOTE: You shouldn't have to change these, to customize this deployment 5 | # for individual hosts, create and edit files in the host_vars folder. 6 | 7 | # --------------------------------------------------- 8 | # Ansible 9 | # --------------------------------------------------- 10 | 11 | # Path of the Python 3 interpreter on the remote server 12 | ansible_python_interpreter: '/usr/bin/python3' 13 | 14 | # --------------------------------------------------- 15 | # JupyterHub config directories 16 | # --------------------------------------------------- 17 | 18 | jupyterhub_srv_dir: /srv/jupyterhub 19 | jupyterhub_config_dir: /etc/jupyterhub 20 | jupyterhub_log_dir: /var/log/jupyterhub 21 | 22 | # --------------------------------------------------- 23 | # Jupyter config directories 24 | # --------------------------------------------------- 25 | 26 | jupyter_config_dir: /etc/jupyter 27 | jupyter_share_dir: /usr/local/share/jupyter 28 | jupyter_templates_dir: "{{jupyter_config_dir}}/templates" 29 | 30 | # --------------------------------------------------- 31 | # IPython config directory 32 | # --------------------------------------------------- 33 | 34 | ipython_config_dir: /etc/ipython 35 | 36 | # --------------------------------------------------- 37 | # Nginx configuration of SSL 38 | # --------------------------------------------------- 39 | 40 | # For externally provided SSL cert 41 | ssl_path: "/etc/nginx/ssl" 42 | ssl_key_path: "{{ssl_path}}/ssl.key" 43 | ssl_cert_path: "{{ssl_path}}/ssl.crt" 44 | 45 | # If letsencrypt is used to get SSL cert 46 | letsencrypt_ssl_key_path: "/etc/letsencrypt/live/{{inventory_hostname}}/privkey.pem" 47 | letsencrypt_ssl_cert_path: "/etc/letsencrypt/live/{{inventory_hostname}}/fullchain.pem" 48 | 49 | # --------------------------------------------------- 50 | # Nbgrader config directories 51 | # --------------------------------------------------- 52 | 53 | nbgrader_log_dir: /var/log/nbgrader 54 | nbgrader_exchange_dir: /srv/nbgrader/exchange 55 | -------------------------------------------------------------------------------- /host_vars/hostname.example: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | # An example `hostname` file 4 | # 5 | # Edit this file to customize settings for a particular host. 6 | # Save as FQDN of the hostname without the `.example` suffix, 7 | # such as `jupyterhub.myuniversity.edu`. 8 | 9 | # ------------------------------------------------------------------------------ 10 | # Required settings 11 | # ------------------------------------------------------------------------------ 12 | 13 | # The base directory of user accounts. 14 | home_dir: /home 15 | 16 | # Users with administrative privileges. 17 | jupyterhub_admin_users: 18 | - instructor1 19 | - instructor2 20 | 21 | # Whitelist of jupyterhub users. 22 | jupyterhub_users: 23 | - instructor1 24 | - instructor2 25 | - grader1 26 | - grader2 27 | - student1 28 | - student2 29 | 30 | # A list of jupyterhub groups to create, each with a list of members. 31 | jupyterhub_groups: 32 | - { 33 | name: group1, 34 | members: ["instructor1", "grader1"] 35 | } 36 | - { 37 | name: group2, 38 | members: ["instructor2", "grader2"] 39 | } 40 | 41 | # Whether to redirect users to JupyterLab (/lab) by default 42 | jupyterlab_default: true 43 | 44 | # R kernel installation, set to true to enable. 45 | install_r_kernel: false 46 | 47 | # bash kernel installation, set to true to enable 48 | install_bash_kernel: true 49 | 50 | # Cleanup single user servers and the proxy on jupyterhub shutdown. Setting 51 | # this to false, will allow jupyterhub to be restarted while leaving the proxy 52 | # and single user servers running. 53 | cleanup_on_shutdown: true 54 | 55 | # ------------------------------------------------------------------------------ 56 | # Packages 57 | # ------------------------------------------------------------------------------ 58 | 59 | # Add or remove packages here for different package managers here (conda, pip, 60 | # cran). The package listed here are on-top of the base installation of jupyter, 61 | # ipython and the IPython and IR kernel. 62 | 63 | conda_packages: 64 | - numpy 65 | - scipy 66 | - matplotlib 67 | - cython 68 | - h5py 69 | - scikit-learn 70 | - scikit-image 71 | - pandas 72 | - sympy 73 | - pillow 74 | - seaborn 75 | - patsy 76 | - statsmodels 77 | - networkx 78 | - dask 79 | - blaze 80 | - odo 81 | - altair 82 | - mkl-service # needed for pymc3 83 | - pymc3 84 | - pytorch 85 | - torchvision 86 | 87 | pip_packages: 88 | - brewer2mpl 89 | - ipythonblocks 90 | - plotly 91 | - tensorflow 92 | - vdom 93 | 94 | jupyterlab_extensions: 95 | - "@jupyterlab/hub-extension" 96 | - "@jupyterlab/geojson-extension" 97 | - "@jupyterlab/plotly-extension" 98 | # - "@jupyter-widgets/jupyterlab-manager" 99 | 100 | cran_packages: 101 | - car 102 | - ggplot2 103 | - XML 104 | - plyr 105 | - randomForest 106 | - Hmisc 107 | - stringr 108 | - RColorBrewer 109 | - reshape 110 | - reshape2 111 | - RCurl 112 | - devtools 113 | - dplyr 114 | - httr 115 | - knitr 116 | - packrat 117 | - rmarkdown 118 | - rvtest 119 | - testit 120 | - testthat 121 | - tidyr 122 | - shiny 123 | - base64enc 124 | - Cairo 125 | - codetools 126 | - table 127 | - gridExtra 128 | - gtable 129 | - hexbin 130 | - jpeg 131 | - Lahman 132 | - MASS 133 | - PKI 134 | - png 135 | - microbenchmark 136 | - mgcv 137 | - mapproj 138 | - maps 139 | - maptools 140 | - mgcv 141 | - multcomp 142 | - nycflights13 143 | - quantreg 144 | - javareconf 145 | - rJava 146 | - roxygen2 147 | - RSQLite 148 | - lattice 149 | - nlme 150 | - RCurl 151 | - RHTMLForms 152 | - RJSONIO 153 | - RSelenium 154 | - rgl 155 | - caret 156 | - data.table 157 | - parallel 158 | - testthat 159 | - rpart 160 | - caret 161 | - RTextTools 162 | - XLSX 163 | 164 | # ------------------------------------------------------------------------------ 165 | # Optional settings 166 | # ------------------------------------------------------------------------------ 167 | 168 | # The following sections are for optional features that can be enabled. 169 | 170 | # ------------------------------------------------------------------------------ 171 | # OAuth 172 | # ------------------------------------------------------------------------------ 173 | 174 | # The default authentication will use PAM, which is the standard UNIX password 175 | # standard. In the default configuration UNIX usernames and passwords will be 176 | # used for JupyterHub. Enabling OAuth through an OAuth provider (such as GitHub) 177 | # will enable users to log in using their username and passwords from the OAuth 178 | # provider. In this case, new users can be added using the JupyterHub Admin 179 | # page, and the users' home directories will be automatically created. 180 | 181 | # Set to `true` to enable. 182 | use_oauth: false 183 | 184 | # The OAuth callback URL. 185 | oauth_callback_url: https://mydomain.org/hub/oauth_callback 186 | 187 | # The OAuth client ID. 188 | oauth_client_id: '' 189 | 190 | # The OAuth client secret. 191 | oauth_client_secret: '' 192 | 193 | # ------------------------------------------------------------------------------ 194 | # nbgrader 195 | # ------------------------------------------------------------------------------ 196 | 197 | # nbgrader is a system for assigning, collecting and grading notebook based 198 | # homework assignments. For more details see: 199 | # http://nbgrader.readthedocs.io/en/stable/ 200 | 201 | # Set to `true` to enable. 202 | use_nbgrader: false 203 | 204 | # ------------------------------------------------------------------------------ 205 | # cull_idle_servers 206 | # ------------------------------------------------------------------------------ 207 | 208 | # Set to `true` to enable. 209 | use_cull_idle_servers: false 210 | 211 | # The interval (in seconds) for checking for idle servers to cull. 212 | cull_every: 600 213 | 214 | # The idle timeout (in seconds). 215 | cull_timeout: 3600 216 | 217 | # ------------------------------------------------------------------------------ 218 | # Misc optional settings 219 | # ------------------------------------------------------------------------------ 220 | 221 | # List of the GitHub usernames who will receive root access via ssh. Public 222 | # GitHub SSH keys will be installed to allow the user to ssh into the server as 223 | # root. 224 | github_usernames: ['instructor1', 'grader1'] 225 | 226 | # To mount local file systems populate this list. (Optional) This adds the 227 | # entries to /etc/fstab, creates the mount points, and mounts them. Note: Disks 228 | # must be partitioned and formatted. 229 | local_mounts: [] 230 | # - name: /mountpoint1 231 | # src: /dev/sdb1 232 | # fstype: ext3 233 | # - name: /mountpoint2 234 | # src: /dev/sdc1 235 | # fstype: ext3 236 | 237 | # SSL using letsencrypt (optional - use letsencrypt default: false) If using 238 | # letsencrypt to generate SSL key/cert, set `use_letsencrypt` to `true` 239 | # Otherwise if not using letsencrypt for SSL, you MUST put your key and cert 240 | # into the security directory as `security/ssl.crt` and `security/ssl.key`. 241 | use_letsencrypt: false 242 | letsencrypt_email: '' 243 | 244 | # Should users have `/public_html/username` directories. Set this to `true` to 245 | # enable. 246 | nginx_public_html: false 247 | 248 | # Set Google Analytics Tracking ID (Optional). 249 | ga_tracking_id: '' 250 | 251 | # Set NewRelic license key (Optional). 252 | newrelic_license_key: '' 253 | 254 | -------------------------------------------------------------------------------- /hosts.example: -------------------------------------------------------------------------------- 1 | # The `hosts` inventory file lists the JupyterHub servers managed by Ansible 2 | 3 | # This provides an inventory of host servers used for JupyterHub 4 | # Edit the fqdn (fully qualified domain name) for your hub server 5 | # For example: 6 | # 7 | # [jupyterhub_hosts] 8 | # www.example.com 9 | # 10 | # Save file as `hosts` (without the file type suffix) when done editing. 11 | # User tip: `hosts.example` is an example file where the contents are 12 | # ignored. Saving the file as `hosts` after editing is important. 13 | 14 | [jupyterhub_hosts] 15 | fqdn.goes.here 16 | -------------------------------------------------------------------------------- /readthedocs.yml: -------------------------------------------------------------------------------- 1 | requirements_file: ./docs/requirements.txt 2 | python: 3 | version: 3 4 | -------------------------------------------------------------------------------- /roles/bash/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # This playbook installs a bash environment and kernel 3 | 4 | - name: pip install bash_kernel package 5 | pip: name={{item}} state=present editable=false executable=pip 6 | become: true 7 | with_items: 8 | - bash_kernel 9 | 10 | - name: install bash_kernel for jupyter 11 | command: python3 -m bash_kernel.install 12 | become: true 13 | -------------------------------------------------------------------------------- /roles/common/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # This playbook restarts the network time protocol and ssh daemons 3 | 4 | - name: restart ntp 5 | service: name=ntp state=restarted 6 | become: true 7 | 8 | - name: restart sshd 9 | service: name=ssh state=restarted 10 | become: true 11 | -------------------------------------------------------------------------------- /roles/common/tasks/hostname.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: set the hostname 4 | hostname: name="{{inventory_hostname}}" 5 | become: yes 6 | -------------------------------------------------------------------------------- /roles/common/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - include_tasks: hostname.yml 4 | - include_tasks: ntp.yml 5 | - include_tasks: packages.yml 6 | - include_tasks: mounts.yml 7 | - include_tasks: ssh.yml 8 | -------------------------------------------------------------------------------- /roles/common/tasks/mounts.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: mount local file systems 4 | mount: name={{item.name}} src={{item.src}} fstype={{item.fstype}} state=mounted 5 | with_items: "{{local_mounts}}" 6 | become: true 7 | -------------------------------------------------------------------------------- /roles/common/tasks/ntp.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # This playbook installs the network time protocol daemon 3 | 4 | - name: install ntp 5 | apt: pkg=ntp state=present 6 | become: true 7 | notify: 8 | - restart ntp 9 | -------------------------------------------------------------------------------- /roles/common/tasks/packages.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | # This playbook installs common packages used on a UNIX/Linux system 4 | 5 | - name: update apt 6 | apt: update_cache=yes cache_valid_time=3600 7 | become: yes 8 | 9 | # required to upgrade apt 10 | - name: install aptitude 11 | apt: pkg=aptitude 12 | become: yes 13 | 14 | # upgrade requires aptitude be installed 15 | - name: upgrade apt 16 | apt: upgrade=safe 17 | become: yes 18 | 19 | - name: install build-essential tools 20 | apt: pkg=build-essential state=present 21 | become: true 22 | 23 | - name: install other developer tools 24 | apt: pkg={{item}} state=present 25 | become: true 26 | with_items: 27 | - gfortran 28 | - git 29 | 30 | - name: install user applications 31 | apt: pkg={{item}} state=present 32 | become: true 33 | with_items: 34 | - vim 35 | - emacs 36 | - tree 37 | - htop 38 | 39 | - name: install open-iscsi for cloud block volumes 40 | apt: pkg=open-iscsi state=present 41 | become: true 42 | -------------------------------------------------------------------------------- /roles/common/tasks/ssh.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: create directory to hold public keys 4 | file: state=directory path=/tmp/pubkeys mode=0755 5 | 6 | - name: fetch public keys from github 7 | get_url: dest=/tmp/pubkeys/{{ item }}-pubkeys url=https://github.com/{{ item }}.keys force=yes 8 | with_items: '{{github_usernames}}' 9 | when: github_usernames is defined and github_usernames != [] 10 | 11 | - name: assemble the authorized keys file 12 | assemble: dest=/root/.ssh/authorized_keys mode=0600 src=/tmp/pubkeys 13 | become: true 14 | when: github_usernames is defined and github_usernames != [] 15 | -------------------------------------------------------------------------------- /roles/cull_idle/files/cull_idle_servers.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """script to monitor and cull idle single-user servers 3 | 4 | Caveats: 5 | 6 | last_activity is not updated with high frequency, 7 | so cull timeout should be greater than the sum of: 8 | 9 | - single-user websocket ping interval (default: 30s) 10 | - JupyterHub.last_activity_interval (default: 5 minutes) 11 | 12 | You can run this as a service managed by JupyterHub with this in your config:: 13 | 14 | 15 | c.JupyterHub.services = [ 16 | { 17 | 'name': 'cull-idle', 18 | 'admin': True, 19 | 'command': 'python cull_idle_servers.py --timeout=3600'.split(), 20 | } 21 | ] 22 | 23 | Or run it manually by generating an API token and storing it in `JUPYTERHUB_API_TOKEN`: 24 | 25 | export JUPYTERHUB_API_TOKEN=`jupyterhub token` 26 | python cull_idle_servers.py [--timeout=900] [--url=http://127.0.0.1:8081/hub/api] 27 | """ 28 | 29 | import datetime 30 | import json 31 | import os 32 | 33 | from dateutil.parser import parse as parse_date 34 | 35 | from tornado.gen import coroutine 36 | from tornado.log import app_log 37 | from tornado.httpclient import AsyncHTTPClient, HTTPRequest 38 | from tornado.ioloop import IOLoop, PeriodicCallback 39 | from tornado.options import define, options, parse_command_line 40 | 41 | 42 | @coroutine 43 | def cull_idle(url, api_token, timeout): 44 | """cull idle single-user servers""" 45 | auth_header = { 46 | 'Authorization': 'token %s' % api_token 47 | } 48 | req = HTTPRequest(url=url + '/users', 49 | headers=auth_header, 50 | ) 51 | now = datetime.datetime.utcnow() 52 | cull_limit = now - datetime.timedelta(seconds=timeout) 53 | client = AsyncHTTPClient() 54 | resp = yield client.fetch(req) 55 | users = json.loads(resp.body.decode('utf8', 'replace')) 56 | futures = [] 57 | for user in users: 58 | last_activity = parse_date(user['last_activity']) 59 | if user['server'] and last_activity < cull_limit: 60 | app_log.info("Culling %s (inactive since %s)", user['name'], last_activity) 61 | req = HTTPRequest(url=url + '/users/%s/server' % user['name'], 62 | method='DELETE', 63 | headers=auth_header, 64 | ) 65 | futures.append((user['name'], client.fetch(req))) 66 | elif user['server'] and last_activity > cull_limit: 67 | app_log.debug("Not culling %s (active since %s)", user['name'], last_activity) 68 | 69 | for (name, f) in futures: 70 | yield f 71 | app_log.debug("Finished culling %s", name) 72 | 73 | if __name__ == '__main__': 74 | define('url', default=os.environ.get('JUPYTERHUB_API_URL') or 'http://127.0.0.1:8081/hub/api', help="The JupyterHub API URL") 75 | define('timeout', default=600, help="The idle timeout (in seconds)") 76 | define('cull_every', default=0, help="The interval (in seconds) for checking for idle servers to cull") 77 | 78 | parse_command_line() 79 | if not options.cull_every: 80 | options.cull_every = options.timeout // 2 81 | 82 | api_token = os.environ['JUPYTERHUB_API_TOKEN'] 83 | 84 | loop = IOLoop.current() 85 | cull = lambda : cull_idle(options.url, api_token, options.timeout) 86 | # run once before scheduling periodic call 87 | loop.run_sync(cull) 88 | # schedule periodic cull 89 | pc = PeriodicCallback(cull, 1e3 * options.cull_every) 90 | pc.start() 91 | try: 92 | loop.start() 93 | except KeyboardInterrupt: 94 | pass 95 | -------------------------------------------------------------------------------- /roles/cull_idle/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: install cull_idle_servers dependencies 4 | pip: name=python-dateutil state=present executable=pip 5 | 6 | - name: install cull_idle_servers.py into {{jupyterhub_srv_dir}} 7 | copy: src=cull_idle_servers.py dest={{jupyterhub_srv_dir}} owner=root group=root mode=0700 8 | 9 | -------------------------------------------------------------------------------- /roles/jupyterhub/tasks/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: install jupyterhub config file 4 | template: src=jupyterhub_config.py.j2 dest={{jupyterhub_config_dir}}/jupyterhub_config.py owner=root group=root mode=0644 5 | become: true 6 | 7 | - name: install jupyterhub cookie secret 8 | copy: src="../../../security/cookie_secret" dest={{jupyterhub_srv_dir}}/cookie_secret owner=root group=root mode=0600 9 | become: true 10 | 11 | - name: install jupyter labhub launch script 12 | template: src=start-jupyter-labhub.sh.j2 dest=/usr/local/bin/start-jupyter-labhub.sh mode=0777 13 | become: true 14 | -------------------------------------------------------------------------------- /roles/jupyterhub/tasks/directories.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: make sure /etc/jupyterhub exists 4 | file: path={{jupyterhub_config_dir}}/ state={{item}} owner=root group=root mode=0755 5 | become: true 6 | with_items: 7 | - directory 8 | - touch 9 | 10 | - name: make sure /srv/jupyterhub exists 11 | file: path={{jupyterhub_srv_dir}}/ state={{item}} owner=root group=root mode=0700 12 | become: true 13 | with_items: 14 | - directory 15 | - touch 16 | 17 | - name: make sure /var/log/jupyterhub exists 18 | file: path={{jupyterhub_log_dir}}/ state={{item}} owner=root group=root mode=0755 19 | become: true 20 | with_items: 21 | - directory 22 | - touch 23 | 24 | - name: make sure /etc/jupyter/templates exists 25 | file: path={{jupyter_templates_dir}}/ state={{item}} owner=root group=root mode=0755 26 | become: true 27 | with_items: 28 | - directory 29 | - touch 30 | 31 | - name: make sure /etc/jupyter exists 32 | file: path={{jupyter_config_dir}} state={{item}} owner=root group=root mode=0755 33 | become: true 34 | with_items: 35 | - directory 36 | - touch 37 | 38 | - name: make sure /etc/ipython exists 39 | file: path={{ipython_config_dir}} state={{item}} owner=root group=root mode=0755 40 | become: true 41 | with_items: 42 | - directory 43 | - touch -------------------------------------------------------------------------------- /roles/jupyterhub/tasks/googleanalytics.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: install single user page.html file for Google Analytics 4 | template: src=page.html.j2 dest={{jupyter_templates_dir}}/page.html owner=root group=root mode=0644 5 | become: true 6 | when: ga_tracking_id is defined and ga_tracking_id != '' 7 | 8 | -------------------------------------------------------------------------------- /roles/jupyterhub/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - include_tasks: packages.yml 4 | - include_tasks: directories.yml 5 | - include_tasks: config.yml 6 | - include_tasks: googleanalytics.yml 7 | - include_tasks: supervisor.yml 8 | 9 | -------------------------------------------------------------------------------- /roles/jupyterhub/tasks/packages.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: install jupyterhub dependencies via conda 4 | conda: name={{item}} state=present 5 | become: true 6 | with_items: 7 | - configurable-http-proxy 8 | - sqlalchemy 9 | 10 | - name: install jupyterhub via pip 11 | pip: name={{item}} state=present editable=false executable=pip 12 | become: true 13 | with_items: 14 | - jupyterhub==0.8.1 15 | - oauthenticator==0.7.2 16 | -------------------------------------------------------------------------------- /roles/jupyterhub/tasks/supervisor.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: install supervisor config for jupyterhub 4 | template: src=jupyterhub.conf.j2 dest=/etc/supervisor/conf.d/jupyterhub.conf owner=root group=root mode=0600 backup=yes 5 | become: true 6 | 7 | - name: install jupyterhub launch script 8 | template: src=start-jupyterhub.sh.j2 dest={{jupyterhub_srv_dir}}/start-jupyterhub.sh mode=0700 9 | become: true 10 | -------------------------------------------------------------------------------- /roles/jupyterhub/templates/jupyterhub.conf.j2: -------------------------------------------------------------------------------- 1 | # {{ ansible_managed }} 2 | 3 | [program:jupyterhub] 4 | command={{jupyterhub_srv_dir}}/start-jupyterhub.sh --config={{ jupyterhub_config_dir }}/jupyterhub_config.py 5 | redirect_stderr=true 6 | stdout_logfile={{ jupyterhub_log_dir }}/jupyterhub.log 7 | autostart=true 8 | autorestart=false 9 | stopasgroup=true 10 | user=root 11 | directory={{jupyterhub_srv_dir}} 12 | -------------------------------------------------------------------------------- /roles/jupyterhub/templates/jupyterhub_config.py.j2: -------------------------------------------------------------------------------- 1 | # {{ ansible_managed }} 2 | # Configuration file for jupyterhub. 3 | 4 | c = get_config() 5 | c.JupyterHub.ip = u'{{ ansible_default_ipv4.address }}' 6 | c.JupyterHub.port = 8000 7 | c.JupyterHub.cookie_secret_file = u'{{ jupyterhub_srv_dir }}/cookie_secret' 8 | c.JupyterHub.db_url = u'{{ jupyterhub_srv_dir }}/jupyterhub.sqlite' 9 | c.JupyterHub.confirm_no_ssl = True 10 | {% if not cleanup_on_shutdown %} 11 | c.JupyterHub.cleanup_proxy = False 12 | c.JupyterHub.cleanup_servers = False 13 | {% endif %} 14 | 15 | # Always start using the labhub script to make JupyterLab work, 16 | # but don't redirect users to it unless jupyterlab_default is set. 17 | c.Spawner.cmd = u'/usr/local/bin/start-jupyter-labhub.sh' 18 | {% if jupyterlab_default %} 19 | c.Spawner.default_url = '/lab' 20 | {% else %} 21 | c.Spawner.default_url = '/tree' 22 | {% endif %} 23 | 24 | # A list of jupyterhub groups to create. 25 | c.JupyterHub.load_groups = { 26 | {%- for group in jupyterhub_groups -%} 27 | '{{group.name}}': [ 28 | {%- for member in group.members -%} 29 | '{{member}}', 30 | {%- endfor -%} 31 | ], 32 | {% endfor -%} 33 | } 34 | 35 | {% if use_oauth %} 36 | c.JupyterHub.authenticator_class = u'oauthenticator.LocalGitHubOAuthenticator' 37 | c.LocalGitHubOAuthenticator.create_system_users = True 38 | c.Authenticator.add_user_cmd = ['adduser', '-q', '--home', '{{home_dir}}/USERNAME', '--gecos', '""', '--disabled-password'] 39 | c.GitHubOAuthenticator.oauth_callback_url = u'{{ oauth_callback_url }}' 40 | c.GitHubOAuthenticator.client_id = u'{{ oauth_client_id }}' 41 | c.GitHubOAuthenticator.client_secret = u'{{ oauth_client_secret }}' 42 | {% endif %} 43 | 44 | {% if jupyterhub_admin_users|length %} 45 | c.Authenticator.admin_users = { 46 | {%- for user in jupyterhub_admin_users[:-1] -%} 47 | '{{user}}', 48 | {%- endfor -%} 49 | '{{-jupyterhub_admin_users[-1]-}}'} 50 | {% else %} 51 | c.Authenticator.admin_users = set() 52 | {% endif %} 53 | 54 | {% if jupyterhub_users|length %} 55 | c.Authenticator.whitelist = { 56 | {%- for user in jupyterhub_users[:-1] -%} 57 | '{{user}}', 58 | {%- endfor -%} 59 | '{{-jupyterhub_users[-1]-}}'} 60 | {% else %} 61 | c.Authenticator.whitelist = set() 62 | {% endif %} 63 | 64 | -------------------------------------------------------------------------------- /roles/jupyterhub/templates/page.html.j2: -------------------------------------------------------------------------------- 1 | {% raw %} 2 | {% extends "templates/page.html" %} 3 | {% block script %} 4 | 16 | {% endblock %} 17 | {% endraw %} -------------------------------------------------------------------------------- /roles/jupyterhub/templates/start-jupyter-labhub.sh.j2: -------------------------------------------------------------------------------- 1 | #!/bin/bash -l 2 | 3 | # Delegate the notebook server launch to the jupyter-labhub script. 4 | exec "jupyter-labhub" $@ -------------------------------------------------------------------------------- /roles/jupyterhub/templates/start-jupyterhub.sh.j2: -------------------------------------------------------------------------------- 1 | #!/bin/bash -l 2 | # {{ ansible_managed }} 3 | # set default lang env to avoid unicode issues 4 | export LANG=${LANG:-en_US.UTF-8} 5 | env | sort 6 | exec jupyterhub $@ 7 | -------------------------------------------------------------------------------- /roles/nbgrader/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: pip install nbgrader 4 | pip: name={{item}} state=present executable=pip 5 | become: true 6 | with_items: 7 | - nbgrader 8 | 9 | - name: make sure /var/log/nbgrader exists 10 | file: path={{nbgrader_log_dir}}/ state={{item}} owner=root group=root mode=0755 11 | become: true 12 | with_items: 13 | - directory 14 | - touch 15 | 16 | - name: make sure /srv/nbgrader/exchange exists 17 | file: path={{nbgrader_exchange_dir}}/ state={{item}} owner=root group=root mode=0777 18 | become: true 19 | with_items: 20 | - directory 21 | - touch 22 | 23 | - name: install the nbgrader nbextensions 24 | command: jupyter nbextension {{item}} --sys-prefix --py nbgrader 25 | become: true 26 | with_items: 27 | - install 28 | - enable 29 | 30 | - name: install the nbgrader serverextension 31 | command: jupyter serverextension enable --sys-prefix --py nbgrader 32 | become: true 33 | 34 | -------------------------------------------------------------------------------- /roles/newrelic/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # This playbook configures newrelic monitoring service 3 | 4 | - name: add newrelic repo key 5 | apt_key: 6 | url: https://download.newrelic.com/548C16BF.gpg 7 | id: 548C16BF 8 | state: present 9 | become: yes 10 | 11 | - name: add newrelic repo 12 | apt_repository: 13 | repo: 'deb http://apt.newrelic.com/debian/ newrelic non-free' 14 | state: present 15 | become: yes 16 | 17 | - name: install newrelic 18 | apt: pkg=newrelic-sysmond state=present update_cache=yes cache_valid_time=3600 19 | become: yes 20 | 21 | - name: set newrelic license key 22 | shell: nrsysmond-config --set license_key={{newrelic_license_key}} 23 | become: yes 24 | 25 | - name: stop newrelic daemon if already running 26 | shell: /etc/init.d/newrelic-sysmond stop 27 | become: yes 28 | ignore_errors: yes 29 | 30 | - name: start newrelic daemon 31 | shell: /etc/init.d/newrelic-sysmond start 32 | become: yes 33 | -------------------------------------------------------------------------------- /roles/nginx/files/letsencrypt-renew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | date >> /var/log/letsencrypt.log 4 | WEBROOT=/etc/letsencrypt/webroot 5 | test -d "$WEBROOT" || mkdir -p "$WEBROOT" 6 | certbot-auto renew --webroot --webroot-path="$WEBROOT" --no-self-upgrade 2>&1 &>> /var/log/letsencrypt.log 7 | service nginx reload 8 | -------------------------------------------------------------------------------- /roles/nginx/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: restart nginx 4 | service: name=nginx state=restarted 5 | 6 | - name: reload nginx 7 | service: name=nginx state=reloaded 8 | -------------------------------------------------------------------------------- /roles/nginx/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: install system nginx 4 | apt: pkg=nginx-full 5 | become: true 6 | 7 | # --------------------------------------------------- 8 | # Install SSL cert/key using letsencrypt 9 | # --------------------------------------------------- 10 | 11 | - fail: msg="you must provide an email for usage with letsencrypt" 12 | when: use_letsencrypt and (letsencrypt_email == '' or letsencrypt_email is undefined) 13 | 14 | - name: stop the nginx service 15 | service: name=nginx state=stopped enabled=yes 16 | become: true 17 | when: use_letsencrypt 18 | 19 | - name: set apt-yes default (needed for certbot) 20 | lineinfile: 21 | dest: /etc/apt/apt.conf.d/99-apt-yes 22 | state: present 23 | line: 'APT::Get::Assume-Yes "true";' 24 | create: yes 25 | become: true 26 | when: use_letsencrypt 27 | 28 | - name: install certbot (letsencrypt) 29 | get_url: 30 | url: https://dl.eff.org/certbot-auto 31 | dest: /usr/local/bin/certbot-auto 32 | mode: 755 33 | when: use_letsencrypt 34 | 35 | - name: SSL credentials with certbot 36 | command: /usr/local/bin/certbot-auto certonly --agree-tos --standalone -m {{ letsencrypt_email }} -d {{ inventory_hostname }} creates={{ letsencrypt_ssl_cert_path }} 37 | become: true 38 | when: use_letsencrypt 39 | 40 | - name: Setup letsencrypt renewal with cron 41 | copy: src=letsencrypt-renew dest=/etc/cron.daily/letsencrypt-renew mode=0755 42 | become: true 43 | when: use_letsencrypt 44 | 45 | # --------------------------------------------------- 46 | # Or, install existing SSL cert/key 47 | # --------------------------------------------------- 48 | 49 | - name: make sure /etc/nginx/ssl exists 50 | file: path={{ssl_path}}/ state={{item}} owner=root group=root mode=0700 51 | become: true 52 | with_items: 53 | - directory 54 | - touch 55 | 56 | # TODO: check if the key/cert exists before trying to install 57 | 58 | - name: install SSL key 59 | copy: src='../../../security/ssl.key' dest={{ssl_key_path}} owner=root group=root mode=0600 60 | become: true 61 | when: not use_letsencrypt 62 | 63 | - name: install SSL certificate 64 | copy: src='../../../security/ssl.crt' dest={{ssl_cert_path}} owner=root group=root mode=0600 65 | become: true 66 | when: not use_letsencrypt 67 | 68 | # --------------------------------------------------- 69 | # Configure and launch nginx 70 | # --------------------------------------------------- 71 | 72 | - name: find notebook static directory 73 | command: python3 -c 'import notebook; import os; print(os.path.join(notebook.__path__[0], "static"));' 74 | register: notebook_static_directory 75 | 76 | - name: install nginx.conf 77 | template: src=nginx.conf.j2 dest=/etc/nginx/nginx.conf owner=root group=root mode=0644 backup=yes 78 | become: true 79 | notify: 80 | - reload nginx 81 | 82 | - name: start the nginx service 83 | service: name=nginx state=started enabled=yes 84 | become: true 85 | 86 | - name: reload nginx 87 | service: name=nginx state=reloaded 88 | become: true 89 | -------------------------------------------------------------------------------- /roles/nginx/templates/nginx.conf.j2: -------------------------------------------------------------------------------- 1 | # {{ ansible_managed }} 2 | 3 | user www-data; 4 | worker_processes {{ ansible_processor_count }}; 5 | pid /run/nginx.pid; 6 | 7 | events { 8 | worker_connections 1024; 9 | } 10 | 11 | http { 12 | include /etc/nginx/mime.types; 13 | default_type application/octet-stream; 14 | 15 | access_log /var/log/nginx/access.log; 16 | error_log /var/log/nginx/error.log info; 17 | 18 | server { 19 | listen 80; 20 | server_name {{ inventory_hostname }}; 21 | rewrite ^ https://$host$request_uri? permanent; 22 | } 23 | 24 | server { 25 | listen 443; 26 | 27 | client_max_body_size 50M; 28 | 29 | server_name {{ inventory_hostname }}; 30 | 31 | 32 | ssl on; 33 | {% if use_letsencrypt %} 34 | ssl_certificate {{ letsencrypt_ssl_cert_path }}; 35 | ssl_certificate_key {{ letsencrypt_ssl_key_path }}; 36 | 37 | # letsencrypt renewal authorization area 38 | location /.well-known/ { 39 | alias /etc/letsencrypt/webroot/.well-known/; 40 | } 41 | {% else %} 42 | ssl_certificate {{ ssl_cert_path }}; 43 | ssl_certificate_key {{ ssl_key_path }}; 44 | {% endif %} 45 | ssl_ciphers "AES128+EECDH:AES128+EDH"; 46 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2; 47 | ssl_prefer_server_ciphers on; 48 | ssl_session_cache shared:SSL:10m; 49 | add_header Strict-Transport-Security "max-age=63072000; includeSubDomains"; 50 | add_header X-Content-Type-Options nosniff; 51 | ssl_stapling on; 52 | ssl_stapling_verify on; 53 | resolver_timeout 5s; 54 | 55 | location ~ /user/([a-zA-Z0-9\-_%]*)/static/(.*) { 56 | alias '{{ notebook_static_directory.stdout }}/$2'; 57 | } 58 | 59 | {% if nginx_public_html %} 60 | location ~ ^/public_html/([a-zA-Z0-9\-_%]*)(/.*)?$ { 61 | alias {{ home_dir }}/$1/public_html$2; 62 | index index.html index.htm Index.html; 63 | autoindex on; 64 | } 65 | {% endif %} 66 | 67 | location / { 68 | proxy_pass http://{{ ansible_default_ipv4.address }}:8000; 69 | 70 | proxy_set_header X-Real-IP $remote_addr; 71 | proxy_set_header Host $host; 72 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 73 | proxy_set_header X-NginX-Proxy true; 74 | } 75 | 76 | location ~* /(user/[^/]*)/(api/kernels/[^/]+/channels|terminals/websocket)/? { 77 | proxy_pass http://{{ ansible_default_ipv4.address }}:8000; 78 | 79 | proxy_set_header X-Real-IP $remote_addr; 80 | proxy_set_header Host $host; 81 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 82 | 83 | proxy_set_header X-NginX-Proxy true; 84 | 85 | # WebSocket support 86 | proxy_http_version 1.1; 87 | proxy_set_header Upgrade $http_upgrade; 88 | proxy_set_header Connection "upgrade"; 89 | proxy_read_timeout 86400; 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /roles/python/tasks/conda.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | # derived from https://github.com/uchida/ansible-miniconda-role 4 | 5 | - name: miniconda installer is downloaded 6 | get_url: 7 | # disable validate-certs due to Python 2 SSL issues 8 | # we use checksum anyway, so it should be fine 9 | validate_certs: False 10 | url: "https://repo.continuum.io/miniconda/{{conda_installer}}" 11 | dest: "/tmp/{{conda_installer}}" 12 | checksum: "{{conda_checksum}}" 13 | mode: 0755 14 | 15 | - name: miniconda is installed 16 | shell: 17 | '"/tmp/{{ conda_installer }}" -b -p "{{ conda_prefix }}"' 18 | args: 19 | creates: "{{ conda_prefix }}" 20 | executable: /bin/bash 21 | 22 | # add conda to path in two places, so it's always registered 23 | - name: add conda to PATH 24 | # add it to the front of bashrc, so it's even on noninteractive paths 25 | lineinfile: 26 | dest: /etc/bash.bashrc 27 | state: present 28 | line: "export PATH={{conda_prefix}}/bin:$PATH" 29 | insertbefore: BOF 30 | 31 | - name: add conda to login PATH 32 | # add it to profile, so it's for all login shells 33 | copy: 34 | dest: /etc/profile.d/conda.sh 35 | content: "export PATH={{conda_prefix}}/bin:$PATH" 36 | 37 | - name: add conda config 38 | copy: 39 | content: "{{ conda_config | to_yaml }}" 40 | dest: "{{ conda_prefix }}/.condarc" 41 | 42 | -------------------------------------------------------------------------------- /roles/python/tasks/jupyter.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Uses conda to install ipython and jupyter dependencies and python3 kernel 3 | 4 | - name: conda install ipython and jupyter deps 5 | conda: name={{item}} state=present 6 | become: true 7 | with_items: 8 | - notebook 9 | - jupyter_console 10 | - ipyparallel 11 | - ipykernel 12 | - nbconvert 13 | - pandoc 14 | - ipywidgets 15 | 16 | - name: install python3 kernelspec 17 | command: python3 -m IPython kernel install 18 | become: true 19 | -------------------------------------------------------------------------------- /roles/python/tasks/jupyterlab.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Install JupyterLab and its extensions 3 | 4 | - name: conda install jupyterlab 5 | conda: name=jupyterlab state=present 6 | become: true 7 | 8 | - name: conda install npmjs for jupyterlab 9 | conda: name=nodejs state=present 10 | become: true 11 | 12 | - name: install jupyterlab extensions 13 | command: jupyter labextension install {{item}} 14 | become: true 15 | with_items: '{{jupyterlab_extensions}}' 16 | -------------------------------------------------------------------------------- /roles/python/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - include_tasks: conda.yml 4 | - include_tasks: jupyter.yml 5 | - include_tasks: jupyterlab.yml 6 | - include_tasks: python3.yml 7 | -------------------------------------------------------------------------------- /roles/python/tasks/python3.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: user conda packages 4 | conda: name={{item}} state=present 5 | become: true 6 | with_items: '{{conda_packages}}' 7 | 8 | - name: user pip packages 9 | pip: name={{item}} state=present executable=pip 10 | become: true 11 | with_items: '{{pip_packages}}' 12 | -------------------------------------------------------------------------------- /roles/python/vars/main.yml: -------------------------------------------------------------------------------- 1 | miniconda_version: 4.3.31 2 | conda_installer: Miniconda3-{{miniconda_version}}-Linux-x86_64.sh 3 | conda_prefix: /opt/conda 4 | conda_checksum: "md5:7fe70b214bee1143e3e3f0467b71453c" 5 | conda_config: 6 | channels: 7 | - conda-forge 8 | - pytorch 9 | - defaults 10 | show_channel_urls: yes 11 | 12 | -------------------------------------------------------------------------------- /roles/r/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: repository | add public key 4 | apt_key: id=E084DAB9 keyserver=keyserver.ubuntu.com state=present 5 | 6 | - name: repository | add cran-r 7 | apt_repository: repo="deb http://cran.rstudio.com/bin/linux/ubuntu {{ansible_distribution_release}}/" state=present update_cache=true 8 | 9 | - name: install r and devtools's dependencies 10 | apt: name={{ item }} state=present 11 | with_items: 12 | - r-recommended 13 | - libcurl4-gnutls-dev 14 | - libxml2-dev 15 | - libssl-dev 16 | 17 | - name: install IRkernel's dependencies 18 | command: R --quiet -e "if (! '{{item}}' %in% installed.packages()[,'Package']) install.packages('{{item}}', repos='http://cran.rstudio.com/')" 19 | with_items: 20 | - repr 21 | - IRdisplay 22 | - evaluate 23 | - crayon 24 | - pbdZMQ 25 | - devtools 26 | - uuid 27 | - digest 28 | - stringi 29 | 30 | - name: install IRkernel 31 | command: R --quiet -e "if (! '{{item}}' %in% installed.packages()[,'Package']) devtools::install_github('IRkernel/{{item}}')" 32 | with_items: 33 | - IRkernel 34 | 35 | - name: install irkernel kernelspec for jupyter 36 | command: R --quiet -e "IRkernel::installspec(user = FALSE)" 37 | 38 | - name: install additional R packages 39 | command: R --quiet -e "if (! '{{item}}' %in% installed.packages()[,'Package']) install.packages('{{item}}', repos='http://cran.rstudio.com/')" 40 | with_items: '{{cran_packages}}' 41 | -------------------------------------------------------------------------------- /roles/saveusers/files/create_users.py: -------------------------------------------------------------------------------- 1 | """Create saved users with the same home directories and uids.""" 2 | import pwd 3 | import os, sys 4 | from subprocess import check_call 5 | import json 6 | 7 | fname = './saved_users.txt' 8 | 9 | if not os.path.isfile(fname): 10 | print('No saved users found') 11 | sys.exit() 12 | 13 | with open(fname, 'r') as f: 14 | users = json.load(f) 15 | 16 | for username, uid in users: 17 | home_dir = os.path.abspath(os.path.join('.', username)) 18 | try: 19 | udata = pwd.getpwnam(username) 20 | except KeyError: 21 | cmd = ['adduser', '-q', '--uid', str(uid), 22 | '--no-create-home', '--home', home_dir, 23 | '--gecos', '""', '--disabled-password', username] 24 | if os.path.isdir(home_dir): 25 | cmd.append('--no-create-home') 26 | print('Creating user: {}'.format(' '.join(cmd))) 27 | try: 28 | check_call(cmd) 29 | except CalledProcessError: 30 | print('Error in creating user: {}'.format(' '.join(cmd))) 31 | else: 32 | if udata.pw_uid == uid and udata.pw_dir == home_dir: 33 | print('User already exists: {}'.format(username)) 34 | else: 35 | print("User exists, but uid or home dir don't match", uid, udata.pw_uid, home_dir, udata.pw_dir) 36 | -------------------------------------------------------------------------------- /roles/saveusers/files/save_users.py: -------------------------------------------------------------------------------- 1 | """Save usernames and uids.""" 2 | 3 | import pwd 4 | import os 5 | import json 6 | 7 | data = [] 8 | users = pwd.getpwall() 9 | for user in users: 10 | username = user.pw_name 11 | uid = user.pw_uid 12 | home_dir = os.path.abspath(os.path.join('.', username)) 13 | if os.path.isdir(home_dir) and home_dir == user.pw_dir: 14 | data.append((username, uid)) 15 | print("Saving user: {}:{}".format(username, uid)) 16 | 17 | with open('./saved_users.txt', 'w') as f: 18 | json.dump(data, f) 19 | -------------------------------------------------------------------------------- /roles/saveusers/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: install save_users.py into {{home_dir}} 4 | copy: src=save_users.py dest={{home_dir}} owner=root group=root mode=0600 5 | 6 | - name: install create_users.py into {{home_dir}} 7 | copy: src=create_users.py dest={{home_dir}} owner=root group=root mode=0600 8 | 9 | - name: run create_users.py 10 | command: python3 create_users.py chdir={{home_dir}} 11 | become: true 12 | when: use_oauth 13 | -------------------------------------------------------------------------------- /roles/start_jupyterhub/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: load jupyterhub supervisor config 4 | supervisorctl: name=jupyterhub state=present 5 | 6 | - name: restart jupyterhub with supervisor 7 | supervisorctl: name=jupyterhub state=restarted 8 | -------------------------------------------------------------------------------- /roles/supervisor/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: restart supervisord 4 | service: name=supervisor state=restarted 5 | become: yes 6 | 7 | - name: reread supervisord 8 | command: supervisorctl reread 9 | become: yes 10 | 11 | - name: update supervisord 12 | command: supervisorctl update 13 | become: yes 14 | -------------------------------------------------------------------------------- /roles/supervisor/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: install apt supervisor package 4 | apt: pkg=supervisor state=present 5 | become: true 6 | 7 | - name: make sure supervisord is running 8 | service: name=supervisor enabled=yes state=started 9 | become: true 10 | 11 | - name: run supervisord as root 12 | lineinfile: dest=/etc/supervisor/supervisord.conf state=present line="user=root" insertafter="^\[supervisord\]" backup=yes 13 | become: true 14 | -------------------------------------------------------------------------------- /saveusers.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # The playbook saves old user accounts on the host 3 | 4 | - hosts: jupyterhub_hosts 5 | tasks: 6 | - name: save the old user accounts 7 | command: python3 save_users.py chdir={{home_dir}} 8 | become: true 9 | when: use_oauth 10 | -------------------------------------------------------------------------------- /security/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterhub/jupyterhub-deploy-teaching/609b0eb2e7f1a2c5d025a40e64ef281a304ae249/security/.gitignore --------------------------------------------------------------------------------