From 4331bc57122b4eddc041be7b699b2ef502add9fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Behmo?= Date: Tue, 22 Jan 2019 21:25:04 +0100 Subject: [PATCH] Tutor v3 complete rewrite Replace all make commands by a single "tutor" binary. Environment and data are all moved to ~/.tutor/local/share/tutor. We take the opportunity to add a web UI and revamp the documentation. This is a complete rewrite. Close #121. Close #147. --- .github/ISSUE_TEMPLATE.md | 4 +- .gitignore | 6 +- .travis.yml | 79 ++++- CHANGELOG.md | 9 + Makefile | 69 +--- README.rst | 18 +- android/.gitignore | 2 - android/Makefile | 32 -- bin/main | 4 + build/.gitignore | 1 - build/Makefile | 59 ---- build/configurator/Dockerfile | 12 - build/configurator/bin/configurator | 242 -------------- build/openedx/requirements/.gitignore | 1 - build/openedx/requirements/README | 3 - build/openedx/themes/.gitignore | 1 - data/.gitignore | 13 - deploy/.gitignore | 1 - deploy/Makefile | 5 - deploy/k8s/.gitignore | 2 - deploy/k8s/Makefile | 128 ------- deploy/k8s/deployments/cms.yml | 50 --- deploy/k8s/deployments/forum.yml | 20 -- deploy/k8s/deployments/lms.yml | 47 --- deploy/k8s/deployments/memcached.yml | 19 -- deploy/k8s/deployments/mongodb.yml | 26 -- deploy/k8s/deployments/mysql.yml | 32 -- deploy/k8s/deployments/nginx.yml | 54 --- deploy/k8s/deployments/rabbitmq.yml | 26 -- deploy/k8s/services/cms.yml | 11 - deploy/k8s/services/forum.yml | 11 - deploy/k8s/services/lms.yml | 11 - deploy/k8s/services/memcached.yml | 11 - deploy/k8s/services/mongodb.yml | 11 - deploy/k8s/services/mysql.yml | 11 - deploy/k8s/services/nginx.yml | 17 - deploy/k8s/services/rabbitmq.yml | 11 - deploy/k8s/volumes/cms-data.yml | 10 - deploy/k8s/volumes/lms-data-pvc.yml | 10 - deploy/k8s/volumes/mysql-pvc.yml | 10 - deploy/k8s/volumes/openedx-staticfiles.yml | 10 - deploy/k8s/volumes/rabbitmq.yml | 10 - deploy/local/.gitignore | 3 - deploy/local/Makefile | 194 ----------- deploy/local/templates/Makefile.env | 3 - deploy/templates/letsencrypt/certonly.sh | 6 - deploy/templates/openedx/scripts/oauth2.sh | 11 - deploy/templates/openedx/scripts/provision.sh | 20 -- deploy/templates/openedx/scripts/stats | 5 - docs/Makefile | 2 +- docs/customise.rst | 72 ++-- docs/dev.rst | 58 ++-- docs/faq.rst | 52 +++ docs/img/webui.png | Bin 0 -> 204421 bytes docs/index.rst | 31 +- docs/install.rst | 41 +++ docs/k8s.rst | 19 +- docs/local.rst | 85 +++-- docs/missing.rst | 12 - docs/options.rst | 19 +- docs/quickstart.rst | 12 +- docs/requirements.rst | 18 - docs/troubleshooting.rst | 30 +- docs/tutor.rst | 33 +- docs/webui.rst | 24 ++ requirements/base.in | 6 + requirements/base.txt | 36 ++ requirements/dev.in | 3 + requirements/dev.txt | 42 +++ setup.py | 49 +++ tutor.spec | 32 ++ .../settings/cms => tutor}/__init__.py | 0 tutor/android.py | 69 ++++ tutor/cli.py | 51 +++ tutor/config.py | 178 ++++++++++ tutor/dev.py | 101 ++++++ tutor/env.py | 107 ++++++ tutor/exceptions.py | 2 + tutor/fmt.py | 26 ++ tutor/images.py | 93 ++++++ tutor/k8s.py | 186 +++++++++++ tutor/local.py | 261 +++++++++++++++ tutor/ops.py | 51 +++ tutor/opts.py | 43 +++ tutor/scripts.py | 41 +++ .../templates/android}/edx.properties | 0 .../templates/android}/gradle.properties | 0 .../templates/android}/tutor.yaml | 0 .../templates/apps}/mysql/auth.env | 0 .../templates/apps}/nginx/cms.conf | 0 .../templates/apps}/nginx/extra.conf | 0 .../templates/apps}/nginx/lms.conf | 0 .../templates/apps}/nginx/tutor.conf | 0 .../templates/apps/notes/settings}/tutor.py | 0 .../apps/openedx/config/cms.auth.json | 0 .../apps}/openedx/config/cms.env.json | 0 .../apps/openedx/config/lms.auth.json | 2 +- .../apps}/openedx/config/lms.env.json | 0 .../apps/openedx/settings/cms}/__init__.py | 0 .../apps}/openedx/settings/cms/development.py | 0 .../apps}/openedx/settings/cms/production.py | 0 .../apps/openedx/settings/lms}/__init__.py | 0 .../apps}/openedx/settings/lms/development.py | 0 .../apps}/openedx/settings/lms/production.py | 0 .../templates/apps/xqueue/settings}/tutor.py | 0 .../templates/build}/android/Dockerfile | 0 .../templates/build}/android/edx.properties | 0 .../templates/build}/forum/Dockerfile | 0 .../templates/build}/notes/Dockerfile | 0 .../templates/build}/openedx/Dockerfile | 6 +- .../build}/openedx/bin/docker-entrypoint.sh | 0 .../build}/openedx/bin/openedx-assets | 0 .../build/openedx/requirements/private.txt | 6 + .../build/openedx/settings/cms}/__init__.py | 0 .../build}/openedx/settings/cms/assets.py | 0 .../build/openedx/settings/lms/__init__.py | 0 .../build}/openedx/settings/lms/assets.py | 0 tutor/templates/build/openedx/themes/README | 1 + .../templates/build}/xqueue/Dockerfile | 0 tutor/templates/config-defaults.yml | 12 + tutor/templates/config.yml | 19 ++ .../templates/k8s/adminuser.yml | 1 + tutor/templates/k8s/deployments.yml | 315 ++++++++++++++++++ .../templates/k8s}/ingress.yml | 1 + {deploy => tutor/templates}/k8s/namespace.yml | 1 + tutor/templates/k8s/services.yml | 123 +++++++ tutor/templates/k8s/volumes.yml | 71 ++++ .../templates/local}/docker-compose.yml | 48 +-- tutor/ui.py | 13 + tutor/utils.py | 53 +++ tutor/webui.py | 134 ++++++++ 131 files changed, 2585 insertions(+), 1457 deletions(-) delete mode 100644 android/.gitignore delete mode 100644 android/Makefile create mode 100755 bin/main delete mode 100644 build/.gitignore delete mode 100644 build/Makefile delete mode 100644 build/configurator/Dockerfile delete mode 100755 build/configurator/bin/configurator delete mode 100644 build/openedx/requirements/.gitignore delete mode 100644 build/openedx/requirements/README delete mode 100644 build/openedx/themes/.gitignore delete mode 100644 data/.gitignore delete mode 100644 deploy/.gitignore delete mode 100644 deploy/Makefile delete mode 100644 deploy/k8s/.gitignore delete mode 100644 deploy/k8s/Makefile delete mode 100644 deploy/k8s/deployments/cms.yml delete mode 100644 deploy/k8s/deployments/forum.yml delete mode 100644 deploy/k8s/deployments/lms.yml delete mode 100644 deploy/k8s/deployments/memcached.yml delete mode 100644 deploy/k8s/deployments/mongodb.yml delete mode 100644 deploy/k8s/deployments/mysql.yml delete mode 100644 deploy/k8s/deployments/nginx.yml delete mode 100644 deploy/k8s/deployments/rabbitmq.yml delete mode 100644 deploy/k8s/services/cms.yml delete mode 100644 deploy/k8s/services/forum.yml delete mode 100644 deploy/k8s/services/lms.yml delete mode 100644 deploy/k8s/services/memcached.yml delete mode 100644 deploy/k8s/services/mongodb.yml delete mode 100644 deploy/k8s/services/mysql.yml delete mode 100644 deploy/k8s/services/nginx.yml delete mode 100644 deploy/k8s/services/rabbitmq.yml delete mode 100644 deploy/k8s/volumes/cms-data.yml delete mode 100644 deploy/k8s/volumes/lms-data-pvc.yml delete mode 100644 deploy/k8s/volumes/mysql-pvc.yml delete mode 100644 deploy/k8s/volumes/openedx-staticfiles.yml delete mode 100644 deploy/k8s/volumes/rabbitmq.yml delete mode 100644 deploy/local/.gitignore delete mode 100644 deploy/local/Makefile delete mode 100644 deploy/local/templates/Makefile.env delete mode 100755 deploy/templates/letsencrypt/certonly.sh delete mode 100755 deploy/templates/openedx/scripts/oauth2.sh delete mode 100755 deploy/templates/openedx/scripts/provision.sh delete mode 100755 deploy/templates/openedx/scripts/stats create mode 100644 docs/faq.rst create mode 100644 docs/img/webui.png create mode 100644 docs/install.rst delete mode 100644 docs/missing.rst delete mode 100644 docs/requirements.rst create mode 100644 docs/webui.rst create mode 100644 requirements/base.in create mode 100644 requirements/base.txt create mode 100644 requirements/dev.in create mode 100644 requirements/dev.txt create mode 100644 setup.py create mode 100644 tutor.spec rename {build/openedx/settings/cms => tutor}/__init__.py (100%) create mode 100644 tutor/android.py create mode 100755 tutor/cli.py create mode 100644 tutor/config.py create mode 100644 tutor/dev.py create mode 100644 tutor/env.py create mode 100644 tutor/exceptions.py create mode 100644 tutor/fmt.py create mode 100644 tutor/images.py create mode 100644 tutor/k8s.py create mode 100644 tutor/local.py create mode 100644 tutor/ops.py create mode 100644 tutor/opts.py create mode 100644 tutor/scripts.py rename {android/templates => tutor/templates/android}/edx.properties (100%) rename {android/templates => tutor/templates/android}/gradle.properties (100%) rename {android/templates => tutor/templates/android}/tutor.yaml (100%) rename {deploy/templates => tutor/templates/apps}/mysql/auth.env (100%) rename {deploy/templates => tutor/templates/apps}/nginx/cms.conf (100%) rename {deploy/templates => tutor/templates/apps}/nginx/extra.conf (100%) rename {deploy/templates => tutor/templates/apps}/nginx/lms.conf (100%) rename {deploy/templates => tutor/templates/apps}/nginx/tutor.conf (100%) rename {deploy/templates/notes => tutor/templates/apps/notes/settings}/tutor.py (100%) rename deploy/templates/openedx/config/lms.auth.json => tutor/templates/apps/openedx/config/cms.auth.json (100%) rename {deploy/templates => tutor/templates/apps}/openedx/config/cms.env.json (100%) rename deploy/templates/openedx/config/cms.auth.json => tutor/templates/apps/openedx/config/lms.auth.json (96%) rename {deploy/templates => tutor/templates/apps}/openedx/config/lms.env.json (100%) rename {build/openedx/settings/lms => tutor/templates/apps/openedx/settings/cms}/__init__.py (100%) rename {deploy/templates => tutor/templates/apps}/openedx/settings/cms/development.py (100%) rename {deploy/templates => tutor/templates/apps}/openedx/settings/cms/production.py (100%) rename {deploy/templates/openedx/settings/cms => tutor/templates/apps/openedx/settings/lms}/__init__.py (100%) rename {deploy/templates => tutor/templates/apps}/openedx/settings/lms/development.py (100%) rename {deploy/templates => tutor/templates/apps}/openedx/settings/lms/production.py (100%) rename {deploy/templates/xqueue => tutor/templates/apps/xqueue/settings}/tutor.py (100%) rename {build => tutor/templates/build}/android/Dockerfile (100%) rename {build => tutor/templates/build}/android/edx.properties (100%) rename {build => tutor/templates/build}/forum/Dockerfile (100%) rename {build => tutor/templates/build}/notes/Dockerfile (100%) rename {build => tutor/templates/build}/openedx/Dockerfile (93%) rename {build => tutor/templates/build}/openedx/bin/docker-entrypoint.sh (100%) rename {build => tutor/templates/build}/openedx/bin/openedx-assets (100%) create mode 100644 tutor/templates/build/openedx/requirements/private.txt rename {deploy/templates/openedx/settings/lms => tutor/templates/build/openedx/settings/cms}/__init__.py (100%) rename {build => tutor/templates/build}/openedx/settings/cms/assets.py (100%) create mode 100644 tutor/templates/build/openedx/settings/lms/__init__.py rename {build => tutor/templates/build}/openedx/settings/lms/assets.py (100%) create mode 100644 tutor/templates/build/openedx/themes/README rename {build => tutor/templates/build}/xqueue/Dockerfile (100%) create mode 100644 tutor/templates/config-defaults.yml create mode 100644 tutor/templates/config.yml rename deploy/k8s/admin.yml => tutor/templates/k8s/adminuser.yml (98%) create mode 100644 tutor/templates/k8s/deployments.yml rename {deploy/k8s/templates => tutor/templates/k8s}/ingress.yml (99%) rename {deploy => tutor/templates}/k8s/namespace.yml (93%) create mode 100644 tutor/templates/k8s/services.yml create mode 100644 tutor/templates/k8s/volumes.yml rename {deploy/local/templates => tutor/templates/local}/docker-compose.yml (75%) create mode 100644 tutor/ui.py create mode 100644 tutor/utils.py create mode 100644 tutor/webui.py diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 146baad..a7dcbcc 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -6,8 +6,8 @@ Include the exact command that you are running and that is causing an error. In ## Unexpected behavior -Include the full, exact output from the command that is causing your issue. Also include relevant error logs; for instance, to debug the LMS take a look at the files in `data/lms/logs`. +Include the full, exact output from the command that is causing your issue. Also include relevant error logs; for instance, to debug the LMS provide the output of `tutor local logs lms --tail=100` ## Additional info (IMPORTANT) -Include the output of the `make info` command. +Provide the output of `tutor --version`. diff --git a/.gitignore b/.gitignore index aec72a3..b95d054 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ .*.swp !.gitignore -/config.json -/data*/ TODO + +/build/tutor +/dist/ +/tutor_openedx.egg-info/ diff --git a/.travis.yml b/.travis.yml index 01b9c16..742384f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,72 @@ -language: minimal -services: - - docker +language: python +matrix: + include: + - os: linux + # We need an older version of python in order to have compatibility with + # older versions of libc + dist: trusty + python: 3.6 + services: + - docker + env: + BUILD_BINARY: "true" + - os: linux + dist: xenial + python: 3.6 + services: + - docker + env: + BUILD_DOCKER: "true" + - os: linux + dist: xenial + python: 3.6 + services: + - docker + env: + BUILD_PYPI: "true" + - os: osx + language: generic + env: + BUILD_BINARY: "true" script: - - make travis + - python3 --version + - pip3 --version + - pip3 install -U setuptools + - pip3 install -r requirements/dev.txt + - make bundle + - ./dist/tutor config noninteractive + - ./dist/tutor images env + - ./dist/tutor local env + +before_deploy: + - cp ./dist/tutor ./dist/tutor-$TRAVIS_OS_NAME + deploy: - provider: script - script: cd build/ && docker login -u "$DOCKER_USERNAME" -p "$DOCKER_PASSWORD" && make push - on: - all_branches: true - condition: $TRAVIS_BRANCH =~ ^master|release\/.*$ + # Push tutor binary to github releases + - provider: releases + api_key: + secure: ffrhGvgxHf3WVnKsKiJZTUoEf3+ZBtIydPHQoTDmHiYMyPnaO6RJWxq5PnoDRr47b1vnvbCE0lhNsGr3sgnruQSN3xHYfT4wz1F6Lz7Y5Jt7Uq1W0/UfSRlB3BwYcFpkUb5fSXLSku+QrV8OTk/yItlY9tppnvXA9h4CZjQqgJYHYc1DRKWQVYnVs/dCttUpiPV3eyIFeQoPKFsHwZXKcm9cVzom5r+lkjwpw3AIuAutPd71jyj/Va/By6B/43yAIqw3sA/thBeL76vqd8C4cMeWE00of1EWnH+sx6pKz32Fqe/o6dVop5PZPCiI+TVIDxR8oLevHtPTCfIwRDg60y23DdH3bMGSw1bAyjeWTnhRGcdYQJi1NKq3hJ0ldy0lOFlUiMkKPdQBtU7i0xIpoXTIhnro0YUUtjbh/QEtyRn8nbMVeenId42Bymah5P8srhE8S2z0x0mirIqNlM/tgLKEOJXdSL4sPKCMwRLcTUIc0fCiwLeHJnHIrMNEfnWsqO1odzv/PS5nLsdiGHBmC63d+xLNllzincIV1djyi5SEzMZxcqmjX1n/G3nKYVXhaIQE0wWQSHqNwLlKluY0kPXCNtU1TPr5SJHGnomQKizVaEsDrdAjylBL+rPwVCAv9z8FCtyOg7uMx2WdD0pvky2Iy4rbl2RBLUHmUZoAjPY= + file: dist/tutor-$TRAVIS_OS_NAME + skip_cleanup: true + on: + tags: true + condition: $BUILD_BINARY = true + + # Push docker images to docker hub + - provider: script + script: ./dist/tutor images build all && \ + ./dist/tutor local databases && \ + docker login -u "$DOCKER_USERNAME" -p "$DOCKER_PASSWORD" && \ + ./dist/tutor images push all + skip_cleanup: true + on: + tags: true + condition: $BUILD_DOCKER = true + + # Push to pypi + - provider: script + script: pip install twine && python setup.py sdist && twine upload dist/*.tar.gz + skip_cleanup: true + on: + tags: true + condition: $BUILD_PYPI = true diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c2d890..75051bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 3.0.0 (2019-06-09) + +- [Improvement] Complete rewrite of Tutor: switch from a make-based project to a single binary which runs all commands. +- [Feature] An web user interface can be created with `tutor webui start` +- [Bugfix] Add missing elasticsearch to Kubernetes deployment (#147) +- [Improvement] Upload `tutor-openedx` to pypi + +## Older changes + - 2019-01-27 [Bugfix] Fix video transcript/srt upload and download of user-uploaded files. Thanks @dannielariola! - 2019-01-20 [Improvement] Make it easy to load custom settings for the local production install - 2019-01-16 [Improvement] Switch license from MIT to AGPL diff --git a/Makefile b/Makefile index 194c271..247b8c6 100644 --- a/Makefile +++ b/Makefile @@ -1,66 +1,17 @@ -.PHONY: android build .DEFAULT_GOAL := help -PWD = $$(pwd) -USERID ?= $$(id -u) +compile-requirements: ## Compile requirements files + pip-compile -o requirements/base.txt requirements/base.in + pip-compile -o requirements/dev.txt requirements/dev.in -build: ## Build all docker images - cd build/ && make build +bundle: ## Bundle the tutor package in a single "dist/tutor" executable + pyinstaller --onefile --name=tutor --add-data=./tutor/templates:./tutor/templates ./bin/main -config.json: ## Generate config.json configuration file interactively - @$(MAKE) -s upgrade-to-tutor - @$(MAKE) -s -C build/ build-configurator 1> /dev/null - @docker run --rm -it \ - --volume="$(PWD):/openedx/config/" \ - -e USERID=$(USERID) -e SILENT=$(SILENT) \ - regis/openedx-configurator:hawthorn \ - configurator interactive - -substitute: config.json - @docker run --rm -it \ - --volume="$(PWD)/config.json:/openedx/config/config.json" \ - --volume="$(TEMPLATES):/openedx/templates" \ - --volume="$(OUTPUT):/openedx/output" \ - -e USERID=$(USERID) -e SILENT=$(SILENT) $(CONFIGURE_OPTS) \ - regis/openedx-configurator:hawthorn \ - configurator substitute /openedx/templates/ /openedx/output/ - -local: ## Configure and run a ready-to-go Open edX platform - $(MAKE) -C deploy/local all - -stop: ## Stop all single server services - $(MAKE) -C deploy/local stop - -android: ## Configure and build a development Android app - cd android/ && make all - -travis: - cd build && make build - cd deploy/local \ - && make configure SILENT=1 CONFIGURE_OPTS="-e SETTING_ACTIVATE_NOTES=1 -e SETTING_ACTIVATE_XQUEUE=1" \ - && make databases - -upgrade-to-tutor: ## Upgrade from earlier versions of tutor - @(stat config/config.json > /dev/null 2>&1 && (\ - echo "You are running an older version of Tutor. Now migrating to the latest version" \ - && echo "Moving config/config.json to ./config.json" && mv config/config.json config.json \ - && echo "Moving config/ to deploy/env/" && mv config/ deploy/env/ \ - && ((ls openedx/themes/* > /dev/null 2>&1 && echo "Moving openedx/themes/* to build/openedx/themes/" && mv openedx/themes/* build/openedx/themes/) || true) \ - && (mv .env deploy/local/ > /dev/null 2>&1 || true)\ - && echo "Done migrating to tutor. This command will not be run again."\ - )) || true - -info: ## Print some information about the current install, for debugging - uname -a - @echo "-------------------------" - git rev-parse HEAD - @echo "-------------------------" - docker version - @echo "-------------------------" - docker-compose --version - @echo "-------------------------" - echo $$EDX_PLATFORM_PATH - echo $$EDX_PLATFORM_SETTINGS +travis: bundle ## Run tests on travis-ci + ./dist/tutor config noninteractive + ./dist/tutor images env + ./dist/tutor images build all + ./dist/tutor local databases ESCAPE =  help: ## Print this help diff --git a/README.rst b/README.rst index 5178a74..1836ae3 100644 --- a/README.rst +++ b/README.rst @@ -13,6 +13,10 @@ Tutor 🎓 Open edX 1-click install for everyone :alt: GitHub closed issues :target: https://github.com/regisb/tutor/issues?q=is%3Aclosed +.. image:: https://img.shields.io/github/license/regisb/tutor.svg + :alt: AGPL License + :target: https://www.gnu.org/licenses/agpl-3.0.en.html + **Tutor** is a one-click install of `Open edX `_, both for production and local development, inside docker containers. Tutor is easy to run, fast, full of cool features, and it is already used by dozens of Open edX platforms in the world. .. image:: https://asciinema.org/a/octNfEnvIA6jNohCBmODBKizE.png @@ -22,18 +26,16 @@ Tutor 🎓 Open edX 1-click install for everyone Quickstart ---------- -:: - - git clone https://github.com/regisb/tutor - cd tutor/deploy/local - make all +1. `Download `_ the latest stable release of Tutor, uncompress the file and place the ``tutor`` executable in your path. +2. Run ``tutor local quickstart`` +3. You're done! Documentation ------------- Extensive documentation is available online: http://docs.tutor.overhang.io/ -How to contribute ------------------ +Contributing +------------ -We go to great lengths to make it as easy as possible for people to run Open edX inside Docker containers. If you have an improvement idea, feel free to `open an issue on Github `_ so that we can discuss it. `Pull requests `_ will be happily examined, too! However, we should be careful to keep the project lean and simple: both to use and to modify. Optional features should not make the user experience more complex. Instead, documentation on how to add the feature is preferred. +We go to great lengths to make it as easy as possible for people to run Open edX inside Docker containers. If you have an improvement idea, feel free to `open an issue on Github `_ so that we can discuss it. `Pull requests `_ will be happily examined, too! diff --git a/android/.gitignore b/android/.gitignore deleted file mode 100644 index 9374b02..0000000 --- a/android/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/config/ -/data/ diff --git a/android/Makefile b/android/Makefile deleted file mode 100644 index bc6f82a..0000000 --- a/android/Makefile +++ /dev/null @@ -1,32 +0,0 @@ -PWD ?= $$(pwd) -.DEFAULT_GOAL := help - -all: environment update android ## Configure and build a development Android app - -env: ## Generate the environment - @$(MAKE) -s -C .. substitute TEMPLATES=$(PWD)/templates OUTPUT=$(PWD)/config - -update: ## Download most recent Android image - docker pull regis/openedx-android:latest - -android: ## Build the Android app, for development - docker run --rm -it \ - --volume=$(PWD)/config/:/openedx/config/ \ - --volume=$(PWD)/data/:/openedx/data \ - regis/openedx-android:latest - @echo "Your development APK file is ready in $(PWD)/data/" - -android-release: ## Build the final Android app (beta) - # Note that this requires that you edit ./config/android/gradle.properties - docker run --rm -it \ - --volume=$(PWD)/config/:/openedx/config/ \ - --volume=$(PWD)/data/:/openedx/data \ - regis/openedx-android:latest \ - ./gradlew assembleProdRelease - @echo "Your production APK file is ready in $(PWD)/data/" - -ESCAPE =  -help: ## Print this help - @grep -E '^([a-zA-Z_-]+:.*?## .*|######* .+)$$' Makefile \ - | sed 's/######* \(.*\)/\n $(ESCAPE)[1;31m\1$(ESCAPE)[0m/g' \ - | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[33m%-30s\033[0m %s\n", $$1, $$2}' diff --git a/bin/main b/bin/main new file mode 100755 index 0000000..f1eaaad --- /dev/null +++ b/bin/main @@ -0,0 +1,4 @@ +#!/usr/bin/env python3 + +from tutor.cli import main +main() diff --git a/build/.gitignore b/build/.gitignore deleted file mode 100644 index 70fdd94..0000000 --- a/build/.gitignore +++ /dev/null @@ -1 +0,0 @@ -openedx/requirements/private.txt diff --git a/build/Makefile b/build/Makefile deleted file mode 100644 index db0c865..0000000 --- a/build/Makefile +++ /dev/null @@ -1,59 +0,0 @@ -#################### Docker image building -.DEFAULT_GOAL := help - -build: build-openedx build-configurator build-forum build-notes build-xqueue build-android ## Build all docker images - -openedx_build_args = -ifdef EDX_PLATFORM_REPOSITORY - openedx_build_args += --build-arg="EDX_PLATFORM_REPOSITORY=$(EDX_PLATFORM_REPOSITORY)" -endif -ifdef EDX_PLATFORM_VERSION - openedx_build_args += --build-arg="EDX_PLATFORM_VERSION=$(EDX_PLATFORM_VERSION)" -endif -ifdef THEMES - openedx_build_args += --build-arg="THEMES=$(THEMES)" -endif - -build-openedx: ## Build the Open edX docker image - docker build -t regis/openedx:latest -t regis/openedx:hawthorn $(openedx_build_args) openedx/ -build-configurator: ## Build the configurator docker image - docker build -t regis/openedx-configurator:latest -t regis/openedx-configurator:hawthorn configurator/ -build-forum: ## Build the forum docker image - docker build -t regis/openedx-forum:latest -t regis/openedx-forum:hawthorn forum/ -build-notes: ## Build the Notes docker image - docker build -t regis/openedx-notes:latest -t regis/openedx-notes:hawthorn notes/ -build-xqueue: ## Build the Xqueue docker image - docker build -t regis/openedx-xqueue:latest -t regis/openedx-xqueue:hawthorn xqueue/ -build-android: ## Build the docker image for Android - docker build -t regis/openedx-android:latest android/ - -################### Pushing images to docker hub - -push: push-openedx push-configurator push-forum push-notes push-xqueue push-android ## Push all images to dockerhub -push-openedx: ## Push Open edX images to dockerhub - docker push regis/openedx:hawthorn - docker push regis/openedx:latest -push-configurator: ## Push configurator image to dockerhub - docker push regis/openedx-configurator:hawthorn - docker push regis/openedx-configurator:latest -push-forum: ## Push forum image to dockerhub - docker push regis/openedx-forum:hawthorn - docker push regis/openedx-forum:latest -push-notes: ## Push notes image to dockerhub - docker push regis/openedx-notes:hawthorn - docker push regis/openedx-notes:latest -push-xqueue: ## Push Xqueue image to dockerhub - docker push regis/openedx-xqueue:hawthorn - docker push regis/openedx-xqueue:latest -push-android: ## Push the Android image to dockerhub - docker push regis/openedx-android:latest - -dockerhub: build push ## Build and push all images to dockerhub - -##################### Information - -ESCAPE =  -help: ## Print this help - @grep -E '^([a-zA-Z_-]+:.*?## .*|######* .+)$$' Makefile \ - | sed 's/######* \(.*\)/\n $(ESCAPE)[1;31m\1$(ESCAPE)[0m/g' \ - | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[33m%-30s\033[0m %s\n", $$1, $$2}' diff --git a/build/configurator/Dockerfile b/build/configurator/Dockerfile deleted file mode 100644 index 0634a6e..0000000 --- a/build/configurator/Dockerfile +++ /dev/null @@ -1,12 +0,0 @@ -FROM python:3.6-alpine -MAINTAINER Régis Behmo - -RUN apk add --no-cache curl -RUN pip install jinja2 - -RUN mkdir /openedx /openedx/config /openedx/templates -COPY ./bin/configurator /usr/local/bin -WORKDIR /openedx/ - -CMD configurator -c /openedx/config/config.json interactive && \ - configurator -c /openedx/config/config.json substitute /openedx/templates/ /openedx/output/ diff --git a/build/configurator/bin/configurator b/build/configurator/bin/configurator deleted file mode 100755 index 5f210e8..0000000 --- a/build/configurator/bin/configurator +++ /dev/null @@ -1,242 +0,0 @@ -#! /usr/bin/env python3 -# coding: utf8 -import argparse -import codecs -import json -import os -import random -import string -import sys - -from collections import OrderedDict - -import jinja2 - - -class Configurator: - - def __init__(self, **default_overrides): - """ - Default values are read, in decreasing order of priority, from: - - SETTING_ environment variable - - Existing config file (in `default_overrides`) - - Value passed to add() - """ - self.__values = OrderedDict() - self.__default_values = default_overrides - if os.environ.get('SILENT'): - self.__input = None - else: - self.__input = input - print("====================================\n" - " Interactive configuration \n" - "====================================") - - def as_dict(self): - return self.__values - - def get_default_value(self, name, default): - setting_name = 'SETTING_' + name.upper() - if os.environ.get(setting_name): - return os.environ[setting_name] - if name in self.__default_values: - return self.__default_values[name] - return default - - def add(self, name, default, question=""): - default = self.get_default_value(name, default) - if not self.__input or not question: - return self.set(name, default) - return self.set(name, self.ask(question, default)) - - def add_bool(self, name, default, question=""): - default = self.get_default_value(name, default) - if default in [1, '1']: - default = True - if default in [0, '0', '']: - default = False - if not self.__input or not question: - return self.set(name, default) - question += " (y/n)" - while True: - answer = self.ask(question, 'y' if default else 'n') - if answer is None or answer == '': - return self.set(name, default) - if answer.lower() in ['y', 'yes']: - return self.set(name, True) - if answer.lower() in ['n', 'no']: - return self.set(name, False) - - def add_choice(self, name, default, choices, question=""): - default = self.get_default_value(name, default) - if not self.__input or not question: - return self.set(name, default) - while True: - answer = self.ask(question, default) - if answer in choices: - return self.set(name, answer) - print("Invalid value. Choices are: {}".format(", ".join(choices))) - - def ask(self, question, default): - return self.__input('\1\2\x1b[35m> {} [{}] \x1b[39;49;00m'.format(question, default)) or default - - def get(self, name): - return self.__values.get(name) - - def set(self, name, value): - self.__values[name] = value - return self - - -def main(): - parser = argparse.ArgumentParser("Config file generator for Open edX") - parser.add_argument('-c', '--config', default=os.path.join("/", "openedx", "config", "config.json"), - help="Load default values from this file. Config values will be saved there.") - subparsers = parser.add_subparsers() - - parser_interactive = subparsers.add_parser('interactive') - parser_interactive.set_defaults(func=interactive) - - parser_substitute = subparsers.add_parser('substitute') - parser_substitute.add_argument('src', help="Template source directory") - parser_substitute.add_argument('dst', help="Destination configuration directory") - parser_substitute.set_defaults(func=substitute) - - args = parser.parse_args() - args.func(args) - -def load_config(path): - if os.path.exists(path): - with open(path) as f: - return json.load(f) - return {} - -def interactive(args): - interactive_configuration(args.config) - -def interactive_configuration(config_path): - configurator = Configurator(**load_config(config_path)) - configurator.add( - 'LMS_HOST', 'www.myopenedx.com', "Your website domain name for students (LMS)" - ).add( - 'CMS_HOST', 'studio.' + configurator.get('LMS_HOST'), "Your website domain name for teachers (CMS)" - ).add( - 'PLATFORM_NAME', "My Open edX", "Your platform name/title" - ).add( - 'CONTACT_EMAIL', 'contact@' + configurator.get('LMS_HOST'), "Your public contact email address", - ).add_choice( - 'LANGUAGE_CODE', 'en', - ['en', 'am', 'ar', 'az', 'bg-bg', 'bn-bd', 'bn-in', 'bs', 'ca', - 'ca@valencia', 'cs', 'cy', 'da', 'de-de', 'el', 'en-uk', 'en@lolcat', - 'en@pirate', 'es-419', 'es-ar', 'es-ec', 'es-es', 'es-mx', 'es-pe', - 'et-ee', 'eu-es', 'fa', 'fa-ir', 'fi-fi', 'fil', 'fr', 'gl', 'gu', - 'he', 'hi', 'hr', 'hu', 'hy-am', 'id', 'it-it', 'ja-jp', 'kk-kz', - 'km-kh', 'kn', 'ko-kr', 'lt-lt', 'ml', 'mn', 'mr', 'ms', 'nb', 'ne', - 'nl-nl', 'or', 'pl', 'pt-br', 'pt-pt', 'ro', 'ru', 'si', 'sk', 'sl', - 'sq', 'sr', 'sv', 'sw', 'ta', 'te', 'th', 'tr-tr', 'uk', 'ur', 'vi', - 'uz', 'zh-cn', 'zh-hk', 'zh-tw'], - "The default language code for the platform" - ).add( - 'SECRET_KEY', random_string(24) - ).add( - 'MYSQL_DATABASE', 'openedx' - ).add( - 'MYSQL_USERNAME', 'openedx' - ).add( - 'MYSQL_PASSWORD', random_string(8) - ).add( - 'MONGODB_DATABASE', 'openedx' - ).add( - 'NOTES_MYSQL_DATABASE', 'notes', - ).add( - 'NOTES_MYSQL_USERNAME', 'notes', - ).add( - 'NOTES_MYSQL_PASSWORD', random_string(8) - ).add( - 'NOTES_SECRET_KEY', random_string(24) - ).add( - 'NOTES_OAUTH2_SECRET', random_string(24) - ).add( - 'XQUEUE_AUTH_USERNAME', 'lms' - ).add( - 'XQUEUE_AUTH_PASSWORD', random_string(8) - ).add( - 'XQUEUE_MYSQL_DATABASE', 'xqueue', - ).add( - 'XQUEUE_MYSQL_USERNAME', 'xqueue', - ).add( - 'XQUEUE_MYSQL_PASSWORD', random_string(8) - ).add( - 'XQUEUE_SECRET_KEY', random_string(24) - ).add_bool( - 'ACTIVATE_HTTPS', False, "Activate SSL/TLS certificates for HTTPS access? Important note: this will NOT work in a development environment.", - ).add_bool( - 'ACTIVATE_NOTES', False, "Activate Student Notes service (https://open.edx.org/features/student-notes)?", - ).add_bool( - 'ACTIVATE_XQUEUE', False, "Activate Xqueue for external grader services? (https://github.com/edx/xqueue)", - ).add( - 'ID', random_string(8) - ) - - # Save values - with open(config_path, 'w') as f: - json.dump(configurator.as_dict(), f, sort_keys=True, indent=4) - set_owner(config_path) - - -def substitute(args): - config = load_config(args.config) - for root, _, filenames in os.walk(args.src): - for filename in filenames: - if filename.startswith('.'): - # Skip hidden files, such as files generated by the IDE - continue - src_file = os.path.join(root, filename) - dst_file = os.path.join(args.dst, os.path.relpath(src_file, args.src)) - substitute_file(config, src_file, dst_file) - -def substitute_file(config, src, dst): - with codecs.open(src, encoding='utf-8') as fi: - template = jinja2.Template(fi.read(), undefined=jinja2.StrictUndefined) - try: - substituted = template.render(**config) - except jinja2.exceptions.UndefinedError as e: - sys.stderr.write("ERROR Missing config value '{}' for template {}\n".format(e.args[0], src)) - sys.exit(1) - - dst_dir = os.path.dirname(dst) - ensure_path_exists(dst_dir) - with codecs.open(dst, encoding='utf-8', mode='w') as fo: - fo.write(substituted) - set_owner(dst) - - # Set same permissions as original file - os.chmod(dst, os.stat(src).st_mode) - -def ensure_path_exists(directory): - """ - Create the required subfolders one by one, if necessary, and assign them - the right uid/gid. - """ - to_create = [] - while not os.path.exists(directory): - to_create.append(directory) - directory = os.path.dirname(directory) - for d in to_create[::-1]: - os.mkdir(d) - set_owner(d) - -def set_owner(path): - """ - If the USER_ID environment variable is defined, use it to set the - owner/group of the given file path. - """ - user_id = int(os.environ.get('USERID', 0)) - if user_id: - os.chown(path, user_id, user_id) - -def random_string(length): - return "".join([random.choice(string.ascii_letters + string.digits) for _ in range(length)]) - -if __name__ == '__main__': - main() diff --git a/build/openedx/requirements/.gitignore b/build/openedx/requirements/.gitignore deleted file mode 100644 index 65bce68..0000000 --- a/build/openedx/requirements/.gitignore +++ /dev/null @@ -1 +0,0 @@ -private.txt diff --git a/build/openedx/requirements/README b/build/openedx/requirements/README deleted file mode 100644 index 7f2e34b..0000000 --- a/build/openedx/requirements/README +++ /dev/null @@ -1,3 +0,0 @@ -Add your additional requirements, such as xblocks, to private.txt. This file -is not under version control, which means that your changes will not be -committed to the upstream repository. diff --git a/build/openedx/themes/.gitignore b/build/openedx/themes/.gitignore deleted file mode 100644 index 72e8ffc..0000000 --- a/build/openedx/themes/.gitignore +++ /dev/null @@ -1 +0,0 @@ -* diff --git a/data/.gitignore b/data/.gitignore deleted file mode 100644 index 2cb709a..0000000 --- a/data/.gitignore +++ /dev/null @@ -1,13 +0,0 @@ -android/ -cms/ -cms_worker/ -letsencrypt/ -lms/ -lms_worker/ -elasticsearch/ -mysql/ -mongodb/ -openedx/ -portainer/ -rabbitmq/ -xqueue/ diff --git a/deploy/.gitignore b/deploy/.gitignore deleted file mode 100644 index 47def24..0000000 --- a/deploy/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/env/ diff --git a/deploy/Makefile b/deploy/Makefile deleted file mode 100644 index d1870e1..0000000 --- a/deploy/Makefile +++ /dev/null @@ -1,5 +0,0 @@ -.PHONY: env -PWD = $$(pwd) - -env: ## Generate the environment from templates and configuration values - @$(MAKE) -s -C .. substitute TEMPLATES=$(PWD)/templates OUTPUT=$(PWD)/env diff --git a/deploy/k8s/.gitignore b/deploy/k8s/.gitignore deleted file mode 100644 index 9939222..0000000 --- a/deploy/k8s/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/env/ -/data diff --git a/deploy/k8s/Makefile b/deploy/k8s/Makefile deleted file mode 100644 index 6dcee09..0000000 --- a/deploy/k8s/Makefile +++ /dev/null @@ -1,128 +0,0 @@ -.PHONY: env volumes services deployments -.DEFAULT_GOAL := help - -podname = kubectl get pods -n openedx --selector=app=$(1) -o name | head -1 | cut -d '/' -f 2 -podexec = kubectl exec -n openedx -it $$($(call podname,$(1))) -- $(2) - -all: configure deploy databases ## Provision a full platform from scratch - -configure: ## Interactive configuration - @$(MAKE) -s -C ../.. --always-make config.json - @$(MAKE) -s env - -env: ## Generate the environment from templates and configuration values - @$(MAKE) -s -C .. env - @$(MAKE) -s -C ../.. substitute TEMPLATES=$(PWD)/templates OUTPUT=$(PWD)/env - -namespace: ## Create the platform namespace - kubectl create -f namespace.yml -configmaps: ## Create configmap objects - kubectl create configmap nginx-config --from-file=../env/nginx --namespace=openedx - kubectl create configmap mysql-config --from-env-file=../env/mysql/auth.env --namespace=openedx - kubectl create configmap openedx-settings-lms --from-file=../env/openedx/settings/lms --namespace=openedx - kubectl create configmap openedx-settings-cms --from-file=../env/openedx/settings/cms --namespace=openedx - kubectl create configmap openedx-config --from-file=../env/openedx/config --namespace=openedx - kubectl create configmap openedx-scripts --from-file=../env/openedx/scripts --namespace=openedx -volumes: ## Create volumes - kubectl create -f volumes/ --recursive=true --namespace=openedx -services: ## Create services - kubectl create -f services/ --recursive=true --namespace=openedx -deployments: ## Create deployments - kubectl create -f deployments/ --recursive=true --namespace=openedx -ingress: ## Create ingress - kubectl create -f env/ingress.yml --namespace=openedx -deploy: namespace volumes configmaps services deployments ingress ## Deploy a platform from scratch - -upgrade: down ## Upgrade an already running platform - @$(MAKE) -s namespace || true - @$(MAKE) -s configmaps || true - @$(MAKE) -s volumes || true - @$(MAKE) -s services || true - @$(MAKE) -s deployments || true - @$(MAKE) -s ingress || true - -down: ## Delete all non-persistent objects - kubectl delete -f services/ --recursive=true --namespace=openedx || true - kubectl delete -f deployments/ --recursive=true --namespace=openedx || true - kubectl delete configmap --all --namespace openedx || true - -delete: ## Delete EVERYTHING! (with no confirmation at all) - # TODO ask question to make sure user reaaaaaaally want to do this - kubectl delete namespace openedx - - -##################### Databases - -databases: provision-databases migrate #provision-oauth2 ## Bootstrap databases -provision-databases: ## Create necessary databases and users - $(call podexec,lms,bash /openedx/scripts/provision.sh) -provision-oauth2: ## Create users for SSO between services - $(call podexec,lms,bash /openedx/scripts/oauth2.sh) -migrate: migrate-openedx migrate-forum ## Perform all database migrations -migrate-openedx: migrate-lms migrate-cms reindex-courses ## Perform database migrations on LMS/CMS -migrate-lms: - $(call podexec,lms,bash -c 'dockerize -wait tcp://mysql:3306 -timeout 20s && ./manage.py lms migrate') -migrate-cms: - $(call podexec,cms,bash -c 'dockerize -wait tcp://mysql:3306 -timeout 20s && ./manage.py cms migrate') -migrate-forum: ## Perform database migrations on discussion forums - $(call podexec,forum,bash -c "bundle exec rake search:initialize && \ - bundle exec rake search:rebuild_index") -migrate-notes: ## Perform database migrations for the Notes service - $(call podexec,notes,./manage.py migrate) -migrate-xqueue: ## Perform database migrations for the XQueue service - $(call podexec,xqueue,./manage.py migrate) -reindex-courses: ## Refresh course index so they can be found in the LMS search engine - $(call podexec,cms,./manage.py cms reindex_course --all --setup) - -##################### Various Open edX commands - -lms-shell: ## Launch a shell inside an lms pod - $(call podexec,lms,bash) -lms-exec: ## Execute a command inside an lms pod: make lms-exec COMMAND="..." - $(call podexec,lms,$(COMMAND)) -pod-exec: ## Execute a command inside an arbitrary pod: make pod-exec APP=appname COMMAND="..." - $(call podexec,$(APP),$(COMMAND)) - -# TODO replace these tasks with Job objects -# Note that here, settings must be defined manually because "exec" does not use -# the docker entrypoint, and thus does not define the DJANGO_SETTINGS_MODULE -# environment variable. -demo-course: ## Import the demo course from edX - $(call podexec,cms,/bin/bash -c "\ - git clone https://github.com/edx/edx-demo-course --branch open-release/hawthorn.2 --depth 1 ../edx-demo-course \ - && python ./manage.py cms --settings=tutor.production import ../data ../edx-demo-course") - -staff-user: ## Create a user with admin rights: make staff-user USERNAME=... EMAIL=... - $(call podexec,lms,/bin/bash -c "\ - ./manage.py lms manage_user --superuser --staff ${USERNAME} ${EMAIL} \ - && ./manage.py lms --settings=tutor.production changepassword ${USERNAME}") - -##################### Various minikube command - -minikube-start: ## Start minikube - minikube start -minikube-stop: ## Stop minikube - minikube stop -minikube-dashboard: ## Open the minikube dashboard - minikube dashboard -minikube-ingress: ## Enable the ingress addon - minikube addons enable ingress - -##################### Various k8s commands - -k8s-proxy: ## Create a proxy to the Kubernetes API server - kubectl proxy -k8s-dashboard: ## Create the dashboard - kubectl create -f https://raw.githubusercontent.com/kubernetes/dashboard/master/aio/deploy/recommended/kubernetes-dashboard.yaml -k8s-admin: ## Create an admin user for accessing the Kubernetes dashboard - kubectl -f admin.yml -k8s-admin-token: ## Print the admin token required to log in the dashboard - kubectl -n kube-system describe secret $$(kubectl -n kube-system get secret | grep admin-user | awk '{print $$1}') | grep token: | awk '{print $$2}' - -##################### Information - -ESCAPE =  -help: ## Print this help - @grep -E '^([a-zA-Z0-9_-]+:.*?## .*|######* .+)$$' Makefile \ - | sed 's/######* \(.*\)/\n $(ESCAPE)[1;31m\1$(ESCAPE)[0m/g' \ - | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[33m%-30s\033[0m %s\n", $$1, $$2}' diff --git a/deploy/k8s/deployments/cms.yml b/deploy/k8s/deployments/cms.yml deleted file mode 100644 index 9cbee8a..0000000 --- a/deploy/k8s/deployments/cms.yml +++ /dev/null @@ -1,50 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: cms -spec: - replicas: 1 - selector: - matchLabels: - app: cms - template: - metadata: - labels: - app: cms - spec: - containers: - - name: cms - image: regis/openedx:hawthorn - env: - - name: SERVICE_VARIANT - value: cms - ports: - - containerPort: 8000 - volumeMounts: - - mountPath: /openedx/edx-platform/lms/envs/tutor/ - name: settings-lms - - mountPath: /openedx/edx-platform/cms/envs/tutor/ - name: settings-cms - - mountPath: /openedx/config - name: config - - mountPath: /openedx/scripts - name: scripts - - mountPath: /openedx/data - name: data - #imagePullPolicy: Always - volumes: - - name: settings-lms - configMap: - name: openedx-settings-lms - - name: settings-cms - configMap: - name: openedx-settings-cms - - name: config - configMap: - name: openedx-config - - name: scripts - configMap: - name: openedx-scripts - - name: data - persistentVolumeClaim: - claimName: cms-data diff --git a/deploy/k8s/deployments/forum.yml b/deploy/k8s/deployments/forum.yml deleted file mode 100644 index 625f947..0000000 --- a/deploy/k8s/deployments/forum.yml +++ /dev/null @@ -1,20 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: forum -spec: - replicas: 1 - selector: - matchLabels: - app: forum - template: - metadata: - labels: - app: forum - spec: - containers: - - name: forum - image: regis/openedx-forum:hawthorn - ports: - - containerPort: 4567 - imagePullPolicy: Always diff --git a/deploy/k8s/deployments/lms.yml b/deploy/k8s/deployments/lms.yml deleted file mode 100644 index 7b3d23f..0000000 --- a/deploy/k8s/deployments/lms.yml +++ /dev/null @@ -1,47 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: lms -spec: - replicas: 1 - selector: - matchLabels: - app: lms - template: - metadata: - labels: - app: lms - spec: - containers: - - name: lms - image: regis/openedx:hawthorn - ports: - - containerPort: 8000 - volumeMounts: - - mountPath: /openedx/edx-platform/lms/envs/tutor/ - name: settings-lms - - mountPath: /openedx/edx-platform/cms/envs/tutor/ - name: settings-cms - - mountPath: /openedx/config - name: config - - mountPath: /openedx/scripts - name: scripts - - mountPath: /openedx/data - name: data - imagePullPolicy: Always - volumes: - - name: settings-lms - configMap: - name: openedx-settings-lms - - name: settings-cms - configMap: - name: openedx-settings-cms - - name: config - configMap: - name: openedx-config - - name: scripts - configMap: - name: openedx-scripts - - name: data - persistentVolumeClaim: - claimName: lms-data diff --git a/deploy/k8s/deployments/memcached.yml b/deploy/k8s/deployments/memcached.yml deleted file mode 100644 index d696953..0000000 --- a/deploy/k8s/deployments/memcached.yml +++ /dev/null @@ -1,19 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: memcached -spec: - replicas: 1 - selector: - matchLabels: - app: memcached - template: - metadata: - labels: - app: memcached - spec: - containers: - - name: memcached - image: memcached:1.4.38 - ports: - - containerPort: 11211 diff --git a/deploy/k8s/deployments/mongodb.yml b/deploy/k8s/deployments/mongodb.yml deleted file mode 100644 index cb81e78..0000000 --- a/deploy/k8s/deployments/mongodb.yml +++ /dev/null @@ -1,26 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: mongodb -spec: - replicas: 1 - selector: - matchLabels: - app: mongodb - template: - metadata: - labels: - app: mongodb - spec: - containers: - - name: mongodb - image: mongo:3.2.16 - command: ["mongod", "--smallfiles", "--nojournal", "--storageEngine", "wiredTiger"] - ports: - - containerPort: 27017 - volumeMounts: - - mountPath: /data/db - name: data - volumes: - - name: data - emptyDir: {} diff --git a/deploy/k8s/deployments/mysql.yml b/deploy/k8s/deployments/mysql.yml deleted file mode 100644 index f92a561..0000000 --- a/deploy/k8s/deployments/mysql.yml +++ /dev/null @@ -1,32 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: mysql -spec: - replicas: 1 - selector: - matchLabels: - app: mysql - template: - metadata: - labels: - app: mysql - spec: - containers: - - name: mysql - image: mysql:5.6.36 - env: - - name: MYSQL_ROOT_PASSWORD - valueFrom: - configMapKeyRef: - name: mysql-config - key: MYSQL_ROOT_PASSWORD - ports: - - containerPort: 3306 - volumeMounts: - - mountPath: /var/lib/mysql - name: data - volumes: - - name: data - persistentVolumeClaim: - claimName: mysql diff --git a/deploy/k8s/deployments/nginx.yml b/deploy/k8s/deployments/nginx.yml deleted file mode 100644 index a065352..0000000 --- a/deploy/k8s/deployments/nginx.yml +++ /dev/null @@ -1,54 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: nginx -spec: - replicas: 1 - selector: - matchLabels: - app: nginx - template: - metadata: - labels: - app: nginx - spec: - initContainers: - - name: clean-openedx-staticfiles - image: regis/openedx:hawthorn - command: ['rm', '-rf', '/var/www/openedx/staticfiles'] - volumeMounts: - - mountPath: /var/www/openedx/ - name: openedx-staticfiles - imagePullPolicy: Always - - name: init-openedx-staticfiles - image: regis/openedx:hawthorn - command: ['cp', '-r', '/openedx/staticfiles', '/var/www/openedx/'] - volumeMounts: - - mountPath: /var/www/openedx/ - name: openedx-staticfiles - imagePullPolicy: Always - containers: - - name: nginx - image: nginx:1.13 - volumeMounts: - - mountPath: /etc/nginx/conf.d/ - name: config - - mountPath: /var/www/openedx/ - name: openedx-staticfiles - - mountPath: /openedx/data/lms - name: data - ports: - - containerPort: 80 - name: http-port - - containerPort: 443 - name: https-port - volumes: - - name: config - configMap: - name: nginx-config - - name: openedx-staticfiles - persistentVolumeClaim: - claimName: openedx-staticfiles - - name: data - persistentVolumeClaim: - claimName: lms-data diff --git a/deploy/k8s/deployments/rabbitmq.yml b/deploy/k8s/deployments/rabbitmq.yml deleted file mode 100644 index 0d8ba3e..0000000 --- a/deploy/k8s/deployments/rabbitmq.yml +++ /dev/null @@ -1,26 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: rabbitmq -spec: - replicas: 1 - selector: - matchLabels: - app: rabbitmq - template: - metadata: - labels: - app: rabbitmq - spec: - containers: - - name: rabbitmq - image: rabbitmq:3.6.10 - ports: - - containerPort: 5672 - volumeMounts: - - mountPath: /var/lib/rabbitmq - name: data - volumes: - - name: data - persistentVolumeClaim: - claimName: rabbitmq diff --git a/deploy/k8s/services/cms.yml b/deploy/k8s/services/cms.yml deleted file mode 100644 index 62e3ecd..0000000 --- a/deploy/k8s/services/cms.yml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: cms -spec: - type: NodePort - ports: - - port: 8000 - protocol: TCP - selector: - app: cms diff --git a/deploy/k8s/services/forum.yml b/deploy/k8s/services/forum.yml deleted file mode 100644 index b3791fc..0000000 --- a/deploy/k8s/services/forum.yml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: forum -spec: - type: NodePort - ports: - - port: 4567 - protocol: TCP - selector: - app: forum diff --git a/deploy/k8s/services/lms.yml b/deploy/k8s/services/lms.yml deleted file mode 100644 index 23d8c52..0000000 --- a/deploy/k8s/services/lms.yml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: lms -spec: - type: NodePort - ports: - - port: 8000 - protocol: TCP - selector: - app: lms diff --git a/deploy/k8s/services/memcached.yml b/deploy/k8s/services/memcached.yml deleted file mode 100644 index 26678e3..0000000 --- a/deploy/k8s/services/memcached.yml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: memcached -spec: - type: NodePort - ports: - - port: 11211 - protocol: TCP - selector: - app: memcached diff --git a/deploy/k8s/services/mongodb.yml b/deploy/k8s/services/mongodb.yml deleted file mode 100644 index 3b64098..0000000 --- a/deploy/k8s/services/mongodb.yml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: mongodb -spec: - type: NodePort - ports: - - port: 27017 - protocol: TCP - selector: - app: mongodb diff --git a/deploy/k8s/services/mysql.yml b/deploy/k8s/services/mysql.yml deleted file mode 100644 index 6a5ee86..0000000 --- a/deploy/k8s/services/mysql.yml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: mysql -spec: - type: NodePort - ports: - - port: 3306 - protocol: TCP - selector: - app: mysql diff --git a/deploy/k8s/services/nginx.yml b/deploy/k8s/services/nginx.yml deleted file mode 100644 index 2ef2f3f..0000000 --- a/deploy/k8s/services/nginx.yml +++ /dev/null @@ -1,17 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: nginx -spec: - type: NodePort - ports: - - port: 80 - protocol: TCP - name: http - targetPort: http-port - - port: 443 - protocol: TCP - name: https - targetPort: https-port - selector: - app: nginx diff --git a/deploy/k8s/services/rabbitmq.yml b/deploy/k8s/services/rabbitmq.yml deleted file mode 100644 index d8e1599..0000000 --- a/deploy/k8s/services/rabbitmq.yml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: rabbitmq -spec: - type: NodePort - ports: - - port: 5672 - protocol: TCP - selector: - app: rabbitmq diff --git a/deploy/k8s/volumes/cms-data.yml b/deploy/k8s/volumes/cms-data.yml deleted file mode 100644 index 69a78ff..0000000 --- a/deploy/k8s/volumes/cms-data.yml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: cms-data -spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 2Gi diff --git a/deploy/k8s/volumes/lms-data-pvc.yml b/deploy/k8s/volumes/lms-data-pvc.yml deleted file mode 100644 index 429af39..0000000 --- a/deploy/k8s/volumes/lms-data-pvc.yml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: lms-data -spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 2Gi diff --git a/deploy/k8s/volumes/mysql-pvc.yml b/deploy/k8s/volumes/mysql-pvc.yml deleted file mode 100644 index 46a7f10..0000000 --- a/deploy/k8s/volumes/mysql-pvc.yml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: mysql -spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 5Gi diff --git a/deploy/k8s/volumes/openedx-staticfiles.yml b/deploy/k8s/volumes/openedx-staticfiles.yml deleted file mode 100644 index 754d0ea..0000000 --- a/deploy/k8s/volumes/openedx-staticfiles.yml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: openedx-staticfiles -spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 1Gi diff --git a/deploy/k8s/volumes/rabbitmq.yml b/deploy/k8s/volumes/rabbitmq.yml deleted file mode 100644 index a5cc473..0000000 --- a/deploy/k8s/volumes/rabbitmq.yml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: rabbitmq -spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 1Gi diff --git a/deploy/local/.gitignore b/deploy/local/.gitignore deleted file mode 100644 index d66b519..0000000 --- a/deploy/local/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -/Makefile.env -/docker-compose.yml -/.env diff --git a/deploy/local/Makefile b/deploy/local/Makefile deleted file mode 100644 index 34b0643..0000000 --- a/deploy/local/Makefile +++ /dev/null @@ -1,194 +0,0 @@ -.PHONY: env -.DEFAULT_GOAL := help - -PWD = $$(pwd) -USERID ?= $$(id -u) -EDX_PLATFORM_SETTINGS ?= tutor.production --include Makefile.env - -DOCKER_COMPOSE_RUN = docker-compose run --rm -DOCKER_COMPOSE_RUN_OPENEDX = $(DOCKER_COMPOSE_RUN) -e SETTINGS=$(EDX_PLATFORM_SETTINGS) \ - --volume="$(PWD)/../../build/openedx/themes:/openedx/themes" -ifneq ($(EDX_PLATFORM_PATH),) - DOCKER_COMPOSE_RUN_OPENEDX += -e USERID=$(USERID) --volume="$(EDX_PLATFORM_PATH):/openedx/edx-platform" -endif - -DOCKER_COMPOSE_RUN_LMS = $(DOCKER_COMPOSE_RUN_OPENEDX) -p 8000:8000 lms -DOCKER_COMPOSE_RUN_CMS = $(DOCKER_COMPOSE_RUN_OPENEDX) -p 8001:8001 cms - -##################### Running Open edX - -all: env ## Configure and run a full-featured platform - @$(MAKE) stats - @$(MAKE) https-certificate - @$(MAKE) update - @$(MAKE) databases - @$(MAKE) daemonize - @echo "All set \o/ You can access the LMS at http://localhost and the CMS at http://studio.localhost" - -run: ## Run the complete platform - docker-compose up -up: run - -daemonize: ## Run the complete platform, with daemonization - docker-compose up -d - @echo "Daemon is up and running" -daemon: daemonize - -stop: ## Stop all services - docker-compose rm --stop --force - -##################### Docker image management - -update: ## Download most recent images - docker-compose pull - -##################### Configuration - -configure: ## Interactive configuration - @$(MAKE) -s -C ../.. --always-make config.json - @$(MAKE) -s env - -env: ## Generate the environment from templates and configuration values - @$(MAKE) -s -C .. env - @$(MAKE) -s -C ../.. substitute TEMPLATES=$(PWD)/templates OUTPUT=$(PWD) - -##################### Database - -databases: init-mysql provision-databases migrate provision-oauth2 ## Bootstrap databases - -init-mysql: ## Make sure that mysql is properly initialized - @(stat ../../data/mysql/mysql 1> /dev/null 2>&1 && echo "MySQL has already been initialized") || (\ - echo "Waiting for mysql initialization..." \ - && docker-compose up -d mysql \ - && until docker-compose logs mysql | grep "MySQL init process done. Ready for start up."; do \ - sleep 1;\ - done \ - && docker-compose stop mysql\ - ) -provision-databases: ## Create necessary databases and users - $(DOCKER_COMPOSE_RUN) -v $(PWD)/../env/openedx/scripts:/openedx/scripts lms /openedx/scripts/provision.sh -provision-oauth2: ## Create users for SSO between services - $(DOCKER_COMPOSE_RUN) -v $(PWD)/../env/openedx/scripts:/openedx/scripts lms /openedx/scripts/oauth2.sh - -migrate: migrate-openedx migrate-forum ## Perform all database migrations - @if [ "$(ACTIVATE_XQUEUE)" = "1" ] ; then \ - $(MAKE) migrate-xqueue ;\ - fi - @if [ "$(ACTIVATE_NOTES)" = "1" ] ; then \ - $(MAKE) migrate-notes ; \ - fi -migrate-openedx: ## Perform database migrations on LMS/CMS - $(DOCKER_COMPOSE_RUN) lms bash -c "dockerize -wait tcp://mysql:3306 -timeout 20s && ./manage.py lms migrate" - $(DOCKER_COMPOSE_RUN) cms bash -c "dockerize -wait tcp://mysql:3306 -timeout 20s && ./manage.py cms migrate" - $(MAKE) reindex-courses -migrate-forum: ## Perform database migrations on discussion forums - $(DOCKER_COMPOSE_RUN) forum bash -c "bundle exec rake search:initialize && \ - bundle exec rake search:rebuild_index" -migrate-notes: ## Perform database migrations for the Notes service - $(DOCKER_COMPOSE_RUN) notes ./manage.py migrate -migrate-xqueue: ## Perform database migrations for the XQueue service - $(DOCKER_COMPOSE_RUN) xqueue ./manage.py migrate - -reindex-courses: ## Refresh course index so they can be found in the LMS search engine - $(DOCKER_COMPOSE_RUN) cms ./manage.py cms reindex_course --all --setup - -##################### Static assets - -# To collect assets we don't rely on the "paver update_assets" command because -# webpack collection incorrectly sets the NODE_ENV variable when using custom -# settings. Thus, each step must be performed separately. This should be fixed -# in the next edx-platform release thanks to https://github.com/edx/edx-platform/pull/18430/ - -assets: ## Copy production-ready static assets. Note that you should not have to run this command yourself: all assets should be sent to nginx on boot. - docker-compose run --rm openedx-assets -assets-development: ## Generate static assets for local development - $(DOCKER_COMPOSE_RUN_OPENEDX) --no-deps lms bash -c "openedx-assets build --env=dev" -assets-development-lms: - $(DOCKER_COMPOSE_RUN_OPENEDX) --no-deps lms bash -c "openedx-assets build --env=dev --system lms" -assets-development-cms: - $(DOCKER_COMPOSE_RUN_OPENEDX) --no-deps lms bash -c "openedx-assets build --env=dev --system cms" -watch-themes: ## Watch for changes in your themes and build development assets - $(DOCKER_COMPOSE_RUN_OPENEDX) --no-deps lms openedx-assets watch-themes --env dev - -#################### Logging - -logs: ## Print all logs from a service since it started. E.g: "make logs service=lms", "make logs service=nginx" - docker-compose logs $(service) -tail: ## Similar to "tail" on the logs of a service. E.g: "make tail service=lms", "make tail service=nginx" - docker-compose logs --tail=10 $(service) -tail-follow: ## Similar to "tail -f" on the logs of a service. E.g: "make tail-follow service=lms", "make tail-follow service=nginx" - docker-compose logs --tail=10 -f $(service) - -##################### Development - -lms: ## Open a bash shell in the LMS - $(DOCKER_COMPOSE_RUN_LMS) bash -cms: ## Open a bash shell in the CMS - $(DOCKER_COMPOSE_RUN_CMS) bash - -lms-python: ## Open a python shell in the LMS - $(DOCKER_COMPOSE_RUN_OPENEDX) lms ./manage.py lms shell -lms-shell: lms-python -lms-runserver: ## Run a local webserver, useful for debugging - $(DOCKER_COMPOSE_RUN_LMS) ./manage.py lms runserver 0.0.0.0:8000 -cms-python: ## Open a python shell in the CMS - $(DOCKER_COMPOSE_RUN_OPENEDX) cms ./manage.py cms shell -cms-shell: cms-python -cms-runserver: ## Run a local webserver, useful for debugging - $(DOCKER_COMPOSE_RUN_CMS) ./manage.py cms runserver 0.0.0.0:8001 - -restart-openedx: ## Restart lms, cms, and workers - docker-compose restart openedx-assets lms lms_worker cms cms_worker - -##################### SSL/TLS (HTTPS certificates) - -https_command = docker run --rm -it \ - --volume="$(PWD)/../env/letsencrypt/:/openedx/letsencrypt/env/" \ - --volume="$(PWD)/../../data/letsencrypt/:/etc/letsencrypt/" \ - -p "80:80" -certbot_image = certbot/certbot:latest - -https-certificate: ## Generate https certificates - @if [ "$(ACTIVATE_HTTPS)" = "1" ] ; then \ - $(https_command) --entrypoint "/openedx/letsencrypt/env/certonly.sh" $(certbot_image); \ - fi - -https-certificate-renew: ## Renew https certificates - $(https_command) $(certbot_image) renew - -##################### Additional commands - -stats: ## Collect anonymous information about the platform - @if [ "$(DISABLE_STATS)" != "1" ] ; then \ - docker run --rm -it --volume="$(PWD)/../env/openedx/scripts:/openedx/scripts" \ - regis/openedx-configurator:hawthorn /openedx/scripts/stats 2> /dev/null || true ; \ - fi - -demo-course: ## Import the demo course from edX - $(DOCKER_COMPOSE_RUN_OPENEDX) cms /bin/bash -c " \ - git clone https://github.com/edx/edx-demo-course --branch open-release/hawthorn.2 --depth 1 ../edx-demo-course \ - && python ./manage.py cms import ../data ../edx-demo-course" - -staff-user: ## Create a user with admin rights: make staff-user USERNAME=... EMAIL=... - $(DOCKER_COMPOSE_RUN_OPENEDX) lms /bin/bash -c "./manage.py lms manage_user --superuser --staff ${USERNAME} ${EMAIL} && ./manage.py lms changepassword ${USERNAME}" - -portainer: ## Run Portainer (https://portainer.io), a UI for container supervision - docker run --rm -it \ - --volume=/var/run/docker.sock:/var/run/docker.sock \ - --volume=$(PWD)/../../data/portainer:/data \ - -p 9000:9000 \ - portainer/portainer:latest - @echo "View the Portainer UI at http://localhost:9000" - -##################### Information - -info: - $(MAKE) -s -C ../.. info - -# Obtained by running "echo '\033' in a shell -ESCAPE =  -help: ## Print this help - @grep -E '^([a-zA-Z_-]+:.*?## .*|######* .+)$$' Makefile \ - | sed 's/######* \(.*\)/\n $(ESCAPE)[1;31m\1$(ESCAPE)[0m/g' \ - | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[33m%-30s\033[0m %s\n", $$1, $$2}' diff --git a/deploy/local/templates/Makefile.env b/deploy/local/templates/Makefile.env deleted file mode 100644 index 5bf0189..0000000 --- a/deploy/local/templates/Makefile.env +++ /dev/null @@ -1,3 +0,0 @@ -ACTIVATE_HTTPS ?= {{ 1 if ACTIVATE_HTTPS else 0 }} -ACTIVATE_XQUEUE ?= {{ 1 if ACTIVATE_XQUEUE else 0 }} -ACTIVATE_NOTES ?= {{ 1 if ACTIVATE_NOTES else 0 }} diff --git a/deploy/templates/letsencrypt/certonly.sh b/deploy/templates/letsencrypt/certonly.sh deleted file mode 100755 index 750b69a..0000000 --- a/deploy/templates/letsencrypt/certonly.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/sh -certbot certonly --standalone -n --agree-tos -m admin@{{ LMS_HOST }} -d {{ LMS_HOST }} -d {{ CMS_HOST }} -d preview.{{ LMS_HOST }} - -{% if ACTIVATE_NOTES %} -certbot certonly --standalone -n --agree-tos -m admin@{{ LMS_HOST }} -d notes.{{ LMS_HOST }} -{% endif %} diff --git a/deploy/templates/openedx/scripts/oauth2.sh b/deploy/templates/openedx/scripts/oauth2.sh deleted file mode 100755 index ce9b05b..0000000 --- a/deploy/templates/openedx/scripts/oauth2.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash -e - -{% if ACTIVATE_NOTES %} -./manage.py lms manage_user notes notes@{{ LMS_HOST }} --staff --superuser -./manage.py lms create_oauth2_client \ - "http://notes.openedx:8000" \ - "http://notes.openedx:8000/complete/edx-oidc/" \ - confidential \ - --client_name edx-notes --client_id notes --client_secret {{ NOTES_OAUTH2_SECRET }} \ - --trusted --logout_uri "http://notes.openedx:8000/logout/" --username notes -{% endif %} diff --git a/deploy/templates/openedx/scripts/provision.sh b/deploy/templates/openedx/scripts/provision.sh deleted file mode 100755 index b540ca6..0000000 --- a/deploy/templates/openedx/scripts/provision.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/bash -e -dockerize -wait tcp://mysql:3306 -timeout 20s - -while ! mysql -u root --password="{{ MYSQL_PASSWORD }}" --host "mysql" -e="\r"; do - echo "Waiting for mysql database to be ready..." - sleep 1 -done - -mysql -u root --password="{{ MYSQL_PASSWORD }}" --host "mysql" -e 'CREATE DATABASE IF NOT EXISTS {{ MYSQL_DATABASE }};' -mysql -u root --password="{{ MYSQL_PASSWORD }}" --host "mysql" -e 'GRANT ALL ON {{ MYSQL_DATABASE }}.* TO "{{ MYSQL_USERNAME }}"@"%" IDENTIFIED BY "{{ MYSQL_PASSWORD }}";' - -{% if ACTIVATE_NOTES %} -mysql -u root --password="{{ MYSQL_PASSWORD }}" --host "mysql" -e 'CREATE DATABASE IF NOT EXISTS {{ NOTES_MYSQL_DATABASE }};' -mysql -u root --password="{{ MYSQL_PASSWORD }}" --host "mysql" -e 'GRANT ALL ON {{ NOTES_MYSQL_DATABASE }}.* TO "{{ NOTES_MYSQL_USERNAME }}"@"%" IDENTIFIED BY "{{ NOTES_MYSQL_PASSWORD }}";' -{% endif %} - -{% if ACTIVATE_XQUEUE %} -mysql -u root --password="{{ MYSQL_PASSWORD }}" --host "mysql" -e 'CREATE DATABASE IF NOT EXISTS {{ XQUEUE_MYSQL_DATABASE }};' -mysql -u root --password="{{ MYSQL_PASSWORD }}" --host "mysql" -e 'GRANT ALL ON {{ XQUEUE_MYSQL_DATABASE }}.* TO "{{ XQUEUE_MYSQL_USERNAME }}"@"%" IDENTIFIED BY "{{ XQUEUE_MYSQL_PASSWORD }}";' -{% endif %} diff --git a/deploy/templates/openedx/scripts/stats b/deploy/templates/openedx/scripts/stats deleted file mode 100755 index a193f9a..0000000 --- a/deploy/templates/openedx/scripts/stats +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh -# This is a small script that helps me collect stats about who is using this -# project and understand how they are using it. Please don't remove me! As you -# can see, no personal data is leaked. -curl --silent -X POST --max-time 5 "https://openedx.overhang.io/stats?id={{ ID }}&lms={{ LMS_HOST|urlencode }}" 2> /dev/null || true diff --git a/docs/Makefile b/docs/Makefile index e4ffe88..8f0414e 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -13,7 +13,7 @@ help: browse: sensible-browser _build/html/index.html -watch: +watch: html while true; do inotifywait -e modify *.rst; $(MAKE) html; done .PHONY: help Makefile diff --git a/docs/customise.rst b/docs/customise.rst index 16376e4..c80b88d 100644 --- a/docs/customise.rst +++ b/docs/customise.rst @@ -1,26 +1,29 @@ .. _customise: -Customising the ``openedx`` docker image -======================================== +Open edX platform customisation +=============================== -The LMS and the CMS all run from the ``openedx`` docker image. The base image is downloaded from `Docker Hub `_ when we run ``make update`` (or ``make all``). But you can also customise and build the image yourself. All image-building commands must be run inside the ``build`` folder:: +There are different ways you can customise your Open edX platform. For instance, optional features can be activated during configuration. But if you want to add unique features to your Open edX platform, you are going to have to modify and re-build the ``openedx`` docker image. This is the image that contains the ``edx-platform`` repository: it is in charge of running the web application for the Open edX "core". Both the LMS and the CMS run from the ``openedx`` docker image. - cd build +On a vanilla platform deployed by Tutor, the image that is run is downloaded from the `regis/openedx repository on Docker Hub `_. This is also the image that is downloaded whenever we run ``tutor local pullimages``. But you can decide to build the image locally instead of downloading it. To do so, generate the image-building environment:: -Build the base image with:: + tutor images env - make build-openedx +Then, build and tag the ``openedx`` image:: -The following sections describe how to modify various aspects of the docker image. After you have built your own image, you can run it as usual:: + tutor images build openedx - make run +The following sections describe how to modify various aspects of the docker image. Every time, you will have to re-build your own image with this command. Re-building should take ~20 minutes on a server with good bandwidth. After that, your custom image will be used for all commands. For instance: -Custom themes -------------- +1. you can start a local platform with ``tutor local start`` +2. restart the services that use the ``openedx`` image with ``tutor local restart openedx`` -Comprehensive theming is enabled by default, but only the default theme is compiled. To compile your own theme, add it to the ``build/openedx/themes/`` folder:: +Adding custom themes +-------------------- - git clone https://github.com/me/myopenedxtheme.git build/openedx/themes/ +Comprehensive theming is enabled by default, but only the default theme is compiled. To compile your own theme, add it to the ``env/build/openedx/themes/`` folder:: + + git clone https://github.com/me/myopenedxtheme.git env/build/openedx/themes/ The ``themes`` folder should have the following structure:: @@ -35,20 +38,20 @@ The ``themes`` folder should have the following structure:: Then you must rebuild the openedx Docker image:: - make build-openedx + tutor images build openedx -Finally, follow the `Open edX documentation to enable your themes `_. +Finally, follow the `Open edX documentation to apply your themes `_. You will not have to modify the ``lms.env.json``/``cms.env.json`` files; just follow the instructions to add a site theme in http://localhost/admin (starting from step 3). -Extra xblocks and requirements ------------------------------- +Installing extra xblocks and requirements +----------------------------------------- -Would you like to include custom xblocks, or extra requirements to your Open edX platform? Additional requirements can be added to the ``openedx/requirements/private.txt`` file. For instance, to include the `polling xblock from Opencraft `_:: +Would you like to include custom xblocks, or extra requirements to your Open edX platform? Additional requirements can be added to the ``env/build/openedx/requirements/private.txt`` file. For instance, to include the `polling xblock from Opencraft `_:: - echo "git+https://github.com/open-craft/xblock-poll.git" >> openedx/requirements/private.txt + echo "git+https://github.com/open-craft/xblock-poll.git" >> env/build/openedx/requirements/private.txt Then, the ``openedx`` docker image must be rebuilt:: - make build-openedx + tutor images build openedx To install xblocks from a private repository that requires authentication, you must first clone the repository inside the ``openedx/requirements`` folder on the host:: @@ -56,29 +59,34 @@ To install xblocks from a private repository that requires authentication, you m Then, declare your extra requirements with the ``-e`` flag in ``openedx/requirements/private.txt``:: - echo "-e ./myprivaterepo" >> openedx/requirements/private.txt + echo "-e ./myprivaterepo" >> env/build/openedx/requirements/private.txt -Forked version of edx-platform ------------------------------- +.. _edx_platform_fork: + +Running a fork of ``edx-platform`` +---------------------------------- You may want to run your own flavor of edx-platform instead of the `official version `_. To do so, you will have to re-build the openedx image with the proper environment variables pointing to your repository and version:: - export EDX_PLATFORM_REPOSITORY=https://mygitrepo/edx-platform.git EDX_PLATFORM_VERSION=my-tag-or-branch - make build-openedx - -You can then restart the services which will now be running your forked version of edx-platform:: - - make restart-openedx + tutor images build openedx \ + --build-arg EDX_PLATFORM_REPOSITORY=https://mygitrepo/edx-platform.git \ + --build-arg EDX_PLATFORM_VERSION=my-tag-or-branch Note that your release must be a fork of Hawthorn in order to work. Otherwise, you may have important compatibility issues with other services. In particular, **don't try to run Tutor with older versions of Open edX**. Running a different ``openedx`` Docker image -------------------------------------------- -By default, Tutor runs the `regis/openedx `_ docker image from Docker Hub. If you have an account on `hub.docker.com `_ or you have a private image registry, you can build your image and push it to your registry. Then add the following content to the ``deploy/local/.env`` file:: +By default, Tutor runs the `regis/openedx `_ docker image from Docker Hub. If you have an account on `hub.docker.com `_ or you have a private image registry, you can build your image and push it to your registry:: - OPENEDX_DOCKER_IMAGE=myusername/myimage:mytag + tutor images build openedx --namespace=myusername --version=mytag + tutor images push openedx --namespace=myusername --version=mytag -Your own image will be used next time you run ``make run``. +Then add the following value to your ``config.yml``:: -Note that the ``make build`` and ``make push`` commands (from the ``build/`` folder) will no longer work as you expect and that you are responsible for building and pushing the image yourself. + OPENEDX_DOCKER_IMAGE: myusername/openedx:mytag + +This value will then be used by Tutor when generating your environment. For instance, this is how you would use your image in a local deployment:: + + tutor local env + tutor local quickstart diff --git a/docs/dev.rst b/docs/dev.rst index 9b1913a..4ef3104 100644 --- a/docs/dev.rst +++ b/docs/dev.rst @@ -5,61 +5,56 @@ Using Tutor for Open edX development In addition to running Open edX in production, you can use the docker containers for local development. This means you can hack on Open edX without setting up a Virtual Machine. Essentially, this replaces the devstack provided by edX. -To begin with, define development settings:: - - export EDX_PLATFORM_SETTINGS=tutor.development - -These settings are necessary to run a local platform in debug mode. - -Run a local webserver ---------------------- +Run a local development webserver +--------------------------------- :: - make lms-runserver - make cms-runserver + tutor dev runserver lms # Access the lms at http://localhost:8000 + tutor dev runserver cms # Access the cms at http://localhost:8001 Open a bash shell ----------------- :: - make lms - make cms + tutor dev shell lms + tutor dev shell cms Debug edx-platform ------------------ -If you have one, you can point to a local version of `edx-platform `_ on your host machine:: +If you have one, you can point to a local version of `edx-platform `_ on your host machine. To do so, pass a ``-P/--edx-platform-path`` option to the commands. For instance:: - export EDX_PLATFORM_PATH=/path/to/your/edx-platform + tutor dev shell lms --edx-platform-path=/path/to/edx-platform -Note that you should use an absolute path here, not a relative path (e.g: ``/path/to/edx-platform`` and not ``../edx-platform``). +If you don't want to rewrite this option every time, you can instead define the environment variable:: + + export TUTOR_EDX_PLATFORM_PATH=/path/to/edx-platform All development commands will then automatically mount your local repo. For instance, you can add a ``import pdb; pdb.set_trace()`` breakpoint anywhere in your code and run:: - make lms-runserver + tutor dev runserver lms --edx-platform-path=/path/to/edx-platform With a customised edx-platform repo, you must be careful to have settings that are compatible with the docker environment. You are encouraged to copy the ``tutor.development`` settings files to our own repo:: - cp -r config/openedx/tutor/lms/ /path/to/edx-platform/lms/envs/tutor - cp -r config/openedx/tutor/cms/ /path/to/edx-platform/cms/envs/tutor + cp -r $(tutor config printroot)/env/apps/openedx/tutor/lms/ /path/to/edx-platform/lms/envs/tutor + cp -r $(tutor config printroot)/env/apps/openedx/tutor/cms/ /path/to/edx-platform/cms/envs/tutor -You can then run your platform with the ``tutor.development`` settings. - -**Note**: containers are built on the Hawthorn release. If you are working on a different version of Open edX, you will have to rebuild the images with the right ``EDX_PLATFORM_VERSION`` argument. You may also want to change the ``EDX_PLATFORM_REPOSITORY`` argument to point to your own fork of edx-platform. +You can then run your platform with the ``tutor.development`` settings. See :ref:`the custom settings section ` for settings that are named differently. +**Note:** containers are built on the Hawthorn release. If you are working on a different version of Open edX, you will have to rebuild the ``openedx`` docker images with the version. See the ":ref:`fork edx-platform `. Customised themes ----------------- -With Tutor, it's pretty easy to develop your own themes. Start by placing your files inside the ``build/openedx/themes`` directory. For instance, you could start from the ``edx.org`` theme present inside the ``edx-platform`` repository:: +With Tutor, it's pretty easy to develop your own themes. Start by placing your files inside the ``env/build/openedx/themes`` directory. For instance, you could start from the ``edx.org`` theme present inside the ``edx-platform`` repository:: - cp -r /path/to/edx-platform/themes/edx.org /path/to/tutor/build/openedx/themes/ + cp -r /path/to/edx-platform/themes/edx.org $(tutor config printroot)/env/build/openedx/themes/ -Don't forget to set the ``EDX_PLATFORM_SETTINGS`` environment variable, as explained above. Then, run a local webserver inside the ``deploy/local`` folder:: +Then, run a local webserver:: - make lms-runserver + tutor dev runserver lms The LMS can then be accessed at http://localhost:8000. @@ -67,7 +62,7 @@ You should follow the `Open edX documentation to enable your themes `_. It uses the original code from the various Open edX repositories, such as `edx-platform `_, `cs_comments_service `_, etc. and packages everything in a way that makes it very easy to install, administer and upgrade Open edX. In particular, all services are run inside Docker containers. + +What's the difference with the official "native" install? +--------------------------------------------------------- + +The `native installation `_ maintained by edX relies on `Ansible scripts `_ to deploy Open edX on one or multiple servers. These scripts suffer from a couple issues that Tutor tries to address: + +1. Complexity: the scripts contain close to 35k lines of code spread over 780 files. They are really hard to understand, debug, and modify, and they are extremly slow. As a consequence, Open edX is often wrongly perceived as a project that is overly complex to manage. In contrast, Tutor generates mostly ``Dockerfile`` and ``docker-compose.yml`` files that make it easy to understand what is going on. Also, the whole installation should take about 10 minutes. +2. Isolation from the OS: Tutor barely needs to touch your server because the entire platform is packaged inside Docker containers. You are thus free to run other services on your server without fear of indirectly crashing your Open edX platform. +3. Compatibility: Open edX is only compatible with Ubuntu 16.04, but that shouldn't mean you are forced to run this specific OS. With Tutor, you can deploy Open edX on just any server you like: Ubuntu 18.04, Red Hat, Debian... All docker-compatible platforms are supported. +4. Security: because you are no longer bound to a single OS, with Tutor you are now free to install security-related upgrades as soon as they become available. +5. Portability: Tutor makes it easy to move your platform from one server to another. Just zip-compress your Tutor project root, send it to another server and you're done. + +There are also many features that are not included in the native install, such as a :ref:`web user interface ` for remotely installing the platform, :ref:`Kubernetes deployment `, additional languages, etc. You'll discover these differences as you explore Tutor :) + +What's the difference with the official devstack? +------------------------------------------------- + +The `devstack `_ is meant for development only, not for production deployment. Tutor can be used both for production deployment and :ref:`locally hacking on Open edX `. + +Is Tutor officially supported by edX? +------------------------------------- + +No. Tutor is developed independently from edX. That means that the folks at edX.org are *not* responsible for troubleshooting issues of this project. Please don't bother Ned ;-) + +What features are missing from Tutor? +------------------------------------- + +Those features are currently not available in Tutor: + +- `discovery service `_ +- `ecommerce `_ +- `analytics `_ + +Those extra services were considered low priority while developing this project, but we are planning on adding them to Tutor, eventually. If you need one or more of these services, feel free to let me know by opening a `Github issue `_. In particular, support for the Analytics stack is going to require a lot of work and I am looking forward to financial sponsorship. Please get in touch if you're interested. + +Why should I trust software written by some random guy on the Internet? +----------------------------------------------------------------------- + +You shouldn't :) I'm `Régis Behmo `_ and I have been working on Tutor since early 2018. I have been a contributor of the Open edX project since 2015. In particular, I have worked for 2 years on `FUN-MOOC `_, one of the top 5 largest Open edX platforms in the world. Here are the talks I have presented at the Open edX conferences: + +- *FUN: Life in the Avant-Garde*, Oct. 2015 (`video `__, `slides `__) +- *Open edX 101: A Source Code Review*, June 2016 (`video `__, `slides `__) +- *Videofront: a Self-Hosted YouTube*, June 2017 (`video `__, `slides `__) diff --git a/docs/img/webui.png b/docs/img/webui.png new file mode 100644 index 0000000000000000000000000000000000000000..4f52c600582c617982406b16b4b248f9dc9ac3d0 GIT binary patch literal 204421 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, both for production and local development, inside docker containers. Tutor is easy to run, fast, full of cool features, and it is already used by dozens of Open edX platforms in the world. .. image:: https://asciinema.org/a/octNfEnvIA6jNohCBmODBKizE.png @@ -29,36 +37,39 @@ Tutor 🎓 Open edX 1-click install for everyone .. include:: quickstart.rst :start-line: 1 -For more advanced usage of Tutor, please refer to the following sections. +But there's a lot more to Tutor than that! For more advanced usage, please refer to the following sections. .. toctree:: :maxdepth: 2 :caption: User guide + install quickstart - requirements local - k8s options customise dev + k8s + webui troubleshooting - missing tutor + faq -Disclaimers & Warnings ----------------------- +Source code +----------- -This project is the follow-up of my work on an `install from scratch of Open edX `_. It does not rely on any hack or complex deployment script. In particular, we do not use the Open edX `Ansible deployment playbooks `_. That means that the folks at edX.org are *not* responsible for troubleshooting issues of this project. Please don't bother Ned ;-) - -In case of trouble, please follow the instructions in the :ref:`troubleshooting` section. +The complete source code for Tutor is available on Github: https://github.com/regisb/tutor Contributing ------------ -We go to great lengths to make it as easy as possible for people to run Open edX inside Docker containers. If you have an improvement idea, feel free to `open an issue on Github `_ so that we can discuss it. `Pull requests `_ will be happily examined, too! However, we should be careful to keep the project lean and simple: both to use and to modify. Optional features should not make the user experience more complex. Instead, documentation on how to add the feature is preferred. +We go to great lengths to make it as easy as possible for people to run Open edX inside Docker containers. If you have an improvement idea, feel free to `open an issue on Github `_ so that we can discuss it. `Pull requests `_ will be happily examined, too! License ------- This work is licensed under the terms of the `GNU Affero General Public License (AGPL) `_. + +The AGPL license covers the Tutor code, including the Dockerfiles, but not the content of the Docker images which can be downloaded from https://hub.docker.com. Software other than Tutor provided with the docker images retain their original license. + +The :ref:`Tutor Web UI ` depends on the `Gotty `_ binary, which is provided under the terms of the `MIT license `_. diff --git a/docs/install.rst b/docs/install.rst new file mode 100644 index 0000000..68eb275 --- /dev/null +++ b/docs/install.rst @@ -0,0 +1,41 @@ +.. _install: + +Installation +============ + +Requirements +------------ + +The only prerequisite for running this is a working docker install. Both docker and docker-compose are required. Follow the instructions from the official documentation: + +- `Docker `_ +- `Docker compose `_ + +Note that the production web server container will bind to port 80, so if you already have a web server running (Apache or Nginx, for instance), you should stop it. + +You should be able to run Open edX on any platform that supports Docker, including Mac OS and Windows. Tutor was tested under various versions of Ubuntu and Mac OS. + +At a minimum, the server running the containers should have 4 Gb of RAM. With less memory, the Open edX , the deployment procedure might crash during migrations (see the :ref:`troubleshooting` section) and the platform will be unbearably slow. + +Also, the host running the containers should be a 64 bit platform. (images are not built for i386 systems) + +Direct binary downloads +----------------------- + +The latest binaries can be downloaded from https://github.com/regisb/tutor/releases. + +Installing from pip +------------------- + +If, for some reason, you'd rather install from pypi instead of downloading a binary, run:: + + pip install tutor-openedx + +Installing from source +---------------------- + +:: + + git clone https://github.com/regisb/tutor + cd tutor + python setup.py develop diff --git a/docs/k8s.rst b/docs/k8s.rst index 127e512..fe46bfb 100644 --- a/docs/k8s.rst +++ b/docs/k8s.rst @@ -23,7 +23,7 @@ In the following, we assume you have a working Kubernetes platform. For a start, Start Minikube:: - make minikube-start + minikube start When minikube starts, it spawns a virtual machine (VM) which you can configure in your VM manager: on most platforms, this is Virtualbox. You should configure your VM to have at least 4Gb RAM; otherwise, database migrations will crash halfway, and that's a nasty issue... @@ -44,16 +44,12 @@ With Kubernetes, your Open edX platform will not be available at localhost or st where ``MINIKUBEIP`` should be replaced by the result of the command ``minikube ip``. -In the following, all commands should be run inside the ``deploy/k8s`` folder:: - - cd deploy/k8s - Quickstart ---------- Launch the platform on k8s in 1 click:: - make all + tutor k8s quickstart All Kubernetes resources are associated to the "openedx" namespace. If you don't see anything in the Kubernetes dashboard, you are probably looking at the wrong namespace... 😉 @@ -65,26 +61,27 @@ Upgrading After pulling updates from the Tutor repository, you can apply changes with:: - make upgrade + tutor k8s stop + tutor k8s start Accessing the Kubernetes dashboard ---------------------------------- Depending on your Kubernetes provider, you may need to create a dashboard yourself. To do so, run:: - make k8s-dashboard + kubectl create -f https://raw.githubusercontent.com/kubernetes/dashboard/master/aio/deploy/recommended/kubernetes-dashboard.yaml Then, you will have to create an admin user:: - make k8s-admin + tutor k8s adminuser Print the admin token required for authentication, and copy its value:: - make k8s-admin-token + tutor k8s admintoken Create a proxy to the Kubernetes API server:: - k8s-proxy + kubectl proxy Use the token to log in the dashboard at the following url: http://localhost:8001/api/v1/namespaces/kube-system/services/https:kubernetes-dashboard:/proxy/ diff --git a/docs/local.rst b/docs/local.rst index 04902be..ad529b7 100644 --- a/docs/local.rst +++ b/docs/local.rst @@ -3,44 +3,57 @@ Local deployment ================ -This method is for deploying Open edX locally on a single server. Docker images are orchestrated with `docker-compose `_. +This method is for deploying Open edX locally on a single server, where docker images are orchestrated with `docker-compose `_. -The following commands should be run inside the ``deploy/local`` folder:: +In the following, environment and data files will be generated in a user-specific project folder which will be referred to as the "**project root**". On Linux, the default project root is ``~/.local/share/tutor``. An alternative project root can be defined by passing the ``--root=...`` option to most commands, or define the ``TUTOR_ROOT=...`` environment variable. - cd deploy/local +All-in-one command +------------------ -You can use these commands individually instead of running the full installation with ``make all``. +A fully-functional platform can be configured and run in one command:: + + tutor local quickstart + +But you may want to run commands one at a time: it's faster when you need to run only part of the local deployment process, and it helps you understand how your platform works. In the following we decompose the ``quickstart`` command. Configuration ------------- :: - make configure + tutor config interactive -This is the only non-automatic step in the install process. You will be asked various questions about your Open edX platform and appropriate configuration files will be generated. If you would like to automate this step then you should run ``make configure`` interactively once. After that, you will have a ``config.json`` file at the root of the repository. +This is the only non-automatic step in the install process. You will be asked various questions about your Open edX platform and appropriate configuration files will be generated. If you would like to automate this step then you should run ``tutor config interactive`` once. After that, there will be a ``config.yml`` file at the root of the project folder: this file contains all the configuration values for your platform, such as randomly generated passwords, domain names, etc. -If you want to run a fully automated install, upload the ``config.json`` file to wherever you want to run Open edX, and then run ``make configure SILENT=1`` instead of ``make configure``. All values from ``config.json`` will be automatically loaded. +If you want to run a fully automated install, upload the ``config.yml`` file to wherever you want to run Open edX. You can then entirely skip the configuration step. -Downloading docker images -------------------------- +Environment files generation +---------------------------- :: - make update + tutor local env -You will need to download the docker images from `Docker Hub `_. Depending on your bandwidth, this might take a long time. Minor image updates will be incremental, and thus much faster. +This command generates environment files, such as the ``*.env.json``, ``*.auth.json`` files, the ``docker-compose.yml`` file, etc. They are generated from templates and the configuration values stored in ``config.yml``. The generated files are placed in the ``env/local`` subfolder of the project root. You may modify and delete those files at will, since they can be easily re-generated with the same ``tutor local env`` command. + +Update docker images +-------------------- + +:: + + tutor local pullimages + +This downloads the latest version of the docker images from `Docker Hub `_. Depending on your bandwidth, this might take a long time. Minor image updates will be incremental, and thus much faster. Database management ------------------- :: - make databases + tutor local databases This command should be run just once. It will create the required databases tables and apply database migrations for all applications. - If migrations are stopped with a ``Killed`` message, this certainly means the docker containers don't have enough RAM. See the :ref:`troubleshooting` section. Running Open edX @@ -48,26 +61,26 @@ Running Open edX :: - make run + tutor local start This will launch the various docker containers required for your Open edX platform. The LMS and the Studio will then be reachable at the domain name you specified during the configuration step. You can also access them at http://localhost and http://studio.localhost. To stop the running containers, just hit Ctrl+C. -In production, you will probably want to daemonize the services. Instead of ``make run``, run:: +In production, you will probably want to daemonize the services. To do so, run:: - make daemonize + tutor local start --detach And then, to stop all services:: - make stop + tutor local stop Creating a new user with staff and admin rights ----------------------------------------------- You will most certainly need to create a user to administer the platform. Just run:: - make staff-user USERNAME=yourusername EMAIL=user@email.com + tutor createuser --staff --superuser yourusername user@email.com You will asked to set the user password interactively. @@ -76,30 +89,30 @@ Importing the demo course On a fresh install, your platform will not have a single course. To import the `Open edX demo course `_, run:: - make demo-course + tutor local importdemocourse Updating the course search index -------------------------------- The course search index can be updated with:: - make reindex-courses + tutor local indexcourses Run this command periodically to ensure that course search results are always up-to-date. .. _portainer: -Docker container web UI with `Portainer `_ ------------------------------------------------------------------ +Docker container web UI with `Portainer `__ +------------------------------------------------------------------ Portainer is a web UI for managing docker containers. It lets you view your entire Open edX platform at a glace. Try it! It's really cool:: - make portainer + tutor local portainer .. .. image:: https://portainer.io/images/screenshots/portainer.gif ..:alt: Portainer demo -After launching your platfom, the web UI will be available at `http://localhost:9000 `_. You will be asked to define a password for the admin user. Then, select a "Local environment" to work on and hit "Connect" and select the "local" group to view all running containers. +After launching your platfom, the web UI will be available at `http://localhost:9000 `_. You will be asked to define a password for the admin user. Then, select a "Local environment" to work on; hit "Connect" and select the "local" group to view all running containers. Among many other things, you'll be able to view the logs for each container, which is really useful. @@ -118,14 +131,22 @@ Additional commands All available commands can be listed by running:: - make help + tutor local help -How to upgrade from `openedx-docker` ------------------------------------- +Upgrading from earlier versions +------------------------------- -Before this project was renamed to Tutor, it was called "openedx-docker", and many additional changes were introduced at the same time. If you checked out openedx-docker before the rename, you can upgrade by running the following commands:: +Versions 1 and 2 of Tutor were organized differently: they relied on many different ``Makefile`` and ``make`` commands instead of a single ``tutor`` executable. To migrate from an earlier version, you should first stop your platform:: - make stop # Stop all services - git pull # Upgrade to the latest version of Tutor - make upgrade-to-tutor # Move some configuration files - make local # Re-configure and restart the platform + make stop + +Then, create the Tutor project root and move your data:: + + mkdir -p $(tutor config printroot) + mv config.json data/ $(tutor config printroot) + +`Download `_ the latest stable release of Tutor, uncompress the file and place the ``tutor`` executable in your path. + +Finally, start your platform again:: + + tutor local quickstart diff --git a/docs/missing.rst b/docs/missing.rst deleted file mode 100644 index b2a61aa..0000000 --- a/docs/missing.rst +++ /dev/null @@ -1,12 +0,0 @@ -.. _missing: - -Missing features -================ - -Those features are currently not available in Tutor: - -- `discovery service `_ -- `ecommerce `_ -- `analytics `_ - -Those extra services were considered low priority while developing this project. If you need one or more of these services, feel free to let me know by opening an issue. diff --git a/docs/options.rst b/docs/options.rst index 72d8d84..452d998 100644 --- a/docs/options.rst +++ b/docs/options.rst @@ -18,13 +18,13 @@ The following DNS records must exist and point to your server:: Thus, **this feature will (probably) not work in development** because the DNS records will (probably) not point to your development machine. -To download the certificate manually, run:: +To create the certificate manually, run:: - make https-certificate + tutor local https create To renew the certificate, run this command once per month:: - make https-certificate-renew + tutor local https renew Student notes ------------- @@ -46,19 +46,10 @@ Android app (beta) The Android app for your platform can be easily built in just one command:: - make android + tutor android build debug If all goes well, the debuggable APK for your platform should then be available in ./data/android. To obtain a release APK, you will need to obtain credentials from the app store and add them to ``config/android/gradle.properties``. Then run:: - make android-release + tutor android build release Building the Android app for an Open edX platform is currently labeled as a **beta feature** because it was not fully tested yet. In particular, there is no easy mechanism for overriding the edX assets in the mobile app. This is still a work-in-progress. - -Stats ------ - -By default, the install script will collect some information about your install and send it to a private server. The only transmitted information are the LMS domain name and the ID of the install. To disable stats collection, define the following environment variable:: - - export DISABLE_STATS=1 - -If you decide to disable stats, please send me a message to tell me about your platform! diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 9f6c8ae..86e9320 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -3,18 +3,16 @@ Quickstart ========== -:: - - git clone https://github.com/regisb/tutor - cd tutor/deploy/local - make all +1. `Download `_ the latest stable release of Tutor, uncompress the file and place the ``tutor`` executable in your path. +2. Run ``tutor local quickstart`` +3. You're done! **That's it?** -Yes :) This is what happens when you run ``make all``: +Yes :) This is what happens when you run ``tutor local quickstart``: 1. You answer a few questions about the configuration of your Open edX platform and your :ref:`selected options ` -2. Configuration files are generated. +2. Configuration files are generated from templates. 3. Docker images are downloaded. 4. Docker containers are provisioned. 5. A full, production-ready platform is run with docker-compose. diff --git a/docs/requirements.rst b/docs/requirements.rst deleted file mode 100644 index f0e9f91..0000000 --- a/docs/requirements.rst +++ /dev/null @@ -1,18 +0,0 @@ -.. _requirements: - -Requirements -============ - -The only prerequisite for running this is a working docker install. You will need both docker and docker-compose. Follow the instructions from the official documentation: - -- `make `_ -- `Docker `_ -- `Docker compose `_ - -Note that the production web server container will bind to port 80, so if you already have a web server running (Apache or Nginx, for instance), you should stop it. - -You should be able to run Open edX on any platform that supports Docker, including Mac OS and Windows. Tutor was tested under various versions of Ubuntu and Mac OS. - -At a minimum, the server running the containers should have 4 Gb of RAM; otherwise, the deployment procedure will crash during migrations (see the :ref:`troubleshooting` section). - -Also, the host running the containers should be a 64 bit platform. (images are not built for i386 systems) diff --git a/docs/troubleshooting.rst b/docs/troubleshooting.rst index 7df1d43..9d559c2 100644 --- a/docs/troubleshooting.rst +++ b/docs/troubleshooting.rst @@ -15,21 +15,20 @@ What should you do if you have a problem? Logging ------- -To view the logs from all containers use the `docker-compose logs `_ command:: +To view the logs from all containers use the ``tutor local logs`` command, which was modeled on the standard `docker-compose logs `_ command:: - docker-compose logs -f + tutor local logs --follow To view the logs from just one container, for instance the web server:: - docker-compose logs -f nginx + tutor local logs --follow nginx The last commands produce the logs since the creation of the containers, which can be a lot. Similar to a ``tail -f``, you can run:: - docker-compose logs --tail=0 -f + tutor local logs--tail=0 -f If you'd rather use a graphical user interface for viewing logs, you are encouraged to try out :ref:`Portainer `. - "Cannot start service nginx: driver failed programming external connectivity" ----------------------------------------------------------------------------- @@ -42,15 +41,14 @@ The containerized Nginx needs to listen to ports 80 and 443 on the host. If ther However, you might not want to do that if you need a webserver for running non-Open edX related applications. In such cases... -2. Run the nginx container on different ports: you can create a ``.env`` file in the ``tutor`` directory in which you indicate different ports. For instance:: +2. Run the nginx container on different ports: to do so, indicate different ports in the ``config.yml`` file. For instance:: - cat .env - NGINX_HTTP_PORT=81 - NGINX_HTTPS_PORT=444 + NGINX_HTTP_PORT: 81 + NGINX_HTTPS_PORT: 444 -In this example, the nginx container ports would be mapped to 81 and 444, instead of 80 and 443. +In this example, the nginx container ports would be mapped to 81 and 444, instead of 80 and 443. Then, re-generate the environment with ``tutor local env`` and restart nginx with ``tutor local restart nginx``. -You should note that with the latter solution, it is your responsibility to configure the webserver on the host as a proxy to the nginx container. See `this `_ for http, and `this `_ for https. +You should note that with the latter solution, it is your responsibility to configure the webserver on the host as a proxy to the nginx container. See `this github issue `_ for http, and `this other github issue `_ for https. Help! The Docker containers are eating all my RAM/CPU/CHEESE ------------------------------------------------------------ @@ -62,7 +60,7 @@ You can identify which containers are consuming most resources by running:: "Running migrations... Killed!" ------------------------------- -The LMS and CMS containers require at least 4 GB RAM, in particular to run the Open edX SQL migrations. On Docker for Mac, by default, containers are allocated at most 2 GB of RAM. On Mac OS, if the ``make all`` command dies after displaying "Running migrations", you most probably need to increase the allocated RAM. Follow `these instructions from the official Docker documentation `_. +Older versions of Open edX required at least 4 GB RAM, in particular to run the Open edX SQL migrations. On Docker for Mac, by default, containers are allocated at most 2 GB of RAM. On Mac OS, if the ``tutor local quickstart`` command dies after displaying "Running migrations", you most probably need to increase the allocated RAM. Follow `these instructions from the official Docker documentation `_. ``Build failed running pavelib.servers.lms: Subprocess return code: 1`` @@ -70,15 +68,15 @@ The LMS and CMS containers require at least 4 GB RAM, in particular to run the O :: - python manage.py lms --settings=development print_setting STATIC_ROOT 2>/dev/null + python manage.py lms print_setting STATIC_ROOT 2>/dev/null ... Build failed running pavelib.servers.lms: Subprocess return code: 1`" -This might occur when you run a ``paver`` command. ``/dev/null`` eats the actual error, so you will have to run the command manually. Run ``make lms`` (or ``make cms``) to open a bash session and then:: +This might occur when you run a ``paver`` command. ``/dev/null`` eats the actual error, so you will have to run the command manually. Run ``tutor dev shell lms`` (or ``tutor dev shell cms``) to open a bash session and then:: - python manage.py lms --settings=development print_setting STATIC_ROOT + python manage.py lms print_setting STATIC_ROOT -Of course, you should replace `development` with your own settings. The error produced should help you better understand what is happening. +The error produced should help you better understand what is happening. ``ValueError: Unable to configure handler 'local'`` --------------------------------------------------- diff --git a/docs/tutor.rst b/docs/tutor.rst index 678f4cd..95f77c4 100644 --- a/docs/tutor.rst +++ b/docs/tutor.rst @@ -3,9 +3,34 @@ Tutor development ================= -Pushing to Docker Hub ---------------------- +Start by cloning the Tutor repository:: -The images are built, tagged and uploaded to Docker Hub in one command:: + git clone https://github.com/regisb/tutor.git + cd tutor/ - make dockerhub +Install requirements +-------------------- + +:: + + pip install -r requirements/dev.txt + +Bundle ``tutor`` executable +--------------------------- + +:: + + make bundle + +Generate the documentation +-------------------------- + +:: + + pip install sphinx sphinx_rtd_theme + cd docs/ + make html + +You can then browse the documentation with:: + + make browse diff --git a/docs/webui.rst b/docs/webui.rst new file mode 100644 index 0000000..3dc5be1 --- /dev/null +++ b/docs/webui.rst @@ -0,0 +1,24 @@ +.. _webui: + +Web UI +====== + +Tutor comes with a web user interface (UI) that allows you to administer your Open edX platform remotely. It's especially convenient for remote administration of the platform. + +Launching the web UI +-------------------- + +:: + + tutor webui start + +You can then access the interface at http://localhost:3737, or http://youserverurl:3737. + +.. image:: img/webui.png + +Authentication +-------------- + +**WARNING** Once you launch the web UI, it is accessible by everyone, which means that your Open edX platform is at risk. If you are planning to leave the web UI up for a long time, you should setup a user and password for authentication:: + + tutor webui configure diff --git a/requirements/base.in b/requirements/base.in new file mode 100644 index 0000000..a8d4136 --- /dev/null +++ b/requirements/base.in @@ -0,0 +1,6 @@ +appdirs +click +click_repl +jinja2 +kubernetes +pyyaml>=4.2b1 diff --git a/requirements/base.txt b/requirements/base.txt new file mode 100644 index 0000000..2c9a7b6 --- /dev/null +++ b/requirements/base.txt @@ -0,0 +1,36 @@ +# +# This file is autogenerated by pip-compile +# To update, run: +# +# pip-compile --output-file requirements/base.txt requirements/base.in +# +adal==1.2.1 # via kubernetes +appdirs==1.4.3 +asn1crypto==0.24.0 # via cryptography +cachetools==3.1.0 # via google-auth +certifi==2018.11.29 # via kubernetes, requests +cffi==1.11.5 # via cryptography +chardet==3.0.4 # via requests +click-repl==0.1.6 +click==7.0 +cryptography==2.5 # via adal +google-auth==1.6.2 # via kubernetes +idna==2.8 # via requests +jinja2==2.10 +kubernetes==8.0.1 +markupsafe==1.1.0 # via jinja2 +oauthlib==3.0.1 # via requests-oauthlib +prompt-toolkit==2.0.8 # via click-repl +pyasn1-modules==0.2.4 # via google-auth +pyasn1==0.4.5 # via pyasn1-modules, rsa +pycparser==2.19 # via cffi +pyjwt==1.7.1 # via adal +python-dateutil==2.8.0 # via adal, kubernetes +pyyaml==4.2b4 +requests-oauthlib==1.2.0 # via kubernetes +requests==2.21.0 # via adal, kubernetes, requests-oauthlib +rsa==4.0 # via google-auth +six==1.12.0 # via click-repl, cryptography, google-auth, kubernetes, prompt-toolkit, python-dateutil, websocket-client +urllib3==1.24.1 # via kubernetes, requests +wcwidth==0.1.7 # via prompt-toolkit +websocket-client==0.54.0 # via kubernetes diff --git a/requirements/dev.in b/requirements/dev.in new file mode 100644 index 0000000..c2e0f6f --- /dev/null +++ b/requirements/dev.in @@ -0,0 +1,3 @@ +-r base.txt +pip-tools +pyinstaller diff --git a/requirements/dev.txt b/requirements/dev.txt new file mode 100644 index 0000000..3a323fb --- /dev/null +++ b/requirements/dev.txt @@ -0,0 +1,42 @@ +# +# This file is autogenerated by pip-compile +# To update, run: +# +# pip-compile --output-file requirements/dev.txt requirements/dev.in +# +adal==1.2.1 +altgraph==0.16.1 # via macholib, pyinstaller +appdirs==1.4.3 +asn1crypto==0.24.0 +cachetools==3.1.0 +certifi==2018.11.29 +cffi==1.11.5 +chardet==3.0.4 +click-repl==0.1.6 +click==7.0 +cryptography==2.5 +future==0.17.1 # via pefile +google-auth==1.6.2 +idna==2.8 +jinja2==2.10 +kubernetes==8.0.1 +macholib==1.11 # via pyinstaller +markupsafe==1.1.0 +oauthlib==3.0.1 +pefile==2018.8.8 # via pyinstaller +pip-tools==3.2.0 +prompt-toolkit==2.0.8 +pyasn1-modules==0.2.4 +pyasn1==0.4.5 +pycparser==2.19 +pyinstaller==3.4 +pyjwt==1.7.1 +python-dateutil==2.8.0 +pyyaml==4.2b4 +requests-oauthlib==1.2.0 +requests==2.21.0 +rsa==4.0 +six==1.12.0 +urllib3==1.24.1 +wcwidth==0.1.7 +websocket-client==0.54.0 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..ce505d7 --- /dev/null +++ b/setup.py @@ -0,0 +1,49 @@ +import io +import os +from setuptools import setup + +here = os.path.abspath(os.path.dirname(__file__)) + +with io.open(os.path.join(here, "README.rst"), "rt", encoding="utf8") as f: + readme = f.read() + +setup( + name="tutor-openedx", + version="3.0.0", + url="http://docs.tutor.overhang.io/", + project_urls={ + "Documentation": "https://docs.tutor.overhang.io/", + "Code": "https://github.com/regisb/tutor", + "Issue tracker": "https://github.com/regisb/tutor/issues", + }, + license="AGPLv3", + author="Régis Behmo", + author_email="regis@behmo.com", + description="The Open edX distribution for the busy system administrator", + long_description=readme, + packages=["tutor"], + include_package_data=True, + python_requires=">=3.6", + install_requires=[ + "appdirs", + "click", + "click_repl", + "jinja2", + "kubernetes", + "pyyaml" + ], + entry_points={ + 'console_scripts': [ + 'tutor=tutor.cli:main', + ], + }, + classifiers=[ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: GNU Affero General Public License v3", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + ], +) diff --git a/tutor.spec b/tutor.spec new file mode 100644 index 0000000..a63fdb3 --- /dev/null +++ b/tutor.spec @@ -0,0 +1,32 @@ +# -*- mode: python -*- + +block_cipher = None + + +a = Analysis(['bin/main'], + pathex=['/home/data/regis/projets/openedx/repos/tutor'], + binaries=[], + datas=[('./tutor/templates', './tutor/templates')], + hiddenimports=[], + hookspath=[], + runtime_hooks=[], + excludes=[], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher, + noarchive=False) +pyz = PYZ(a.pure, a.zipped_data, + cipher=block_cipher) +exe = EXE(pyz, + a.scripts, + a.binaries, + a.zipfiles, + a.datas, + [], + name='tutor', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + runtime_tmpdir=None, + console=True ) diff --git a/build/openedx/settings/cms/__init__.py b/tutor/__init__.py similarity index 100% rename from build/openedx/settings/cms/__init__.py rename to tutor/__init__.py diff --git a/tutor/android.py b/tutor/android.py new file mode 100644 index 0000000..93194ef --- /dev/null +++ b/tutor/android.py @@ -0,0 +1,69 @@ +import click + +from . import config +from . import env as tutor_env +from . import fmt +from . import opts +from . import utils + + +DOCKER_IMAGE = "regis/openedx-android:hawthorn" + +@click.group( + help="Build an Android app for your Open edX platform [BETA FEATURE]" +) +def android(): + pass + +@click.command( + help="Generate the environment required for building the application" +) +@opts.root +def env(root): + tutor_env.render_target(root, config.load(root), "android") + +@click.group( + help="Build the application" +) +def build(): + pass + +@click.command( + help="Build the application in debug mode" +) +@opts.root +def debug(root): + docker_run( + root, "./gradlew", "assembleProdDebuggable", "&&", + "cp", "OpenEdXMobile/build/outputs/apk/prod/debuggable/*.apk", "/openedx/data/" + ) + click.echo(fmt.info("The debuggable APK file is available in {}".format(tutor_env.data_path(root, "android")))) + +@click.command( + help="Build the application in release mode" +) +@opts.root +def release(root): + docker_run(root, "./gradlew", "assembleProdRelease") + click.echo(fmt.info("The production APK file is available in {}".format(tutor_env.data_path(root, "android")))) + +@click.command( + help="Pull the docker image" +) +@opts.root +def pullimage(): + utils.execute("docker", "pull", DOCKER_IMAGE) + +def docker_run(root, *command): + utils.docker_run( + "--volume={}/:/openedx/config/".format(tutor_env.pathjoin(root, "android")), + "--volume={}:/openedx/data".format(tutor_env.data_path(root, "android")), + DOCKER_IMAGE, + *command + ) + +build.add_command(debug) +build.add_command(release) +android.add_command(build) +android.add_command(env) +android.add_command(pullimage) diff --git a/tutor/cli.py b/tutor/cli.py new file mode 100755 index 0000000..7715c8b --- /dev/null +++ b/tutor/cli.py @@ -0,0 +1,51 @@ +#! /usr/bin/env python3 +import sys + +import click +import click_repl + +from .android import android +from .config import config +from .dev import dev +from .images import images +from .k8s import k8s +from .local import local +from .ui import ui +from .webui import webui +from . import exceptions +from . import fmt + + +def main(): + try: + cli() + except exceptions.TutorError as e: + sys.stderr.write(fmt.error("Error: {}\n".format(e.args[0]))) + sys.exit(1) + +@click.group(context_settings={'help_option_names': ['-h', '--help', 'help']}) +@click.version_option() +def cli(): + pass + +@click.command( + help="Print this help", + name="help", +) +def print_help(): + with click.Context(cli) as context: + click.echo(cli.get_help(context)) + +click_repl.register_repl(cli, name="ui") +cli.add_command(images) +cli.add_command(config) +cli.add_command(local) +cli.add_command(dev) +cli.add_command(android) +cli.add_command(k8s) +cli.add_command(ui) +cli.add_command(webui) +cli.add_command(print_help) + +if __name__ == "__main__": + main() diff --git a/tutor/config.py b/tutor/config.py new file mode 100644 index 0000000..f8627f5 --- /dev/null +++ b/tutor/config.py @@ -0,0 +1,178 @@ +import json +import os +import yaml + +import click + +from . import exceptions +from . import env +from . import fmt +from . import opts + + +@click.group( + short_help="Configure Open edX", + help="""Configure Open edX and store configuration values in $TUTOR_ROOT/config.yml""" +) +@opts.root +def config(root): + pass + +@click.command( + help="Create and save configuration interactively", +) +@opts.root +@opts.key_value +def interactive(root, s): + config = {} + load_files(config, root) + for k, v in s: + config[k] = v + load_interactive(config) + save(config, root) + +@click.command( + help="Create and save configuration without user interaction", +) +@opts.root +@opts.key_value +def noninteractive(root, s): + config = {} + load_files(config, root) + for k, v in s: + config[k] = v + save(config, root) + +@click.command( + help="Print the tutor project root", +) +@opts.root +def printroot(root): + click.echo(root) + +def load(root): + """ + Load configuration, and generate it interactively if the file does not + exist. + """ + config = {} + load_files(config, root) + + if not os.path.exists(config_path(root)): + load_interactive(config) + save(config, root) + + load_defaults(config) + + return config + +def load_files(config, root): + convert_json2yml(root) + + # Load base values + base = yaml.load(env.read("config.yml")) + for k, v in base.items(): + config[k] = v + + # Load user file + path = config_path(root) + if os.path.exists(path): + with open(path) as fi: + loaded = yaml.load(fi.read()) + for key, value in loaded.items(): + config[key] = value + +def load_interactive(config): + ask("Your website domain name for students (LMS)", "LMS_HOST", config) + ask("Your website domain name for teachers (CMS)", "CMS_HOST", config) + ask("Your platform name/title", "PLATFORM_NAME", config) + ask("Your public contact email address", "CONTACT_EMAIL", config) + ask_choice( + "The default language code for the platform", + "LANGUAGE_CODE", config, + ['en', 'am', 'ar', 'az', 'bg-bg', 'bn-bd', 'bn-in', 'bs', 'ca', + 'ca@valencia', 'cs', 'cy', 'da', 'de-de', 'el', 'en-uk', 'en@lolcat', + 'en@pirate', 'es-419', 'es-ar', 'es-ec', 'es-es', 'es-mx', 'es-pe', + 'et-ee', 'eu-es', 'fa', 'fa-ir', 'fi-fi', 'fil', 'fr', 'gl', 'gu', + 'he', 'hi', 'hr', 'hu', 'hy-am', 'id', 'it-it', 'ja-jp', 'kk-kz', + 'km-kh', 'kn', 'ko-kr', 'lt-lt', 'ml', 'mn', 'mr', 'ms', 'nb', 'ne', + 'nl-nl', 'or', 'pl', 'pt-br', 'pt-pt', 'ro', 'ru', 'si', 'sk', 'sl', + 'sq', 'sr', 'sv', 'sw', 'ta', 'te', 'th', 'tr-tr', 'uk', 'ur', 'vi', + 'uz', 'zh-cn', 'zh-hk', 'zh-tw'], + ) + ask_bool( + ("Activate SSL/TLS certificates for HTTPS access? Important note:" + "this will NOT work in a development environment."), + "ACTIVATE_HTTPS", config + ) + ask_bool( + "Activate Student Notes service (https://open.edx.org/features/student-notes)?", + "ACTIVATE_NOTES", config + ) + ask_bool( + "Activate Xqueue for external grader services (https://github.com/edx/xqueue)?", + "ACTIVATE_XQUEUE", config + ) + +def load_defaults(config): + defaults = yaml.load(env.read("config-defaults.yml")) + for k, v in defaults.items(): + if k not in config: + config[k] = v + +def ask(question, key, config): + default = env.render_str(config[key], config) + config[key] = click.prompt( + fmt.question(question), + prompt_suffix=" ", default=default, show_default=True, + ) + +def ask_bool(question, key, config): + default = "y" if config[key] else "n" + suffix = " [Yn]" if config[key] else " [yN]" + answer = click.prompt( + fmt.question(question) + suffix, + type=click.Choice(["y", "n"]), + prompt_suffix=" ", default=default, show_default=False, show_choices=False, + ) + config[key] = answer == "y" + +def ask_choice(question, key, config, choices): + default = config[key] + answer = click.prompt( + fmt.question(question), + type=click.Choice(choices), + prompt_suffix=" ", default=default, show_choices=False, + ) + config[key] = answer + +def convert_json2yml(root): + json_path = os.path.join(root, "config.json") + if not os.path.exists(json_path): + return + if os.path.exists(config_path(root)): + raise exceptions.TutorError( + "Both config.json and config.yml exist in {}: only one of these files must exist to continue".format(root) + ) + with open(json_path) as fi: + config = json.load(fi) + save(config, root) + os.remove(json_path) + click.echo(fmt.info("File config.json detected in {} and converted to config.yml".format(root))) + +def save(config, root): + env.render_dict(config) + path = config_path(root) + directory = os.path.dirname(path) + if not os.path.exists(directory): + os.makedirs(directory) + with open(path, "w") as of: + yaml.dump(config, of, default_flow_style=False) + click.echo(fmt.info("Configuration saved to {}".format(path))) + +def config_path(root): + return os.path.join(root, "config.yml") + +config.add_command(interactive) +config.add_command(noninteractive) +config.add_command(printroot) diff --git a/tutor/dev.py b/tutor/dev.py new file mode 100644 index 0000000..d144c49 --- /dev/null +++ b/tutor/dev.py @@ -0,0 +1,101 @@ +import click + +from . import env as tutor_env +from . import opts +from . import utils + + +@click.group( + help="Run Open edX platform with development settings", +) +def dev(): + pass + +@click.command( + help="Run a command in one of the containers", + context_settings={"ignore_unknown_options": True}, +) +@opts.root +@opts.edx_platform_path +@opts.edx_platform_settings +@click.argument("service") +@click.argument("command", default=None, required=False) +@click.argument("args", nargs=-1, required=False) +def run(root, edx_platform_path, edx_platform_settings, service, command, args): + run_command = [service] + if command: + run_command.append(command) + if args: + run_command += args + port = service_port(service) + docker_compose_run_with_port( + root, edx_platform_path, edx_platform_settings, port, *run_command + ) + +@click.command( + help="Run a development server", +) +@opts.root +@opts.edx_platform_path +@opts.edx_platform_settings +@click.argument("service", type=click.Choice(["lms", "cms"])) +def runserver(root, edx_platform_path, edx_platform_settings, service): + port = service_port(service) + docker_compose_run_with_port( + root, edx_platform_path, edx_platform_settings, port, + service, "./manage.py", "runserver", "0.0.0.0:{}".format(port), + ) + +@click.command( + help="Launch a shell", +) +@opts.root +@opts.edx_platform_path +@opts.edx_platform_settings +@click.argument("service", type=click.Choice(["lms", "cms"])) +def shell(root, edx_platform_path, edx_platform_settings, service): + port = service_port(service) + docker_compose_run_with_port( + root, edx_platform_path, edx_platform_settings, port, + service, "bash" + ) + +@click.command( + help="Watch for changes in your themes and recompile assets when needed" +) +@opts.root +@opts.edx_platform_path +@opts.edx_platform_settings +def watchthemes(root, edx_platform_path, edx_platform_settings): + docker_compose_run( + root, edx_platform_path, edx_platform_settings, + "--no-deps", "lms", "openedx-assets", "watch-themes", "--env", "dev" + ) + +def docker_compose_run_with_port(root, edx_platform_path, edx_platform_settings, port, *command): + docker_compose_run( + root, edx_platform_path, edx_platform_settings, + "-p", "{port}:{port}".format(port=port), *command + ) + +def docker_compose_run(root, edx_platform_path, edx_platform_settings, *command): + run_command = [ + "run", "--rm", + "-e", "SETTINGS={}".format(edx_platform_settings), + ] + if edx_platform_path: + run_command.append("--volume={}:/openedx/edx-platform".format(edx_platform_path)) + run_command += command + return utils.docker_compose( + "-f", tutor_env.pathjoin(root, "local", "docker-compose.yml"), + "--project-name", "tutor_dev", + *run_command + ) + +def service_port(service): + return 8000 if service == "lms" else 8001 + +dev.add_command(run) +dev.add_command(runserver) +dev.add_command(shell) +dev.add_command(watchthemes) diff --git a/tutor/env.py b/tutor/env.py new file mode 100644 index 0000000..d56bfda --- /dev/null +++ b/tutor/env.py @@ -0,0 +1,107 @@ +import codecs +import os +import shutil + +import jinja2 + +from . import exceptions +from . import utils + + +TEMPLATES_ROOT = os.path.join(os.path.dirname(__file__), "templates") + +def render_target(root, config, target): + """ + Render the templates located in `target` and store them with the same + hierarchy at `root`. + """ + for src, dst in walk_templates(root, target): + if is_part_of_env(src): + with codecs.open(src, encoding='utf-8') as fi: + substituted = render_str(fi.read(), config) + with open(dst, "w") as of: + of.write(substituted) + +def render_dict(config): + """ + Render the values from the dict. This is useful for rendering the default + values from config.yml. + + Args: + config (dict) + """ + rendered = {} + for key, value in config.items(): + if isinstance(value, str): + rendered[key] = render_str(value, config) + else: + rendered[key] = value + for k, v in rendered.items(): + config[k] = v + pass + +def render_str(text, config): + """ + Args: + text (str) + config (dict) + + Return: + substituted (str) + """ + template = jinja2.Template(text, undefined=jinja2.StrictUndefined) + try: + return template.render( + RAND8=utils.random_string(8), + RAND24=utils.random_string(24), + **config + ) + except jinja2.exceptions.UndefinedError as e: + raise exceptions.TutorError("Missing configuration value: {}".format(e.args[0])) + +def copy_target(root, target): + """ + Copy the templates located in `path` and store them with the same hierarchy + at `root`. + """ + for src, dst in walk_templates(root, target): + if is_part_of_env(src): + shutil.copy(src, dst) + +def is_part_of_env(path): + return not os.path.basename(path).startswith(".") + +def read(*path): + """ + Read template content located at `path`. + """ + src = os.path.join(TEMPLATES_ROOT, *path) + with codecs.open(src, encoding='utf-8') as fi: + return fi.read() + +def walk_templates(root, target): + """ + Iterate on the template files from `templates/target`. + + Yield: + src: template path + dst: destination path inside root + """ + target_root = os.path.join(TEMPLATES_ROOT, target) + for dirpath, _, filenames in os.walk(os.path.join(TEMPLATES_ROOT, target)): + dst_dir = pathjoin( + root, target, + os.path.relpath(dirpath, target_root) + ) + if not os.path.exists(dst_dir): + os.makedirs(dst_dir) + for filename in filenames: + src = os.path.join(dirpath, filename) + dst = os.path.join(dst_dir, filename) + yield src, dst + +def data_path(root, *path): + return os.path.join(os.path.abspath(root), "data", *path) + +def pathjoin(root, target, *path): + return os.path.join(root, "env", target, *path) diff --git a/tutor/exceptions.py b/tutor/exceptions.py new file mode 100644 index 0000000..3caadc0 --- /dev/null +++ b/tutor/exceptions.py @@ -0,0 +1,2 @@ +class TutorError(Exception): + pass diff --git a/tutor/fmt.py b/tutor/fmt.py new file mode 100644 index 0000000..e5daad9 --- /dev/null +++ b/tutor/fmt.py @@ -0,0 +1,26 @@ +import click + +def title(text): + indent = 8 + separator = "=" * (len(text) + 2 * indent) + message = "{separator}\n{indent}{text}\n{separator}".format( + separator=separator, + indent=" " * indent, + text=text, + ) + return click.style(message, fg="green") + +def info(text): + return click.style(text, fg="blue") + +def error(text): + return click.style(text, fg="red") + +def command(text): + return click.style(text, fg="magenta") + +def question(text): + return click.style(text, fg="yellow") + +def alert(text): + return click.style("⚠️ " + text, fg="yellow", bold=True) diff --git a/tutor/images.py b/tutor/images.py new file mode 100644 index 0000000..0c4f913 --- /dev/null +++ b/tutor/images.py @@ -0,0 +1,93 @@ +import click + +from . import env as tutor_env +from . import fmt +from . import opts +from . import utils + +@click.group(short_help="Manage docker images") +def images(): + pass + +option_namespace = click.option("-n", "--namespace", default="regis", show_default=True) +option_version = click.option("-V", "--version", default="hawthorn", show_default=True) +all_images = ["openedx", "forum", "notes", "xqueue", "android"] +argument_image = click.argument( + "image", type=click.Choice(["all"] + all_images), +) + +@click.command( + short_help="Generate environment", + help="""Generate the environment files required to build and customise the docker images.""" +) +@opts.root +def env(root): + tutor_env.copy_target(root, "build") + click.echo(fmt.info("Environment generated in {}".format(root))) + +@click.command( + short_help="Download docker images", + help=("""Download the docker images from hub.docker.com. + The images will come from {namespace}/{image}:{version}.""") +) +@option_namespace +@option_version +@argument_image +def download(namespace, version, image): + for image in image_list(image): + utils.docker('image', 'pull', get_tag(namespace, image, version)) + +@click.command( + short_help="Build docker images", + help=("""Build the docker images necessary for an Open edX platform. + The images will be tagged as {namespace}/{image}:{version}.""")) +@opts.root +@option_namespace +@option_version +@argument_image +@click.option( + "-a", "--build-arg", multiple=True, + help="Set build-time docker ARGS in the form 'myarg=value'. This option may be specified multiple times." +) +def build(root, namespace, version, image, build_arg): + for image in image_list(image): + tag = get_tag(namespace, image, version) + click.echo(fmt.info("Building image {}".format(tag))) + command = [ + "build", "-t", tag, + tutor_env.pathjoin(root, "build", image) + ] + for arg in build_arg: + command += [ + "--build-arg", arg + ] + utils.docker(*command) + +@click.command( + short_help="Push images to hub.docker.com", +) +@option_namespace +@option_version +@argument_image +def push(namespace, version, image): + for image in image_list(image): + tag = get_tag(namespace, image, version) + click.echo(fmt.info("Pushing image {}".format(tag))) + utils.execute("docker", "push", tag) + +def get_tag(namespace, image, version): + name = "openedx" if image == "openedx" else "openedx-{}".format(image) + return "{namespace}{sep}{image}:{version}".format( + namespace=namespace, + sep="/" if namespace else "", + image=name, + version=version, + ) + +def image_list(image): + return all_images if image == "all" else [image] + +images.add_command(env) +images.add_command(download) +images.add_command(build) +images.add_command(push) diff --git a/tutor/k8s.py b/tutor/k8s.py new file mode 100644 index 0000000..90ba057 --- /dev/null +++ b/tutor/k8s.py @@ -0,0 +1,186 @@ +import click +import kubernetes + +from . import config as tutor_config +from . import env as tutor_env +from . import exceptions +from . import fmt +from . import opts +from . import ops +from . import utils + + +@click.group(help="Run Open edX on Kubernetes [BETA FEATURE]") +def k8s(): + pass + +@click.command( + help="Configure and run Open edX from scratch" +) +@opts.root +def quickstart(root): + click.echo(fmt.title("Interactive platform configuration")) + tutor_config.interactive.callback(root, []) + click.echo(fmt.title("Environment generation")) + env.callback(root) + click.echo(fmt.title("Stopping any existing platform")) + stop.callback(root) + click.echo(fmt.title("Starting the platform")) + start.callback(root) + +@click.command( + short_help="Generate environment", + help="Generate the environment files required to run Open edX", +) +@opts.root +def env(root): + config = tutor_config.load(root) + tutor_env.render_target(root, config, "apps") + tutor_env.render_target(root, config, "k8s") + click.echo(fmt.info("Environment generated in {}".format(root))) + +@click.command(help="Run all configured Open edX services") +@opts.root +def start(root): + kubectl_no_fail("create", "-f", tutor_env.pathjoin(root, "k8s", "namespace.yml")) + + kubectl("create", "configmap", "nginx-config", "--from-file", tutor_env.pathjoin(root, "apps", "nginx")) + kubectl("create", "configmap", "mysql-config", "--from-env-file", tutor_env.pathjoin(root, "apps", "mysql", "auth.env")) + kubectl("create", "configmap", "openedx-settings-lms", "--from-file", tutor_env.pathjoin(root, "apps", "openedx", "settings", "lms")) + kubectl("create", "configmap", "openedx-settings-cms", "--from-file", tutor_env.pathjoin(root, "apps", "openedx", "settings", "cms")) + kubectl("create", "configmap", "openedx-config", "--from-file", tutor_env.pathjoin(root, "apps", "openedx", "config")) + + kubectl("create", "-f", tutor_env.pathjoin(root, "k8s", "volumes.yml")) + kubectl("create", "-f", tutor_env.pathjoin(root, "k8s", "ingress.yml")) + kubectl("create", "-f", tutor_env.pathjoin(root, "k8s", "services.yml")) + kubectl("create", "-f", tutor_env.pathjoin(root, "k8s", "deployments.yml")) + +@click.command(help="Stop a running platform") +def stop(): + kubectl("delete", "deployments,services,ingress,configmaps", "--all") + +@click.command(help="Completely delete an existing platform") +@click.option("-y", "--yes", is_flag=True, help="Do not ask for confirmation") +def delete(yes): + if not yes: + click.confirm('Are you sure you want to delete the platform? All data will be removed.', abort=True) + kubectl("delete", "namespace", K8s.NAMESPACE) + +@click.command( + help="Create databases and run database migrations", +) +@opts.root +def databases(root): + ops.migrate(root, run_bash) + +@click.command(help="Create an Open edX user and interactively set their password") +@opts.root +@click.option("--superuser", is_flag=True, help="Make superuser") +@click.option("--staff", is_flag=True, help="Make staff user") +@click.argument("name") +@click.argument("email") +def createuser(root, superuser, staff, name, email): + ops.create_user(root, run_bash, superuser, staff, name, email) + +@click.command(help="Import the demo course") +@opts.root +def importdemocourse(root): + ops.import_demo_course(root, run_bash) + +@click.command(help="Re-index courses for better searching") +@opts.root +def indexcourses(root): + # Note: this is currently broken with "pymongo.errors.ConnectionFailure: [Errno 111] Connection refused" + # I'm not quite sure the settings are correctly picked up. Which is weird because migrations work very well. + ops.index_courses(root, run_bash) + +@click.command( + help="Launch a shell in LMS or CMS", +) +@opts.root +@click.argument("service", type=click.Choice(["lms", "cms"])) +def shell(root, service): + K8s().execute(service, "bash") + +@click.command(help="Create a Kubernetesadmin user") +@opts.root +def adminuser(root): + utils.kubectl("create", "-f", tutor_env.pathjoin(root, "k8s", "adminuser.yml")) + +@click.command(help="Print the Kubernetes admin user token") +@opts.root +def admintoken(root): + click.echo(K8s().admin_token()) + +def kubectl(*command): + """ + Run kubectl commands in the right namespace. Also, errors are completely + ignored, to avoid stopping on "AlreadyExists" errors. + """ + args = list(command) + args += [ + "--namespace", K8s.NAMESPACE + ] + kubectl_no_fail(*args) + +def kubectl_no_fail(*command): + """ + Run kubectl commands and ignore exceptions, to avoid stopping on + "AlreadyExists" errors. + """ + try: + utils.kubectl(*command) + except exceptions.TutorError: + pass + + +class K8s: + CLIENT = None + NAMESPACE = "openedx" + + def __init__(self): + pass + + @property + def client(self): + if self.CLIENT is None: + kubernetes.config.load_kube_config() + self.CLIENT = kubernetes.client.CoreV1Api() + return self.CLIENT + + def pod_name(self, app): + selector = "app=" + app + try: + return self.client.list_namespaced_pod("openedx", label_selector=selector).items[0].metadata.name + except IndexError: + raise exceptions.TutorError("Pod with app {} does not exist. Make sure that the pod is running.") + + def admin_token(self): + # Note: this is a HORRIBLE way of looking for a secret + try: + secret = [ + s for s in self.client.list_namespaced_secret("kube-system").items if s.metadata.name.startswith("admin-user-token") + ][0] + except IndexError: + raise exceptions.TutorError("Secret 'admin-user-token'. Make sure that admin user was created.") + return self.client.read_namespaced_secret(secret.metadata.name, "kube-system").data["token"] + + def execute(self, app, *command): + podname = self.pod_name(app) + kubectl_no_fail("exec", "--namespace", self.NAMESPACE, "-it", podname, "--", *command) + +def run_bash(root, service, command): + K8s().execute(service, "bash", "-e", "-c", command) + +k8s.add_command(quickstart) +k8s.add_command(env) +k8s.add_command(start) +k8s.add_command(stop) +k8s.add_command(delete) +k8s.add_command(databases) +k8s.add_command(createuser) +k8s.add_command(importdemocourse) +k8s.add_command(indexcourses) +k8s.add_command(shell) +k8s.add_command(adminuser) +k8s.add_command(admintoken) diff --git a/tutor/local.py b/tutor/local.py new file mode 100644 index 0000000..b294216 --- /dev/null +++ b/tutor/local.py @@ -0,0 +1,261 @@ +import os +from time import sleep + +import click + +from . import config as tutor_config +from . import fmt +from . import opts +from . import scripts +from . import utils +from . import env as tutor_env +from . import ops + + +@click.group( + short_help="Run Open edX locally", + help="Run Open edX platform locally, with docker-compose.", +) +def local(): + pass + +@click.command( + help="Configure and run Open edX from scratch" +) +@opts.root +def quickstart(root): + click.echo(fmt.title("Interactive platform configuration")) + tutor_config.interactive.callback(root, []) + click.echo(fmt.title("Environment generation")) + env.callback(root) + click.echo(fmt.title("Stopping any existing platform")) + stop.callback(root) + click.echo(fmt.title("Docker image updates")) + pullimages.callback(root) + click.echo(fmt.title("Database creation and migrations")) + databases.callback(root) + click.echo(fmt.title("HTTPS certificates generation")) + https_create.callback(root) + click.echo(fmt.title("Starting the platform in detached mode")) + start.callback(root, True) + +@click.command( + short_help="Generate environment", + help="Generate the environment files required to run Open edX", +) +@opts.root +def env(root): + config = tutor_config.load(root) + tutor_env.render_target(root, config, "apps") + tutor_env.render_target(root, config, "local") + click.echo(fmt.info("Environment generated in {}".format(root))) + +@click.command( + help="Update docker images", +) +@opts.root +def pullimages(root): + docker_compose(root, "pull") + +@click.command( + help="Run all configured Open edX services", +) +@opts.root +@click.option("-d", "--detach", is_flag=True, help="Start in daemon mode") +def start(root, detach): + command = ["up"] + if detach: + command.append("-d") + docker_compose(root, *command) + + if detach: + click.echo(fmt.info("The Open edX platform is now running in detached mode")) + config = tutor_config.load(root) + http = "https" if config["ACTIVATE_HTTPS"] else "http" + urls = [] + if not config["ACTIVATE_HTTPS"]: + urls += [ + "http://localhost", + "http://studio.localhost", + ] + urls.append("{http}://{lms_host}".format(http=http, lms_host=config["LMS_HOST"])) + urls.append("{http}://{cms_host}".format(http=http, cms_host=config["CMS_HOST"])) + click.echo(fmt.info("""Your Open edX platform is ready and can be accessed at the following urls: + + {}""".format("\n ".join(urls)))) + + +@click.command(help="Stop a running platform",) +@opts.root +def stop(root): + docker_compose(root, "rm", "--stop", "--force") + +@click.command( + help="""Restart some components from a running platform. +You may specify 'openedx' to restart the lms, cms and workers, or 'all' to +restart all services.""", +) +@opts.root +@click.argument('service') +def restart(root, service): + command = ["restart"] + if service == "openedx": + command += ["lms", "cms", "lms_worker", "cms_worker"] + elif service != "all": + command += [service] + docker_compose(root, *command) + +@click.command( + help="Run a command in one of the containers", + context_settings={"ignore_unknown_options": True}, +) +@opts.root +@click.argument("service") +@click.argument("command", default=None, required=False) +@click.argument("args", nargs=-1, required=False) +def run(root, service, command, args): + run_command = [ + "run", + "--rm", + service + ] + if command: + run_command.append(command) + if args: + run_command += args + docker_compose(root, *run_command) + +@click.command( + help="Create databases and run database migrations", +) +@opts.root +def databases(root): + mysql_data_path = tutor_env.data_path(root, "mysql", "mysql") + if not os.path.exists(mysql_data_path): + click.echo(fmt.info("Initializing MySQL database...")) + docker_compose(root, "up", "-d", "mysql") + while not os.path.exists(mysql_data_path): + click.echo(fmt.info(" waiting for creation of {}".format(mysql_data_path))) + sleep(4) + click.echo(fmt.info("MySQL database initialized")) + docker_compose(root, "stop", "mysql") + ops.migrate(root, run_bash) + +@click.group(help="Manage https certificates") +def https(): + pass + +@click.command(help="Create https certificates", name="create") +@opts.root +def https_create(root): + """ + Note: there are a couple issues with https certificate generation. + 1. Certificates are generated and renewed by using port 80, which is not necessarily open. + a. It may be occupied by the nginx container + b. It may be occupied by an external web server + 2. On certificate renewal, nginx is not reloaded + """ + config = tutor_config.load(root) + if not config['ACTIVATE_HTTPS']: + click.echo(fmt.info("HTTPS is not activated: certificate generation skipped")) + return + + utils.docker_run( + "--volume", "{}:/etc/letsencrypt/".format(tutor_env.data_path(root, "letsencrypt")), + "-p", "80:80", + "--entrypoint=sh", + "certbot/certbot:latest", + "-c", tutor_env.render_str(scripts.https_certificates_create, config), + ) + +@click.command(help="Renew https certificates", name="renew") +@opts.root +def https_renew(root): + config = tutor_config.load(root) + if not config['ACTIVATE_HTTPS']: + click.echo(fmt.info("HTTPS is not activated: certificate renewal skipped")) + return + docker_run = [ + "--volume", "{}:/etc/letsencrypt/".format(tutor_env.data_path(root, "letsencrypt")), + "-p", "80:80", + "certbot/certbot:latest", "renew" + ] + utils.docker_run(*docker_run) + +@click.command(help="View output from containers") +@opts.root +@click.option("-f", "--follow", is_flag=True, help="Follow log output") +@click.option("--tail", type=int, help="Number of lines to show from each container") +@click.argument("service", nargs=-1, required=False) +def logs(root, follow, tail, service): + command = ["logs"] + if follow: + command += ["--follow"] + if tail is not None: + command += ["--tail", str(tail)] + command += service + docker_compose(root, *command) + +@click.command(help="Create an Open edX user and interactively set their password") +@opts.root +@click.option("--superuser", is_flag=True, help="Make superuser") +@click.option("--staff", is_flag=True, help="Make staff user") +@click.argument("name") +@click.argument("email") +def createuser(root, superuser, staff, name, email): + ops.create_user(root, run_bash, superuser, staff, name, email) + +@click.command(help="Import the demo course") +@opts.root +def importdemocourse(root): + ops.import_demo_course(root, run_bash) + +@click.command(help="Re-index courses for better searching") +@opts.root +def indexcourses(root): + ops.index_courses(root, run_bash) + +@click.command( + help="Run Portainer (https://portainer.io), a UI for container supervision", + short_help="Run Portainer, a UI for container supervision", +) +@opts.root +@click.option("-p", "--port", type=int, default=9000, show_default=True, help="Bind port") +def portainer(root, port): + docker_run = [ + "--volume=/var/run/docker.sock:/var/run/docker.sock", + "--volume={}:/data".format(tutor_env.data_path(root, "portainer")), + "-p", "{port}:{port}".format(port=port), + "portainer/portainer:latest", + "--bind=:{}".format(port), + ] + click.echo(fmt.info("View the Portainer UI at http://localhost:{port}".format(port=port))) + utils.docker_run(*docker_run) + +def run_bash(root, service, command): + docker_compose(root, "run", "--rm", service, "bash", "-e", "-c", command) + +def docker_compose(root, *command): + return utils.docker_compose( + "-f", tutor_env.pathjoin(root, "local", "docker-compose.yml"), + "--project-name", "tutor_local", + *command + ) + +https.add_command(https_create) +https.add_command(https_renew) + +local.add_command(quickstart) +local.add_command(env) +local.add_command(pullimages) +local.add_command(start) +local.add_command(stop) +local.add_command(restart) +local.add_command(run) +local.add_command(databases) +local.add_command(https) +local.add_command(logs) +local.add_command(createuser) +local.add_command(importdemocourse) +local.add_command(indexcourses) +local.add_command(portainer) diff --git a/tutor/ops.py b/tutor/ops.py new file mode 100644 index 0000000..03866e6 --- /dev/null +++ b/tutor/ops.py @@ -0,0 +1,51 @@ +import click + +from . import config as tutor_config +from . import env +from . import fmt +from . import scripts + +# TODO merge this with scripts.py + +def migrate(root, run_func): + config = tutor_config.load(root) + click.echo(fmt.info("Creating lms/cms databases...")) + run_template(config, root, "lms", scripts.create_databases, run_func) + click.echo(fmt.info("Running lms migrations...")) + run_template(config, root, "lms", scripts.migrate_lms, run_func) + click.echo(fmt.info("Running cms migrations...")) + run_template(config, root, "cms", scripts.migrate_cms, run_func) + click.echo(fmt.info("Running forum migrations...")) + run_template(config, root, "forum", scripts.migrate_forum, run_func) + if config["ACTIVATE_NOTES"]: + click.echo(fmt.info("Running notes migrations...")) + run_template(config, root, "notes", scripts.migrate_notes, run_func) + if config["ACTIVATE_XQUEUE"]: + click.echo(fmt.info("Running xqueue migrations...")) + run_template(config, root, "xqueue", scripts.migrate_xqueue, run_func) + click.echo(fmt.info("Creating oauth2 users...")) + run_template(config, root, "lms", scripts.oauth2, run_func) + click.echo(fmt.info("Databases ready.")) + +def create_user(root, run_func, superuser, staff, name, email): + config = { + "OPTS": "", + "USERNAME": name, + "EMAIL": email, + } + if superuser: + config["OPTS"] += " --superuser" + if staff: + config["OPTS"] += " --staff" + run_template(config, root, "lms", scripts.create_user, run_func) + +def import_demo_course(root, run_func): + run_template({}, root, "lms", scripts.import_demo_course, run_func) + +def index_courses(root, run_func): + run_template({}, root, "cms", scripts.index_courses, run_func) + +def run_template(config, root, service, template, run_func): + command = env.render_str(template, config).strip() + if command: + run_func(root, service, command) diff --git a/tutor/opts.py b/tutor/opts.py new file mode 100644 index 0000000..e978db9 --- /dev/null +++ b/tutor/opts.py @@ -0,0 +1,43 @@ +import appdirs +import click + +root = click.option( + "-r", "--root", + envvar="TUTOR_ROOT", + default=appdirs.user_data_dir(appname="tutor"), show_default=True, + type=click.Path(resolve_path=True), + help="Root project directory (environment variable: TUTOR_ROOT)" +) + +edx_platform_path = click.option( + "-P", "--edx-platform-path", + envvar="TUTOR_EDX_PLATFORM_PATH", + type=click.Path(exists=True, dir_okay=True, resolve_path=True), + help="Mount a local edx-platform from the host (environment variable: TUTOR_EDX_PLATFORM_PATH)" +) + +edx_platform_settings = click.option( + "-S", "--edx-platform-settings", + envvar="TUTOR_EDX_PLATFORM_SETTINGS", + default="tutor.development", + help="Mount a local edx-platform from the host (environment variable: TUTOR_EDX_PLATFORM_PATH)" +) + +class YamlParamType(click.ParamType): + name = "yaml" + + def convert(self, value, param, ctx): + try: + k, v = value.split("=") + except ValueError: + self.fail("'{}' is not of the form 'key=value'.".format(value), param, ctx) + if v.isdigit(): + v = int(v) + elif v in ["true", "false"]: + v = (v == "true") + return (k, v) + +key_value = click.option( + "-s", type=YamlParamType(), multiple=True, metavar="KEY=VAL", + help="Set a configuration value (can be used multiple times)" +) diff --git a/tutor/scripts.py b/tutor/scripts.py new file mode 100644 index 0000000..453e203 --- /dev/null +++ b/tutor/scripts.py @@ -0,0 +1,41 @@ +create_databases = """dockerize -wait tcp://mysql:3306 -timeout 20s +mysql -u root --password="{{ MYSQL_PASSWORD }}" --host "mysql" -e 'CREATE DATABASE IF NOT EXISTS {{ MYSQL_DATABASE }};' +mysql -u root --password="{{ MYSQL_PASSWORD }}" --host "mysql" -e 'GRANT ALL ON {{ MYSQL_DATABASE }}.* TO "{{ MYSQL_USERNAME }}"@"%" IDENTIFIED BY "{{ MYSQL_PASSWORD }}";' + +{% if ACTIVATE_NOTES %} +mysql -u root --password="{{ MYSQL_PASSWORD }}" --host "mysql" -e 'CREATE DATABASE IF NOT EXISTS {{ NOTES_MYSQL_DATABASE }};' +mysql -u root --password="{{ MYSQL_PASSWORD }}" --host "mysql" -e 'GRANT ALL ON {{ NOTES_MYSQL_DATABASE }}.* TO "{{ NOTES_MYSQL_USERNAME }}"@"%" IDENTIFIED BY "{{ NOTES_MYSQL_PASSWORD }}";' +{% endif %} + +{% if ACTIVATE_XQUEUE %} +mysql -u root --password="{{ MYSQL_PASSWORD }}" --host "mysql" -e 'CREATE DATABASE IF NOT EXISTS {{ XQUEUE_MYSQL_DATABASE }};' +mysql -u root --password="{{ MYSQL_PASSWORD }}" --host "mysql" -e 'GRANT ALL ON {{ XQUEUE_MYSQL_DATABASE }}.* TO "{{ XQUEUE_MYSQL_USERNAME }}"@"%" IDENTIFIED BY "{{ XQUEUE_MYSQL_PASSWORD }}";' +{% endif %} +""" + +migrate_lms = "dockerize -wait tcp://mysql:3306 -timeout 20s && ./manage.py lms migrate" +migrate_cms = "dockerize -wait tcp://mysql:3306 -timeout 20s && ./manage.py cms migrate" +migrate_forum = "bundle exec rake search:initialize && bundle exec rake search:rebuild_index" +migrate_notes = "./manage.py migrate" +migrate_xqueue = "./manage.py migrate" + +oauth2 = """{% if ACTIVATE_NOTES %} +./manage.py lms manage_user notes notes@{{ LMS_HOST }} --staff --superuser +./manage.py lms create_oauth2_client \ + "http://notes.openedx:8000" \ + "http://notes.openedx:8000/complete/edx-oidc/" \ + confidential \ + --client_name edx-notes --client_id notes --client_secret {{ NOTES_OAUTH2_SECRET }} \ + --trusted --logout_uri "http://notes.openedx:8000/logout/" --username notes +{% endif %}""" + +https_certificates_create = """certbot certonly --standalone -n --agree-tos -m admin@{{ LMS_HOST }} -d {{ LMS_HOST }} -d {{ CMS_HOST }} -d preview.{{ LMS_HOST }} +{% if ACTIVATE_NOTES %} +certbot certonly --standalone -n --agree-tos -m admin@{{ LMS_HOST }} -d notes.{{ LMS_HOST }} +{% endif %}""" + +create_user = """./manage.py lms --settings=tutor.production manage_user {{ OPTS }} {{ USERNAME }} {{ EMAIL }} +./manage.py lms --settings=tutor.production changepassword {{ USERNAME }}""" +import_demo_course = """git clone https://github.com/edx/edx-demo-course --branch open-release/hawthorn.2 --depth 1 ../edx-demo-course +python ./manage.py cms --settings=tutor.production import ../data ../edx-demo-course""" +index_courses = "./manage.py cms --settings=tutor.production reindex_course --all --setup" diff --git a/android/templates/edx.properties b/tutor/templates/android/edx.properties similarity index 100% rename from android/templates/edx.properties rename to tutor/templates/android/edx.properties diff --git a/android/templates/gradle.properties b/tutor/templates/android/gradle.properties similarity index 100% rename from android/templates/gradle.properties rename to tutor/templates/android/gradle.properties diff --git a/android/templates/tutor.yaml b/tutor/templates/android/tutor.yaml similarity index 100% rename from android/templates/tutor.yaml rename to tutor/templates/android/tutor.yaml diff --git a/deploy/templates/mysql/auth.env b/tutor/templates/apps/mysql/auth.env similarity index 100% rename from deploy/templates/mysql/auth.env rename to tutor/templates/apps/mysql/auth.env diff --git a/deploy/templates/nginx/cms.conf b/tutor/templates/apps/nginx/cms.conf similarity index 100% rename from deploy/templates/nginx/cms.conf rename to tutor/templates/apps/nginx/cms.conf diff --git a/deploy/templates/nginx/extra.conf b/tutor/templates/apps/nginx/extra.conf similarity index 100% rename from deploy/templates/nginx/extra.conf rename to tutor/templates/apps/nginx/extra.conf diff --git a/deploy/templates/nginx/lms.conf b/tutor/templates/apps/nginx/lms.conf similarity index 100% rename from deploy/templates/nginx/lms.conf rename to tutor/templates/apps/nginx/lms.conf diff --git a/deploy/templates/nginx/tutor.conf b/tutor/templates/apps/nginx/tutor.conf similarity index 100% rename from deploy/templates/nginx/tutor.conf rename to tutor/templates/apps/nginx/tutor.conf diff --git a/deploy/templates/notes/tutor.py b/tutor/templates/apps/notes/settings/tutor.py similarity index 100% rename from deploy/templates/notes/tutor.py rename to tutor/templates/apps/notes/settings/tutor.py diff --git a/deploy/templates/openedx/config/lms.auth.json b/tutor/templates/apps/openedx/config/cms.auth.json similarity index 100% rename from deploy/templates/openedx/config/lms.auth.json rename to tutor/templates/apps/openedx/config/cms.auth.json diff --git a/deploy/templates/openedx/config/cms.env.json b/tutor/templates/apps/openedx/config/cms.env.json similarity index 100% rename from deploy/templates/openedx/config/cms.env.json rename to tutor/templates/apps/openedx/config/cms.env.json diff --git a/deploy/templates/openedx/config/cms.auth.json b/tutor/templates/apps/openedx/config/lms.auth.json similarity index 96% rename from deploy/templates/openedx/config/cms.auth.json rename to tutor/templates/apps/openedx/config/lms.auth.json index a851ba2..d2d89e5 100644 --- a/deploy/templates/openedx/config/cms.auth.json +++ b/tutor/templates/apps/openedx/config/lms.auth.json @@ -17,7 +17,7 @@ } }, "DOC_STORE_CONFIG": { - "db": "openedx", + "db": "{{ MONGODB_DATABASE }}", "host": "mongodb" }, "DATABASES": { diff --git a/deploy/templates/openedx/config/lms.env.json b/tutor/templates/apps/openedx/config/lms.env.json similarity index 100% rename from deploy/templates/openedx/config/lms.env.json rename to tutor/templates/apps/openedx/config/lms.env.json diff --git a/build/openedx/settings/lms/__init__.py b/tutor/templates/apps/openedx/settings/cms/__init__.py similarity index 100% rename from build/openedx/settings/lms/__init__.py rename to tutor/templates/apps/openedx/settings/cms/__init__.py diff --git a/deploy/templates/openedx/settings/cms/development.py b/tutor/templates/apps/openedx/settings/cms/development.py similarity index 100% rename from deploy/templates/openedx/settings/cms/development.py rename to tutor/templates/apps/openedx/settings/cms/development.py diff --git a/deploy/templates/openedx/settings/cms/production.py b/tutor/templates/apps/openedx/settings/cms/production.py similarity index 100% rename from deploy/templates/openedx/settings/cms/production.py rename to tutor/templates/apps/openedx/settings/cms/production.py diff --git a/deploy/templates/openedx/settings/cms/__init__.py b/tutor/templates/apps/openedx/settings/lms/__init__.py similarity index 100% rename from deploy/templates/openedx/settings/cms/__init__.py rename to tutor/templates/apps/openedx/settings/lms/__init__.py diff --git a/deploy/templates/openedx/settings/lms/development.py b/tutor/templates/apps/openedx/settings/lms/development.py similarity index 100% rename from deploy/templates/openedx/settings/lms/development.py rename to tutor/templates/apps/openedx/settings/lms/development.py diff --git a/deploy/templates/openedx/settings/lms/production.py b/tutor/templates/apps/openedx/settings/lms/production.py similarity index 100% rename from deploy/templates/openedx/settings/lms/production.py rename to tutor/templates/apps/openedx/settings/lms/production.py diff --git a/deploy/templates/xqueue/tutor.py b/tutor/templates/apps/xqueue/settings/tutor.py similarity index 100% rename from deploy/templates/xqueue/tutor.py rename to tutor/templates/apps/xqueue/settings/tutor.py diff --git a/build/android/Dockerfile b/tutor/templates/build/android/Dockerfile similarity index 100% rename from build/android/Dockerfile rename to tutor/templates/build/android/Dockerfile diff --git a/build/android/edx.properties b/tutor/templates/build/android/edx.properties similarity index 100% rename from build/android/edx.properties rename to tutor/templates/build/android/edx.properties diff --git a/build/forum/Dockerfile b/tutor/templates/build/forum/Dockerfile similarity index 100% rename from build/forum/Dockerfile rename to tutor/templates/build/forum/Dockerfile diff --git a/build/notes/Dockerfile b/tutor/templates/build/notes/Dockerfile similarity index 100% rename from build/notes/Dockerfile rename to tutor/templates/build/notes/Dockerfile diff --git a/build/openedx/Dockerfile b/tutor/templates/build/openedx/Dockerfile similarity index 93% rename from build/openedx/Dockerfile rename to tutor/templates/build/openedx/Dockerfile index 5830050..fdf6526 100644 --- a/build/openedx/Dockerfile +++ b/tutor/templates/build/openedx/Dockerfile @@ -53,12 +53,8 @@ RUN npm set progress=false \ ENV PATH ./node_modules/.bin:${PATH} # Install private requirements: this is useful for installing custom xblocks. -# In particular, to install xblocks from a private repository, clone the -# repositories to ./requirements on the host and add `-e ./myxblock/` to -# ./requirements/private.txt. COPY ./requirements/ /openedx/requirements -RUN touch /openedx/requirements/private.txt \ - && pip install -r /openedx/requirements/private.txt +RUN pip install -r /openedx/requirements/private.txt # Create folder that will store *.env.json and *.auth.json files, as well as # the tutor-specific settings files. diff --git a/build/openedx/bin/docker-entrypoint.sh b/tutor/templates/build/openedx/bin/docker-entrypoint.sh similarity index 100% rename from build/openedx/bin/docker-entrypoint.sh rename to tutor/templates/build/openedx/bin/docker-entrypoint.sh diff --git a/build/openedx/bin/openedx-assets b/tutor/templates/build/openedx/bin/openedx-assets similarity index 100% rename from build/openedx/bin/openedx-assets rename to tutor/templates/build/openedx/bin/openedx-assets diff --git a/tutor/templates/build/openedx/requirements/private.txt b/tutor/templates/build/openedx/requirements/private.txt new file mode 100644 index 0000000..a0ea63a --- /dev/null +++ b/tutor/templates/build/openedx/requirements/private.txt @@ -0,0 +1,6 @@ +# Add your additional requirements, such as xblocks, to this file. For +# requirements coming from private repositories, clone the repository to this +# folder and then add your requirement with the `-e` flag. Ex: +# +# git clone git@myserver:myprivaterepo.git +# echo "-e ./myprivaterepo/" >> private.txt diff --git a/deploy/templates/openedx/settings/lms/__init__.py b/tutor/templates/build/openedx/settings/cms/__init__.py similarity index 100% rename from deploy/templates/openedx/settings/lms/__init__.py rename to tutor/templates/build/openedx/settings/cms/__init__.py diff --git a/build/openedx/settings/cms/assets.py b/tutor/templates/build/openedx/settings/cms/assets.py similarity index 100% rename from build/openedx/settings/cms/assets.py rename to tutor/templates/build/openedx/settings/cms/assets.py diff --git a/tutor/templates/build/openedx/settings/lms/__init__.py b/tutor/templates/build/openedx/settings/lms/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/build/openedx/settings/lms/assets.py b/tutor/templates/build/openedx/settings/lms/assets.py similarity index 100% rename from build/openedx/settings/lms/assets.py rename to tutor/templates/build/openedx/settings/lms/assets.py diff --git a/tutor/templates/build/openedx/themes/README b/tutor/templates/build/openedx/themes/README new file mode 100644 index 0000000..6b80d52 --- /dev/null +++ b/tutor/templates/build/openedx/themes/README @@ -0,0 +1 @@ +Place here your custom theme folders to be included in your openedx image. diff --git a/build/xqueue/Dockerfile b/tutor/templates/build/xqueue/Dockerfile similarity index 100% rename from build/xqueue/Dockerfile rename to tutor/templates/build/xqueue/Dockerfile diff --git a/tutor/templates/config-defaults.yml b/tutor/templates/config-defaults.yml new file mode 100644 index 0000000..393630b --- /dev/null +++ b/tutor/templates/config-defaults.yml @@ -0,0 +1,12 @@ +--- +MYSQL_DATABASE: "openedx" +MYSQL_USERNAME: "openedx" +MONGODB_DATABASE: "openedx" +NOTES_MYSQL_DATABASE: "notes" +NOTES_MYSQL_USERNAME: "notes" +XQUEUE_AUTH_USERNAME: "lms" +XQUEUE_MYSQL_DATABASE: "xqueue" +XQUEUE_MYSQL_USERNAME: "xqueue" +OPENEDX_DOCKER_IMAGE: "regis/openedx:hawthorn" +NGINX_HTTP_PORT: 80 +NGINX_HTTPS_PORT: 443 diff --git a/tutor/templates/config.yml b/tutor/templates/config.yml new file mode 100644 index 0000000..02bd0b2 --- /dev/null +++ b/tutor/templates/config.yml @@ -0,0 +1,19 @@ +--- +LMS_HOST: "www.myopenedx.com" +CMS_HOST: "studio.{{ LMS_HOST }}" +PLATFORM_NAME: "My Open edX" +CONTACT_EMAIL: "contact@{{ LMS_HOST }}" +LANGUAGE_CODE: "en" +SECRET_KEY: "{{ RAND24 }}" +MYSQL_PASSWORD: "{{ RAND8 }}" +NOTES_MYSQL_PASSWORD: "{{ RAND8 }}" +NOTES_SECRET_KEY: "{{ RAND24 }}" +NOTES_OAUTH2_SECRET: "{{ RAND24 }}" +XQUEUE_AUTH_PASSWORD: "{{ RAND8 }}" +XQUEUE_MYSQL_PASSWORD: "{{ RAND8 }}" +XQUEUE_SECRET_KEY: "{{ RAND24 }}" +ACTIVATE_HTTPS: false +ACTIVATE_NOTES: false +ACTIVATE_XQUEUE: false +ID: "{{ RAND8 }}" +# pouac plonk diff --git a/deploy/k8s/admin.yml b/tutor/templates/k8s/adminuser.yml similarity index 98% rename from deploy/k8s/admin.yml rename to tutor/templates/k8s/adminuser.yml index cac3641..66f2501 100644 --- a/deploy/k8s/admin.yml +++ b/tutor/templates/k8s/adminuser.yml @@ -1,3 +1,4 @@ +--- apiVersion: v1 kind: ServiceAccount metadata: diff --git a/tutor/templates/k8s/deployments.yml b/tutor/templates/k8s/deployments.yml new file mode 100644 index 0000000..3f6d911 --- /dev/null +++ b/tutor/templates/k8s/deployments.yml @@ -0,0 +1,315 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: cms +spec: + replicas: 1 + selector: + matchLabels: + app: cms + template: + metadata: + labels: + app: cms + spec: + containers: + - name: cms + image: regis/openedx:hawthorn + env: + - name: SERVICE_VARIANT + value: cms + ports: + - containerPort: 8000 + volumeMounts: + - mountPath: /openedx/edx-platform/lms/envs/tutor/ + name: settings-lms + - mountPath: /openedx/edx-platform/cms/envs/tutor/ + name: settings-cms + - mountPath: /openedx/config + name: config + - mountPath: /openedx/data + name: data + #imagePullPolicy: Always + volumes: + - name: settings-lms + configMap: + name: openedx-settings-lms + - name: settings-cms + configMap: + name: openedx-settings-cms + - name: config + configMap: + name: openedx-config + - name: data + persistentVolumeClaim: + claimName: cms-data + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: forum +spec: + replicas: 1 + selector: + matchLabels: + app: forum + template: + metadata: + labels: + app: forum + spec: + containers: + - name: forum + image: regis/openedx-forum:hawthorn + ports: + - containerPort: 4567 + imagePullPolicy: Always + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: lms +spec: + replicas: 1 + selector: + matchLabels: + app: lms + template: + metadata: + labels: + app: lms + spec: + containers: + - name: lms + image: regis/openedx:hawthorn + ports: + - containerPort: 8000 + volumeMounts: + - mountPath: /openedx/edx-platform/lms/envs/tutor/ + name: settings-lms + - mountPath: /openedx/edx-platform/cms/envs/tutor/ + name: settings-cms + - mountPath: /openedx/config + name: config + - mountPath: /openedx/data + name: data + imagePullPolicy: Always + volumes: + - name: settings-lms + configMap: + name: openedx-settings-lms + - name: settings-cms + configMap: + name: openedx-settings-cms + - name: config + configMap: + name: openedx-config + - name: data + persistentVolumeClaim: + claimName: lms-data + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: elasticsearch +spec: + replicas: 1 + selector: + matchLabels: + app: elasticsearch + template: + metadata: + labels: + app: elasticsearch + spec: + containers: + - name: elasticsearch + image: elasticsearch:1.5.2 + env: + - name: ES_JAVA_OPTS + value: "-Xms1g -Xmx1g" + - name: "cluster.name" + value: openedx + - name: "bootstrap.memory_lock" + value: "true" + ports: + - containerPort: 9200 + volumeMounts: + - mountPath: /usr/share/elasticsearch/data + name: data + volumes: + - name: data + persistentVolumeClaim: + claimName: elasticsearch + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: memcached +spec: + replicas: 1 + selector: + matchLabels: + app: memcached + template: + metadata: + labels: + app: memcached + spec: + containers: + - name: memcached + image: memcached:1.4.38 + ports: + - containerPort: 11211 + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: mongodb +spec: + replicas: 1 + selector: + matchLabels: + app: mongodb + template: + metadata: + labels: + app: mongodb + spec: + containers: + - name: mongodb + image: mongo:3.2.16 + command: ["mongod", "--smallfiles", "--nojournal", "--storageEngine", "wiredTiger"] + ports: + - containerPort: 27017 + volumeMounts: + - mountPath: /data/db + name: data + volumes: + - name: data + emptyDir: {} + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: mysql +spec: + replicas: 1 + selector: + matchLabels: + app: mysql + template: + metadata: + labels: + app: mysql + spec: + containers: + - name: mysql + image: mysql:5.6.36 + env: + - name: MYSQL_ROOT_PASSWORD + valueFrom: + configMapKeyRef: + name: mysql-config + key: MYSQL_ROOT_PASSWORD + ports: + - containerPort: 3306 + volumeMounts: + - mountPath: /var/lib/mysql + name: data + volumes: + - name: data + persistentVolumeClaim: + claimName: mysql + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx +spec: + replicas: 1 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + initContainers: + - name: clean-openedx-staticfiles + image: regis/openedx:hawthorn + command: ['rm', '-rf', '/var/www/openedx/staticfiles'] + volumeMounts: + - mountPath: /var/www/openedx/ + name: openedx-staticfiles + imagePullPolicy: Always + - name: init-openedx-staticfiles + image: regis/openedx:hawthorn + command: ['cp', '-r', '/openedx/staticfiles', '/var/www/openedx/'] + volumeMounts: + - mountPath: /var/www/openedx/ + name: openedx-staticfiles + imagePullPolicy: Always + containers: + - name: nginx + image: nginx:1.13 + volumeMounts: + - mountPath: /etc/nginx/conf.d/ + name: config + - mountPath: /var/www/openedx/ + name: openedx-staticfiles + - mountPath: /openedx/data/lms + name: data + ports: + - containerPort: 80 + name: http-port + - containerPort: 443 + name: https-port + volumes: + - name: config + configMap: + name: nginx-config + - name: openedx-staticfiles + persistentVolumeClaim: + claimName: openedx-staticfiles + - name: data + persistentVolumeClaim: + claimName: lms-data + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: rabbitmq +spec: + replicas: 1 + selector: + matchLabels: + app: rabbitmq + template: + metadata: + labels: + app: rabbitmq + spec: + containers: + - name: rabbitmq + image: rabbitmq:3.6.10 + ports: + - containerPort: 5672 + volumeMounts: + - mountPath: /var/lib/rabbitmq + name: data + volumes: + - name: data + persistentVolumeClaim: + claimName: rabbitmq + diff --git a/deploy/k8s/templates/ingress.yml b/tutor/templates/k8s/ingress.yml similarity index 99% rename from deploy/k8s/templates/ingress.yml rename to tutor/templates/k8s/ingress.yml index c4dad2e..04d5358 100644 --- a/deploy/k8s/templates/ingress.yml +++ b/tutor/templates/k8s/ingress.yml @@ -1,3 +1,4 @@ +--- apiVersion: extensions/v1beta1 kind: Ingress metadata: diff --git a/deploy/k8s/namespace.yml b/tutor/templates/k8s/namespace.yml similarity index 93% rename from deploy/k8s/namespace.yml rename to tutor/templates/k8s/namespace.yml index efd7187..be191b5 100644 --- a/deploy/k8s/namespace.yml +++ b/tutor/templates/k8s/namespace.yml @@ -1,3 +1,4 @@ +--- apiVersion: v1 kind: Namespace metadata: diff --git a/tutor/templates/k8s/services.yml b/tutor/templates/k8s/services.yml new file mode 100644 index 0000000..fc5ce4f --- /dev/null +++ b/tutor/templates/k8s/services.yml @@ -0,0 +1,123 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: cms +spec: + type: NodePort + ports: + - port: 8000 + protocol: TCP + selector: + app: cms + +--- +apiVersion: v1 +kind: Service +metadata: + name: forum +spec: + type: NodePort + ports: + - port: 4567 + protocol: TCP + selector: + app: forum + +--- +apiVersion: v1 +kind: Service +metadata: + name: lms +spec: + type: NodePort + ports: + - port: 8000 + protocol: TCP + selector: + app: lms + +--- +apiVersion: v1 +kind: Service +metadata: + name: elasticsearch +spec: + type: NodePort + ports: + - port: 9200 + protocol: TCP + selector: + app: elasticsearch + +--- +apiVersion: v1 +kind: Service +metadata: + name: memcached +spec: + type: NodePort + ports: + - port: 11211 + protocol: TCP + selector: + app: memcached + +--- +apiVersion: v1 +kind: Service +metadata: + name: mongodb +spec: + type: NodePort + ports: + - port: 27017 + protocol: TCP + selector: + app: mongodb + +--- +apiVersion: v1 +kind: Service +metadata: + name: mysql +spec: + type: NodePort + ports: + - port: 3306 + protocol: TCP + selector: + app: mysql + +--- +apiVersion: v1 +kind: Service +metadata: + name: nginx +spec: + type: NodePort + ports: + - port: 80 + protocol: TCP + name: http + targetPort: http-port + - port: 443 + protocol: TCP + name: https + targetPort: https-port + selector: + app: nginx + +--- +apiVersion: v1 +kind: Service +metadata: + name: rabbitmq +spec: + type: NodePort + ports: + - port: 5672 + protocol: TCP + selector: + app: rabbitmq + diff --git a/tutor/templates/k8s/volumes.yml b/tutor/templates/k8s/volumes.yml new file mode 100644 index 0000000..332f31b --- /dev/null +++ b/tutor/templates/k8s/volumes.yml @@ -0,0 +1,71 @@ +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: cms-data +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 2Gi + +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: lms-data +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 2Gi + +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: elasticsearch +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 2Gi + +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: mysql +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 5Gi + +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: openedx-staticfiles +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi + +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: rabbitmq +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi diff --git a/deploy/local/templates/docker-compose.yml b/tutor/templates/local/docker-compose.yml similarity index 75% rename from deploy/local/templates/docker-compose.yml rename to tutor/templates/local/docker-compose.yml index e452b47..f34b6dc 100644 --- a/deploy/local/templates/docker-compose.yml +++ b/tutor/templates/local/docker-compose.yml @@ -21,7 +21,7 @@ services: restart: unless-stopped volumes: - ../../data/mysql:/var/lib/mysql - env_file: ../env/mysql/auth.env + env_file: ../apps/mysql/auth.env elasticsearch: image: elasticsearch:1.5.2 @@ -38,7 +38,7 @@ services: - ../../data/elasticsearch:/usr/share/elasticsearch/data openedx-assets: - image: ${OPENEDX_DOCKER_IMAGE:-regis/openedx:hawthorn} + image: {{ OPENEDX_DOCKER_IMAGE }} volumes: - ../../data/openedx:/var/www/openedx command: bash -c "rm -rf /var/www/openedx/staticfiles && cp -r /openedx/staticfiles/ /var/www/openedx/" @@ -47,10 +47,10 @@ services: image: nginx:1.13 restart: unless-stopped ports: - - "${NGINX_HTTP_PORT:-80}:80" - - "${NGINX_HTTPS_PORT:-443}:443" + - "{{ NGINX_HTTP_PORT }}:80" + - "{{ NGINX_HTTPS_PORT }}:443" volumes: - - ../env/nginx:/etc/nginx/conf.d/ + - ../apps/nginx:/etc/nginx/conf.d/ - ../../data/openedx:/var/www/openedx:ro - ../../data/cms:/openedx/data/cms/:ro - ../../data/lms:/openedx/data/lms/:ro @@ -83,15 +83,15 @@ services: ############# LMS and CMS lms: - image: ${OPENEDX_DOCKER_IMAGE:-regis/openedx:hawthorn} + image: {{ OPENEDX_DOCKER_IMAGE }} environment: SERVICE_VARIANT: lms SETTINGS: ${EDX_PLATFORM_SETTINGS:-tutor.production} restart: unless-stopped volumes: - - ../env/openedx/settings/lms/:/openedx/edx-platform/lms/envs/tutor/ - - ../env/openedx/settings/cms/:/openedx/edx-platform/cms/envs/tutor/ - - ../env/openedx/config/:/openedx/config/ + - ../apps/openedx/settings/lms/:/openedx/edx-platform/lms/envs/tutor/ + - ../apps/openedx/settings/cms/:/openedx/edx-platform/cms/envs/tutor/ + - ../apps/openedx/config/:/openedx/config/ - ../../data/lms:/openedx/data depends_on: - elasticsearch @@ -103,15 +103,15 @@ services: - smtp cms: - image: ${OPENEDX_DOCKER_IMAGE:-regis/openedx:hawthorn} + image: {{ OPENEDX_DOCKER_IMAGE }} environment: SERVICE_VARIANT: cms SETTINGS: ${EDX_PLATFORM_SETTINGS:-tutor.production} restart: unless-stopped volumes: - - ../env/openedx/settings/lms/:/openedx/edx-platform/lms/envs/tutor/ - - ../env/openedx/settings/cms/:/openedx/edx-platform/cms/envs/tutor/ - - ../env/openedx/config/:/openedx/config/ + - ../apps/openedx/settings/lms/:/openedx/edx-platform/lms/envs/tutor/ + - ../apps/openedx/settings/cms/:/openedx/edx-platform/cms/envs/tutor/ + - ../apps/openedx/config/:/openedx/config/ - ../../data/cms:/openedx/data depends_on: - elasticsearch @@ -125,7 +125,7 @@ services: # We could probably create one service per queue here. For small instances, it is not necessary. lms_worker: - image: ${OPENEDX_DOCKER_IMAGE:-regis/openedx:hawthorn} + image: {{ OPENEDX_DOCKER_IMAGE }} environment: SERVICE_VARIANT: lms SETTINGS: ${EDX_PLATFORM_SETTINGS:-tutor.production} @@ -133,15 +133,15 @@ services: command: ./manage.py lms celery worker --loglevel=info --hostname=edx.lms.core.default.%%h --maxtasksperchild 100 restart: unless-stopped volumes: - - ../env/openedx/settings/lms/:/openedx/edx-platform/lms/envs/tutor/ - - ../env/openedx/settings/cms/:/openedx/edx-platform/cms/envs/tutor/ - - ../env/openedx/config/:/openedx/config/ + - ../apps/openedx/settings/lms/:/openedx/edx-platform/lms/envs/tutor/ + - ../apps/openedx/settings/cms/:/openedx/edx-platform/cms/envs/tutor/ + - ../apps/openedx/config/:/openedx/config/ - ../../data/lms:/openedx/data depends_on: - lms cms_worker: - image: ${OPENEDX_DOCKER_IMAGE:-regis/openedx:hawthorn} + image: {{ OPENEDX_DOCKER_IMAGE }} environment: SERVICE_VARIANT: cms SETTINGS: ${EDX_PLATFORM_SETTINGS:-tutor.production} @@ -149,9 +149,9 @@ services: command: ./manage.py cms celery worker --loglevel=info --hostname=edx.cms.core.default.%%h --maxtasksperchild 100 restart: unless-stopped volumes: - - ../env/openedx/settings/lms/:/openedx/edx-platform/lms/envs/tutor/ - - ../env/openedx/settings/cms/:/openedx/edx-platform/cms/envs/tutor/ - - ../env/openedx/config/:/openedx/config/ + - ../apps/openedx/settings/lms/:/openedx/edx-platform/lms/envs/tutor/ + - ../apps/openedx/settings/cms/:/openedx/edx-platform/cms/envs/tutor/ + - ../apps/openedx/config/:/openedx/config/ - ../../data/cms:/openedx/data depends_on: - cms @@ -167,7 +167,7 @@ services: environment: DJANGO_SETTINGS_MODULE: notesserver.settings.tutor volumes: - - ../env/notes/tutor.py:/openedx/edx-notes-api/notesserver/settings/tutor.py + - ../apps/notes/settings/tutor.py:/openedx/edx-notes-api/notesserver/settings/tutor.py - ../../data/notes:/openedx/data restart: unless-stopped depends_on: @@ -179,7 +179,7 @@ services: xqueue: image: regis/openedx-xqueue:hawthorn volumes: - - ../env/xqueue/tutor.py:/openedx/xqueue/xqueue/tutor.py + - ../apps/xqueue/settings/tutor.py:/openedx/xqueue/xqueue/tutor.py - ../../data/xqueue:/openedx/data environment: DJANGO_SETTINGS_MODULE: xqueue.tutor @@ -190,7 +190,7 @@ services: xqueue_consumer: image: regis/openedx-xqueue:hawthorn volumes: - - ../env/xqueue/tutor.py:/openedx/xqueue/xqueue/tutor.py + - ../apps/xqueue/settings/tutor.py:/openedx/xqueue/xqueue/tutor.py - ../../data/xqueue:/openedx/data environment: DJANGO_SETTINGS_MODULE: xqueue.tutor diff --git a/tutor/ui.py b/tutor/ui.py new file mode 100644 index 0000000..6a3c682 --- /dev/null +++ b/tutor/ui.py @@ -0,0 +1,13 @@ +import click +import click_repl + +@click.command( + short_help="Interactive shell", + help="Launch an interactive shell for launching Tutor commands" +) +def ui(): + click.echo("""Welcome to the Tutor interactive shell UI! +Type "help" to view all available commands. +Type "local quickstart" to configure and launch a new platform from scratch. +""") + click_repl.repl(click.get_current_context()) diff --git a/tutor/utils.py b/tutor/utils.py new file mode 100644 index 0000000..951d25d --- /dev/null +++ b/tutor/utils.py @@ -0,0 +1,53 @@ +import click +import random +import shutil +import string +import subprocess + +from . import exceptions +from . import fmt + + +def random_string(length): + return "".join([random.choice(string.ascii_letters + string.digits) for _ in range(length)]) + +def docker_run(*command): + return docker("run", "--rm", "-it", *command) + +def docker(*command): + if shutil.which("docker") is None: + raise exceptions.TutorError("docker is not installed. Please follow instructions from https://docs.docker.com/install/") + return execute("docker", *command) + +def docker_compose(*command): + if shutil.which("docker-compose") is None: + raise exceptions.TutorError("docker-compose is not installed. Please follow instructions from https://docs.docker.com/compose/install/") + return execute("docker-compose", *command) + +def kubectl(*command): + if shutil.which("kubectl") is None: + raise exceptions.TutorError( + "kubectl is not installed. Please follow instructions from https://kubernetes.io/docs/tasks/tools/install-kubectl/" + ) + return execute("kubectl", *command) + +def execute(*command): + click.echo(fmt.command(" ".join(command))) + with subprocess.Popen(command) as p: + try: + result = p.wait(timeout=None) + except KeyboardInterrupt: + p.kill() + p.wait() + raise + except Exception as e: + p.kill() + p.wait() + raise exceptions.TutorError("Command failed: {}".format( + " ".join(command) + )) + if result > 0: + raise exceptions.TutorError("Command failed with status {}: {}".format( + result, + " ".join(command) + )) diff --git a/tutor/webui.py b/tutor/webui.py new file mode 100644 index 0000000..934d26c --- /dev/null +++ b/tutor/webui.py @@ -0,0 +1,134 @@ +import io +import os +import platform +import subprocess +import sys +import tarfile +import yaml +from urllib.request import urlopen + +import click + +# Note: it is important that this module does not depend on config, such that +# the web ui can be launched even where there is no configuration. +from . import fmt +from . import opts +from . import env as tutor_env + +@click.group( + short_help="Web user interface", + help="""Run Tutor commands from a web terminal""" +) +def webui(): + pass + +@click.command( + help="Start the web UI", +) +@opts.root +@click.option( + "-p", "--port", default=3737, type=int, show_default=True, + help="Port number to listen", +) +@click.option( + "-h", "--host", default="0.0.0.0", show_default=True, + help="Host address to listen", +) +def start(root, port, host): + check_gotty_binary(root) + click.echo(fmt.info("Access the Tutor web UI at http://{}:{}".format(host, port))) + while True: + config = load_config(root) + user = config["user"] + password = config["password"] + command = [ + gotty_path(root), "--permit-write", + "--address", host, "--port", str(port), + "--title-format", "Tutor web UI - {{ .Command }} ({{ .Hostname }})", + ] + if user and password: + credential = "{}:{}".format(user, password) + command += ["--credential", credential] + else: + click.echo(fmt.alert("Running web UI without user authentication. Run 'tutor webui configure' to setup authentication")) + command += [sys.argv[0], "ui"] + p = subprocess.Popen(command) + while True: + try: + p.wait(timeout=2) + except subprocess.TimeoutExpired: + new_config = load_config(root) + if new_config != config: + click.echo("WARNING configuration changed. Tutor web UI is now going to restart. Reload this page to continue.") + p.kill() + p.wait() + break + +@click.command(help="Configure authentication") +@opts.root +@click.option("-u", "--user", prompt="User name", help="Authentication user name") +@click.option( + "-p", "--password", + prompt=True, hide_input=True, confirmation_prompt=True, + help="Authentication password" +) +def configure(root, user, password): + save_config(root, { + "user": user, + "password": password, + }) + click.echo(fmt.info( + "The web UI configuration has been updated. " + "If at any point you wish to reset your username and password, " + "just delete the following file:\n\n {}".format(config_path(root)) + )) + +def check_gotty_binary(root): + path = gotty_path(root) + if os.path.exists(path): + return + click.echo(fmt.info("Downloading gotty to {}...".format(path))) + + # Generate release url + # Note: I don't know how to handle arm + architecture = "amd64" if platform.architecture()[0] == "64bit" else "386" + url = "https://github.com/yudai/gotty/releases/download/v1.0.1/gotty_{system}_{architecture}.tar.gz".format( + system=platform.system().lower(), + architecture=architecture, + ) + + # Download + response = urlopen(url) + + # Decompress + dirname = os.path.dirname(path) + if not os.path.exists(dirname): + os.makedirs(dirname) + compressed = tarfile.open(fileobj=io.BytesIO(response.read())) + compressed.extract("./gotty", dirname) + +def load_config(root): + path = config_path(root) + if not os.path.exists(path): + save_config(root, { + "user": None, + "password": None, + }) + with open(config_path(root)) as f: + return yaml.load(f) + +def save_config(root, config): + with open(config_path(root), "w") as of: + yaml.dump(config, of, default_flow_style=False) + +def gotty_path(root): + return get_path(root, "gotty") + +def config_path(root): + return get_path(root, "config.yml") + +def get_path(root, filename): + return tutor_env.pathjoin(root, "webui", filename) + +webui.add_command(start) +webui.add_command(configure)