diff --git a/.ci/fuzzy.sh b/.ci/fuzzy.sh index e17077d..818a9ff 100755 --- a/.ci/fuzzy.sh +++ b/.ci/fuzzy.sh @@ -43,14 +43,14 @@ function fuzzy() { ### ### 1 Argument ### - cmd="${MY_PATH}/../bin/vhost_gen.py -${arg1} ${val1}" + cmd="${MY_PATH}/../bin/vhost-gen -${arg1} ${val1}" out="$( eval "${cmd}" 2>&1 || true )" if errored "${out}"; then printf "[%04d] %s\n" "${count}" "${cmd}" echo "${out}" exit 1 fi - cmd="${MY_PATH}/../bin/vhost_gen.py -p ${val1}" + cmd="${MY_PATH}/../bin/vhost-gen -p ${val1}" out="$( eval "${cmd}" 2>&1 || true )" if errored "${out}"; then printf "[%04d] %s\n" "${count}" "${cmd}" @@ -61,14 +61,14 @@ function fuzzy() { ### ### 2 Arguments ### - cmd="${MY_PATH}/../bin/vhost_gen.py -${arg1} ${val1} -${arg2} ${val2}" + cmd="${MY_PATH}/../bin/vhost-gen -${arg1} ${val1} -${arg2} ${val2}" out="$( eval "${cmd}" 2>&1 || true )" if errored "${out}"; then printf "[%04d] %s\n" "${count}" "${cmd}" echo "${out}" exit 1 fi - cmd="${MY_PATH}/../bin/vhost_gen.py -p ${val1} -n NAME" + cmd="${MY_PATH}/../bin/vhost-gen -p ${val1} -n NAME" out="$( eval "${cmd}" 2>&1 || true )" if errored "${out}"; then printf "[%04d] %s\n" "${count}" "${cmd}" @@ -79,14 +79,14 @@ function fuzzy() { ### ### 3 Arguments ### - cmd="${MY_PATH}/../bin/vhost_gen.py -${arg1} ${val1} -${arg2} ${val2} -${arg3} ${val3}" + cmd="${MY_PATH}/../bin/vhost-gen -${arg1} ${val1} -${arg2} ${val2} -${arg3} ${val3}" out="$( eval "${cmd}" 2>&1 || true )" if errored "${out}"; then printf "[%04d] %s\n" "${count}" "${cmd}" echo "${out}" exit 1 fi - cmd="${MY_PATH}/../bin/vhost_gen.py -p ${val1} -n NAME -${arg3} ${val3}" + cmd="${MY_PATH}/../bin/vhost-gen -p ${val1} -n NAME -${arg3} ${val3}" out="$( eval "${cmd}" 2>&1 || true )" if errored "${out}"; then printf "[%04d] %s\n" "${count}" "${cmd}" @@ -97,14 +97,14 @@ function fuzzy() { ### ### 4 Arguments ### - cmd="${MY_PATH}/../bin/vhost_gen.py -${arg1} ${val1} -${arg2} ${val2} -${arg3} ${val3} -${arg4} ${val4}" + cmd="${MY_PATH}/../bin/vhost-gen -${arg1} ${val1} -${arg2} ${val2} -${arg3} ${val3} -${arg4} ${val4}" out="$( eval "${cmd}" 2>&1 || true )" if errored "${out}"; then printf "[%04d] %s\n" "${count}" "${cmd}" echo "${out}" exit 1 fi - cmd="${MY_PATH}/../bin/vhost_gen.py -p ${val1} -n NAME -${arg3} ${val3} -${arg4} ${val4}" + cmd="${MY_PATH}/../bin/vhost-gen -p ${val1} -n NAME -${arg3} ${val3} -${arg4} ${val4}" out="$( eval "${cmd}" 2>&1 || true )" if errored "${out}"; then printf "[%04d] %s\n" "${count}" "${cmd}" @@ -115,14 +115,14 @@ function fuzzy() { ### ### 5 Arguments ### - cmd="${MY_PATH}/../bin/vhost_gen.py -${arg1} ${val1} -${arg2} ${val2} -${arg3} ${val3} -${arg4} ${val4} -${arg5} ${val5}" + cmd="${MY_PATH}/../bin/vhost-gen -${arg1} ${val1} -${arg2} ${val2} -${arg3} ${val3} -${arg4} ${val4} -${arg5} ${val5}" out="$( eval "${cmd}" 2>&1 || true )" if errored "${out}"; then printf "[%04d] %s\n" "${count}" "${cmd}" echo "${out}" exit 1 fi - cmd="${MY_PATH}/../bin/vhost_gen.py -p ${val1} -n NAME -${arg3} ${val3} -${arg4} ${val4} -${arg5} ${val5}" + cmd="${MY_PATH}/../bin/vhost-gen -p ${val1} -n NAME -${arg3} ${val3} -${arg4} ${val4} -${arg5} ${val5}" out="$( eval "${cmd}" 2>&1 || true )" if errored "${out}"; then printf "[%04d] %s\n" "${count}" "${cmd}" diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml new file mode 100644 index 0000000..af19ab2 --- /dev/null +++ b/.github/workflows/linting.yml @@ -0,0 +1,35 @@ +--- + +### +### Lints all generic and json files in the whole git repository +### + +name: linting +on: + pull_request: + push: + branches: + - master + tags: + +jobs: + lint: + runs-on: ubuntu-latest + strategy: + fail-fast: False + matrix: + target: + - pycodestyle + - pydocstyle + - black + + name: "[ ${{ matrix.target }} ]" + steps: + - name: Checkout repository + uses: actions/checkout@master + + - name: Lint + run: | + make ${target} + env: + target: ${{ matrix.target }} diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml new file mode 100644 index 0000000..67a18b0 --- /dev/null +++ b/.github/workflows/testing.yml @@ -0,0 +1,39 @@ +--- + +### +### Lints all generic and json files in the whole git repository +### + +name: testing +on: + pull_request: + push: + branches: + - master + tags: + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: False + matrix: + version: + - '2.7' + - '3.6' + - '3.7' + - '3.8' + target: + - test + + name: "[ ${{ matrix.target }} ${{ matrix.version}} ]" + steps: + - name: Checkout repository + uses: actions/checkout@master + + - name: Lint + run: | + make ${target} VERSION=${version} + env: + target: ${{ matrix.target }} + version: ${{ matrix.version }} diff --git a/.gitignore b/.gitignore index 72bc3fe..fd75e20 100644 --- a/.gitignore +++ b/.gitignore @@ -1,86 +1,5 @@ -# Note: -# To effectively apply the changes you will have -# to re-index the git index (if there are already -# commited files) -# -# $ git rm -r --cached . -# $ git add . -# $ git commit -m ".gitignore index rebuild" -# - - -###################################### -# CUSTOM -###################################### -*.pyc - - -###################################### -# GENERIC -###################################### - -###### std ###### -.lock -*.log - -###### patches/diffs ###### -*.patch -*.diff -*.orig -*.rej - - -###################################### -# Operating Systems -###################################### - -###### OSX ###### -._* -.DS* -.Spotlight-V100 -.Trashes - -###### Windows ###### -Thumbs.db -ehthumbs.db -Desktop.ini -$RECYCLE.BIN/ -*.lnk -*.shortcut - -###################################### -# Editors -###################################### - -###### Sublime ###### -*.sublime-workspace -*.sublime-project - -###### Eclipse ###### -.classpath -.buildpath -.project -.settings/ - -###### Netbeans ###### -/nbproject/ - -###### Intellij IDE ###### -.idea/ -.idea_modules/ - -###### vim ###### -*.swp -*.swo -*.swn -*.swm -*~ - -###### TextMate ###### -.tm_properties -*.tmproj - -###### BBEdit ###### -*.bbprojectd -*.bbproject - +vhost_gen.egg-info/ +build/ +dist/ +.*pyc +__pycache__/ diff --git a/.travis.yml b/.travis.yml index 4393ebe..333fc95 100644 --- a/.travis.yml +++ b/.travis.yml @@ -70,8 +70,8 @@ install: ### before_script: # Show versions - - ./bin/vhost_gen.py --help - - ./bin/vhost_gen.py --version + - ./bin/vhost-gen --help + - ./bin/vhost-gen --version ### @@ -85,19 +85,19 @@ script: - make test # Saving (nginx) - - ./bin/vhost_gen.py -p ./ -n name -t etc/templates/ -c etc/conf.yml -s + - ./bin/vhost-gen -p ./ -n name -t etc/templates/ -c etc/conf.yml -s - cat /etc/nginx/conf.d/name.conf # Saving (nginx example) - - ./bin/vhost_gen.py -p ./ -n name -t etc/templates/ -c examples/conf.nginx.yml -s + - ./bin/vhost-gen -p ./ -n name -t etc/templates/ -c examples/conf.nginx.yml -s - cat /etc/nginx/conf.d/name.conf # Saving (apache22 example) - - ./bin/vhost_gen.py -p ./ -n name -t etc/templates/ -c examples/conf.apache22.yml -s + - ./bin/vhost-gen -p ./ -n name -t etc/templates/ -c examples/conf.apache22.yml -s - cat /etc//httpd/conf.d/name.conf # Saving (apache24 example) - - ./bin/vhost_gen.py -p ./ -n name -t etc/templates/ -c examples/conf.apache24.yml -s + - ./bin/vhost-gen -p ./ -n name -t etc/templates/ -c examples/conf.apache24.yml -s - cat /etc/apache2/conf.d/name.conf # Find flaws via fuzzying (before installing config files) diff --git a/Makefile b/Makefile index b92f038..0b09a45 100644 --- a/Makefile +++ b/Makefile @@ -1,98 +1,178 @@ -# Unix Makefile +ifneq (,) +.error This Makefile requires GNU Make. +endif -# Configuration -SHELL = /bin/sh +# ------------------------------------------------------------------------------------------------- +# Default configuration +# ------------------------------------------------------------------------------------------------- +.PHONY: help lint pycodestyle pydocstyle black dist sdist bdist build checkbuild deploy autoformat clean -MKDIR_P = mkdir -p -BINARY = vhost_gen.py +VERSION = 2.7 +BINPATH = bin/ +BINNAME = vhost-gen + CONFIG = conf.yml TPLDIR = templates -all: - @echo "Nothing to make." - @echo "Type 'make install' or 'make uninstall'" - +# ------------------------------------------------------------------------------------------------- +# Default Target +# ------------------------------------------------------------------------------------------------- help: - @echo Options - @echo " make lint" - @echo " Check for python errors" - @echo "" - @echo " make test" - @echo " Test vhost-gen" - @echo "" - @echo " make install" - @echo " Install everthing (requires sudo or root)" - @echo "" - @echo " make uninstall" - @echo " Remove everything (requires sudo or root)" - @echo "" - @echo " make help" - @echo " Show this help screen" + @echo "lint Lint source code" + @echo "test Test source code" + @echo "autoformat Autoformat code according to Python black" + @echo "install Install (requires sudo or root)" + @echo "uninstall Uninstall (requires sudo or root)" + @echo "build Build Python package" + @echo "dist Create source and binary distribution" + @echo "sdist Create source distribution" + @echo "bdist Create binary distribution" + @echo "clean Build" -lint: - if pycodestyle --version >/dev/null 2>&1; then pycodestyle -v --max-line-length=100 bin/vhost_gen.py; else echo "not installed"; fi - if pylint --version >/dev/null 2>&1; then pylint bin/vhost_gen.py; else echo "not installed"; fi - if flake8 --version >/dev/null 2>&1; then flake8 --max-line-len=100 bin/vhost_gen.py; else echo "not installed"; fi +# ------------------------------------------------------------------------------------------------- +# Lint Targets +# ------------------------------------------------------------------------------------------------- +lint: pycodestyle pydocstyle black + +pycodestyle: + docker run --rm -v $(PWD):/data cytopia/pycodestyle --show-source --show-pep8 $(BINPATH)$(BINNAME) + +pydocstyle: + docker run --rm -v $(PWD):/data cytopia/pydocstyle $(BINPATH)$(BINNAME) + +black: + docker run --rm -v ${PWD}:/data cytopia/black -l 100 --check --diff $(BINPATH)$(BINNAME) -test: - # [NORMAL] Check for python errors - ./bin/vhost_gen.py -p ./ -n name -t etc/templates/ >/dev/null - ./bin/vhost_gen.py -p ./ -n name -t etc/templates/ -c etc/conf.yml >/dev/null - ./bin/vhost_gen.py -p ./ -n name -t etc/templates/ -c examples/conf.nginx.yml >/dev/null - ./bin/vhost_gen.py -p ./ -n name -t etc/templates/ -c examples/conf.apache22.yml >/dev/null - ./bin/vhost_gen.py -p ./ -n name -t etc/templates/ -c examples/conf.apache24.yml >/dev/null - # [REVERSE] Check for python errors - ./bin/vhost_gen.py -r http://127.0.0.1:3000 -l / -n name -t etc/templates/ >/dev/null - ./bin/vhost_gen.py -r http://127.0.0.1:3000 -l / -n name -t etc/templates/ -c etc/conf.yml >/dev/null - ./bin/vhost_gen.py -r http://127.0.0.1:3000 -l / -n name -t etc/templates/ -c examples/conf.nginx.yml >/dev/null - ./bin/vhost_gen.py -r http://127.0.0.1:3000 -l / -n name -t etc/templates/ -c examples/conf.apache22.yml >/dev/null - ./bin/vhost_gen.py -r http://127.0.0.1:3000 -l / -n name -t etc/templates/ -c examples/conf.apache24.yml >/dev/null - # [NORMAL] Check for template generation errors - ./bin/vhost_gen.py -p ./ -n name -t etc/templates/ | grep -v '__' - ./bin/vhost_gen.py -p ./ -n name -t etc/templates/ -c etc/conf.yml | grep -v '__' - ./bin/vhost_gen.py -p ./ -n name -t etc/templates/ -c examples/conf.nginx.yml | grep -v '__' - ./bin/vhost_gen.py -p ./ -n name -t etc/templates/ -c examples/conf.apache22.yml | grep -v '__' - ./bin/vhost_gen.py -p ./ -n name -t etc/templates/ -c examples/conf.apache24.yml | grep -v '__' - # [REVERSE] Check for template generation errors - ./bin/vhost_gen.py -r http://127.0.0.1:3000 -l / -n name -t etc/templates/ | grep -v '__' - ./bin/vhost_gen.py -r http://127.0.0.1:3000 -l / -n name -t etc/templates/ -c etc/conf.yml | grep -v '__' - ./bin/vhost_gen.py -r http://127.0.0.1:3000 -l / -n name -t etc/templates/ -c examples/conf.nginx.yml | grep -v '__' - ./bin/vhost_gen.py -r http://127.0.0.1:3000 -l / -n name -t etc/templates/ -c examples/conf.apache22.yml | grep -v '__' - ./bin/vhost_gen.py -r http://127.0.0.1:3000 -l / -n name -t etc/templates/ -c examples/conf.apache24.yml | grep -v '__' +# ------------------------------------------------------------------------------------------------- +# Test Targets +# ------------------------------------------------------------------------------------------------- + +test: + @$(MAKE) --no-print-directory _test FILE=check-errors-normal.sh + @$(MAKE) --no-print-directory _test FILE=check-errors-reverse.sh + @$(MAKE) --no-print-directory _test FILE=check-errors-template-normal.sh + @$(MAKE) --no-print-directory _test FILE=check-errors-template-reverse.sh + + +_test: + @echo "--------------------------------------------------------------------------------" + @echo " Test $(FILE)" + @echo "--------------------------------------------------------------------------------" + docker run \ + --rm \ + $$(tty -s && echo "-it" || echo) \ + -v $(PWD):/data \ + -w /data \ + python:$(VERSION)-alpine \ + sh -c "pip install -r requirements.txt \ + && apk add bash make \ + && make install \ + && tests/$(FILE)" + + +# ------------------------------------------------------------------------------------------------- +# Build Targets +# ------------------------------------------------------------------------------------------------- + +dist: sdist bdist + +sdist: + docker run \ + --rm \ + $$(tty -s && echo "-it" || echo) \ + -v $(PWD):/data \ + -w /data \ + -u $$(id -u):$$(id -g) \ + python:$(VERSION)-alpine \ + python setup.py sdist + +bdist: + docker run \ + --rm \ + $$(tty -s && echo "-it" || echo) \ + -v $(PWD):/data \ + -w /data \ + -u $$(id -u):$$(id -g) \ + python:$(VERSION)-alpine \ + python setup.py bdist_wheel --universal + +build: + docker run \ + --rm \ + $$(tty -s && echo "-it" || echo) \ + -v $(PWD):/data \ + -w /data \ + -u $$(id -u):$$(id -g) \ + python:$(VERSION)-alpine \ + python setup.py build + +checkbuild: + docker run \ + --rm \ + $$(tty -s && echo "-it" || echo) \ + -v $(PWD):/data \ + -w /data \ + python:$(VERSION)-alpine \ + sh -c "pip install twine \ + && twine check dist/*" + + +# ------------------------------------------------------------------------------------------------- +# Publish Targets +# ------------------------------------------------------------------------------------------------- + +deploy: + docker run \ + --rm \ + $$(tty -s && echo "-it" || echo) \ + -v $(PWD):/data \ + -w /data \ + python:$(VERSION)-alpine \ + sh -c "pip install twine \ + && twine upload dist/*" + + +# ------------------------------------------------------------------------------------------------- +# Misc Targets +# ------------------------------------------------------------------------------------------------- + +autoformat: + docker run \ + --rm \ + $$(tty -s && echo "-it" || echo) \ + -v $(PWD):/data \ + -w /data \ + cytopia/black -l 100 $(BINPATH)$(BINNAME) +clean: + -rm -rf $(BINNAME).egg-info/ + -rm -rf dist/ + -rm -rf build/ install: @echo "Installing files" @echo "" - @# Create directories - ${MKDIR_P} /etc/vhost-gen - ${MKDIR_P} /etc/vhost-gen/templates - + mkdir -p /etc/vhost-gen + mkdir -p /etc/vhost-gen/templates @# Install binary - install -m 0755 bin/${BINARY} /usr/bin/${BINARY} - + install -m 0755 $(BINPATH)/$(BINNAME) /usr/bin/$(BINNAME) @# Install configs - install -m 0644 etc/${CONFIG} /etc/vhost-gen/${CONFIG} - install -m 0644 etc/${TPLDIR}/*.yml /etc/vhost-gen/${TPLDIR} - + install -m 0644 etc/$(CONFIG) /etc/vhost-gen/$(CONFIG) + install -m 0644 etc/$(TPLDIR)/*.yml /etc/vhost-gen/$(TPLDIR) @echo "Installation complete:" @echo "----------------------------------------------------------------------" @echo "" - uninstall: @echo "Removing files" @echo "" - rm -r /etc/vhost-gen - rm /usr/bin/${BINARY} - - + rm /usr/bin/$(BINNAME) @echo "Uninstallation complete:" @echo "----------------------------------------------------------------------" @echo "" diff --git a/README.md b/README.md index 325cba8..ca956ee 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,60 @@ # vhost-gen -[![Build Status](https://travis-ci.org/devilbox/vhost-gen.svg?branch=master)](https://travis-ci.org/devilbox/vhost-gen) ![Version](https://img.shields.io/github/tag/devilbox/vhost-gen.svg) +[![](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) +[![PyPI](https://img.shields.io/pypi/v/vhost-gen)](https://pypi.org/project/vhost-gen/) +[![PyPI - Status](https://img.shields.io/pypi/status/vhost-gen)](https://pypi.org/project/vhost-gen/) +[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/vhost-gen)](https://pypi.org/project/vhost-gen/) +[![PyPI - Format](https://img.shields.io/pypi/format/vhost-gen)](https://pypi.org/project/vhost-gen/) +[![PyPI - Implementation](https://img.shields.io/pypi/implementation/vhost-gen)](https://pypi.org/project/vhost-gen/) +[![PyPI - License](https://img.shields.io/pypi/l/vhost-gen)](https://pypi.org/project/vhost-gen/) -**[vhost_gen.py](bin/vhost_gen.py)** will dynamically generate **vhost** or **reverse proxy** configuration files for Apache 2.2, Apache 2.4 and Nginx depending on what you have set in [conf.yml](etc/conf.yml). This makes it easy to switch between different web servers while keeping the exact same functionality. +[![Build Status](https://github.com/devilbox/vhost-gen/workflows/linting/badge.svg)](https://github.com/devilbox/vhost-gen/actions?workflow=linting) +[![Build Status](https://github.com/devilbox/vhost-gen/workflows/testing/badge.svg)](https://github.com/devilbox/vhost-gen/actions?workflow=testing) +[![Build Status](https://travis-ci.org/devilbox/vhost-gen.svg?branch=master)](https://travis-ci.org/devilbox/vhost-gen) + +**[vhost-gen](bin/vhost-gen)** will dynamically generate **vhost** or **reverse proxy** configuration files for Apache 2.2, Apache 2.4 and Nginx depending on what you have set in [conf.yml](etc/conf.yml). This makes it easy to switch between different web servers while keeping the exact same functionality. --- +## Installation + +#### Via pip +```bash +pip install vhost-gen +``` + +#### From git +```bash +git clone https://github.com/devilbox/vhost-gen +cd vhost-gen +sudo make install +``` + + ## What is all the fuzz? -Imagine you have to create virtual hosts for your web server over and over again. The only things that might change are document root, log files and server names and possibly some other minor changes. Instead of having to copy and adjust the server's vhost config file each time, you can use `vhost_gen.py` to generate them for you. By supporting different web server versions, it makes it also easy for you to switch back and forth between Apache 2.2, Apache 2.4 and Nginx. +Imagine you have to create virtual hosts for your web server over and over again. The only things that might change are document root, log files and server names and possibly some other minor changes. Instead of having to copy and adjust the server's vhost config file each time, you can use `vhost-gen` to generate them for you. By supporting different web server versions, it makes it also easy for you to switch back and forth between Apache 2.2, Apache 2.4 and Nginx. -```shell +```bash # vHost -$ vhost_gen.py -p /shared/httpd/www.example.com -n www.example.com +$ vhost-gen -p /shared/httpd/www.example.com -n www.example.com # Reverse Proxy -$ vhost_gen.py -r http://127.0.0.1:8080 -l / -n api.example.com +$ vhost-gen -r http://127.0.0.1:8080 -l / -n api.example.com ``` -**`vhost_gen.py`** alone simply creates a new virtual host every time you execute it. The goal however is to also automate the execution of the vhost generator itself. +**`vhost-gen`** alone simply creates a new virtual host every time you execute it. The goal however is to also automate the execution of the vhost generator itself. #### 1. Reverse Proxy automation: [watcherp](https://github.com/devilbox/watcherp) Here enters **[watcherp](https://github.com/devilbox/watcherp)** the game. **[watcherp](https://github.com/devilbox/watcherp)** listens for changes of port bindings and triggers a command whenever a new port has been bound or a binding has been removed. By combining these two tools, you could automate the creating of reverse proxies with one command: -```shell +```bash # %n will be replaced by watcherp with the address a port has binded # %p will be replaced by watcherp with the the actual port number that started binding # -p argument from watcherp specifies ports to ignore for changes $ watcherp -v \ -p 80,443 \ - -a "vhost_gen.py -r 'http://%n:%p' -l '/' -n '%n.example.com' -s" \ + -a "vhost-gen -r 'http://%n:%p' -l '/' -n '%n.example.com' -s" \ -d "rm /etc/nginx/conf.d/%n.example.com.conf" \ -t "nginx -s reload" ``` @@ -38,25 +63,25 @@ $ watcherp -v \ Here enters **[watcherd](https://github.com/devilbox/watcherd)** the game. **[watcherd](https://github.com/devilbox/watcherd)** listens for directory changes and triggers a command whenever a directory has been created or deleted. By combining these two tools, you could automate mass virtual hosting with one command: -```shell +```bash # %n will be replaced by watcherd with the new directory name # %p will be replaced by watcherd with the new directory path $ watcherd -v \ -p /shared/httpd \ - -a "vhost_gen.py -p %p -n %n -s" \ + -a "vhost-gen -p %p -n %n -s" \ -d "rm /etc/nginx/conf.d/%n.conf" \ -t "nginx -s reload" ``` ##### More customization -Now it might look much more interesting. With the above command every vhost will have the exact same definition (except server name, document root and log file names). It is however also possible that every vhost could be customized depending on their needs. **`vhost_gen.py`** allows for additional overwriting its template. So inside each newly created folder you could have a sub-directory (e.g. `templates/`) with folder specific defines. Those custom templates would only be sourced if they exist: +Now it might look much more interesting. With the above command every vhost will have the exact same definition (except server name, document root and log file names). It is however also possible that every vhost could be customized depending on their needs. **`vhost-gen`** allows for additional overwriting its template. So inside each newly created folder you could have a sub-directory (e.g. `templates/`) with folder specific defines. Those custom templates would only be sourced if they exist: -```shell +```bash # Note: Adding -o %p/templates $ watcherd -v \ -p /shared/httpd \ - -a "vhost_gen.py -p %p -n %n -o %p/templates -s" \ + -a "vhost-gen -p %p -n %n -o %p/templates -s" \ -d "rm /etc/nginx/conf.d/%n.conf" \ -t "nginx -s reload" ``` @@ -66,7 +91,7 @@ $ watcherd -v \ If you don't trust the stability of **[watcherd](https://github.com/devilbox/watcherd)** or want other means of controlling this daemon, you can utilize **[supervisord](http://supervisord.org/)**: ```ini [program:watcherd] -command=watcherd -v -p /shared/httpd -a "vhost_gen.py -p %%p -n %%n -s" -d "rm /etc/nginx/custom.d/%%n.conf" -t "nginx -s reload" +command=watcherd -v -p /shared/httpd -a "vhost-gen -p %%p -n %%n -s" -d "rm /etc/nginx/custom.d/%%n.conf" -t "nginx -s reload" startsecs = 0 autorestart = true stdout_logfile=/dev/stdout @@ -79,7 +104,7 @@ stderr_events_enabled=true #### 3. Dockerizing -If you don't want to implement it yourself, there are already four fully functional dockerized containers available that offer automated mass virtual hosting based on `vhost_gen.py` and `watcherd`: +If you don't want to implement it yourself, there are already four fully functional dockerized containers available that offer automated mass virtual hosting based on `vhost-gen` and `watcherd`: | Base Image | Web server | Repository | |------------|------------|------------| @@ -128,24 +153,12 @@ If you are not satisfied with the default definitions for the webserver configur **The following describes the program flow:** -1. [vhost_gen.py](bin/vhost_gen.py) will read /etc/vhost-gen/conf.yml to get defines and webserver type/version +1. [vhost-gen](bin/vhost-gen) will read /etc/vhost-gen/conf.yml to get defines and webserver type/version 2. Base on the webserver version/type, it will read etc/templates/.yml template 3. Variables in the chosen template are replaced 4. The vHost name (`-n`) is also placed into the template 5. Template is written to webserver's config location (defined in /etc/vhost-gen/conf.yml) -#### Installation - -The Makefile will simply copy everything to their correct location. -```shell -$ sudo make install -``` - -To uninstall type: -```shell -$ sudo make uninstall -``` - ## Usage @@ -214,12 +227,12 @@ If you are not satisfied with the `Allow from all` permissions, simply rewrite t #### Available command line options -```shell -Usage: vhost_gen.py -p|r -n [-l -m -c -t -o -d -s -v] - vhost_gen.py --help - vhost_gen.py --version +```bash +Usage: vhost-gen -p|r -n [-l -m -c -t -o -d -s -v] + vhost-gen --help + vhost-gen --version -vhost_gen.py will dynamically generate vhost configuration files +vhost-gen will dynamically generate vhost configuration files for Nginx, Apache 2.2 or Apache 2.4 depending on what you have set in /etc/vhot-gen/conf.yml @@ -267,6 +280,7 @@ Misc arguments: --version Show version. ``` + ## Contributing This is an open source project and done in spare time. If you want to help out you are warmly welcome. You could do one or more of the following things: @@ -275,3 +289,10 @@ This is an open source project and done in spare time. If you want to help out y * Submit template examples * Create templates for other web servers * Whatever else you can imagine of + + +## License + +**[MIT License](LICENSE.md)** + +Copyright (c) 2017 [cytopia](https://github.com/cytopia) diff --git a/bin/vhost-gen b/bin/vhost-gen new file mode 100755 index 0000000..ae1bcba --- /dev/null +++ b/bin/vhost-gen @@ -0,0 +1,973 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# The MIT License (MIT) +# +# Copyright (c) 2017 cytopia + +""" +vHost creator for Apache 2.2, Apache 2.4 and Nginx. +""" + +############################################################ +# Imports +############################################################ + +from __future__ import print_function +import os +import sys +import time +import re +import getopt +import itertools +import yaml + + +############################################################ +# Globals +############################################################ + +# Default paths +CONFIG_PATH = "/etc/vhost-gen/conf.yml" +TEMPLATE_DIR = "/etc/vhost-gen/templates" + +# stdout/stderr log paths +STDOUT_ACCESS = "/tmp/www-access.log" +STDERR_ERROR = "/tmp/www-error.log" + +# Default configuration +DEFAULT_CONFIG = { + "server": "nginx", + "conf_dir": "/etc/nginx/conf.d", + "custom": "", + "vhost": { + "port": "80", + "ssl_port": "443", + "name": {"prefix": "", "suffix": ""}, + "docroot": {"suffix": ""}, + "index": ["index.php", "index.html", "index.htm"], + "ssl": { + "http2": True, + "dir_crt": "", + "dir_key": "", + "honor_cipher_order": "on", + "ciphers": "HIGH:!aNULL:!MD5", + "protocols": "TLSv1 TLSv1.1 TLSv1.2", + }, + "log": { + "access": {"prefix": "", "stdout": False}, + "error": {"prefix": "", "stderr": False}, + "dir": {"create": False, "path": "/var/log/nginx"}, + }, + "php_fpm": {"enable": False, "address": "", "port": 9000, "timeout": 180}, + "alias": [], + "deny": [], + "server_status": {"enable": False, "alias": "/server-status"}, + }, +} + +# Available templates +TEMPLATES = {"apache22": "apache22.yml", "apache24": "apache24.yml", "nginx": "nginx.yml"} + + +############################################################ +# System Functions +############################################################ + + +def print_help(): + """Show program help.""" + print( + """ +Usage: vhost-gen -p|r -n [-l -c -t -o -d -s -v] + vhost-gen --help + vhost-gen --version + +vhost-gen will dynamically generate vhost configuration files +for Nginx, Apache 2.2 or Apache 2.4 depending on what you have set +in /etc/vhot-gen/conf.yml + +Required arguments: + -p|r You need to choose one of the mutually exclusive arguments. + -p: Path to document root/ + -r: http(s)://Host:Port for reverse proxy. + Depening on the choice, it will either generate a document serving + vhost or a reverse proxy vhost. + Note, when using -p, this can also have a suffix directory to be set + in conf.yml + -l Location path when using reverse proxy. + Note, this is not required for normal document root server (-p) + -n Name of vhost + Note, this can also have a prefix and/or suffix to be set in conf.yml + +Optional arguments: + -m Vhost generation mode. Possible values are: + -m plain: Only generate http version (default) + -m ssl: Only generate https version + -m both: Generate http and https version + -m redir: Generate https version and make http redirect to https + -c Path to global configuration file. + If not set, the default location is /etc/vhost-gen/conf.yml + If no config is found, a default is used with all features turned off. + -t Path to global vhost template directory. + If not set, the default location is /etc/vhost-gen/templates/ + If vhost template files are not found in this directory, the program will + abort. + -o Path to local vhost template directory. + This is used as a secondary template directory and definitions found here + will be merged with the ones found in the global template directory. + Note, definitions in local vhost teplate directory take precedence over + the ones found in the global template directory. + -d Make this vhost the default virtual host. + Note, this will also change the server_name directive of nginx to '_' + as well as discarding any prefix or suffix specified for the name. + Apache does not have any specialities, the first vhost takes precedence. + -s If specified, the generated vhost will be saved in the location found in + conf.yml. If not specified, vhost will be printed to stdout. + -v Be verbose. + +Misc arguments: + --help Show this help. + --version Show version. +""" + ) + + +def print_version(): + """Show program version.""" + print("vhost-gen v1.0.0 (2020-01-23)") + print("cytopia ") + print("https://github.com/devilbox/vhost-gen") + print("The MIT License (MIT)") + + +############################################################ +# Wrapper Functions +############################################################ + + +def str_replace(string, replacer): + """Generic string replace.""" + + # Replace all 'keys' with 'values' + for key, val in replacer.items(): + string = string.replace(key, val) + + return string + + +def str_indent(text, amount, char=" "): + """Indent every newline inside str by specified value.""" + padding = amount * char + return "".join(padding + line for line in text.splitlines(True)) + + +def to_str(string): + """Dummy string retriever.""" + if string is None: + return "" + return str(string) + + +def load_yaml(path): + """Wrapper to load yaml file safely.""" + + try: + with open(path, "r") as stream: + try: + data = yaml.safe_load(stream) + if data is None: + data = dict() + return (True, data, "") + except yaml.YAMLError as err: + return (False, dict(), str(err)) + except IOError: + return (False, dict(), "File does not exist: " + path) + + +def merge_yaml(yaml1, yaml2): + """Merge two yaml strings. The secondary takes precedence.""" + return dict(itertools.chain(yaml1.items(), yaml2.items())) + + +def symlink(src, dst, force=False): + """ + Wrapper function to create a symlink with the addition of + being able to overwrite an already existing file. + """ + + if os.path.isdir(dst): + return (False, "[ERR] destination is a directory: " + dst) + + if force and os.path.exists(dst): + try: + os.remove(dst) + except OSError as err: + return (False, "[ERR] Cannot delete: " + dst + ": " + str(err)) + + try: + os.symlink(src, dst) + except OSError as err: + return (False, "[ERR] Cannot create link: " + str(err)) + + return (True, None) + + +############################################################ +# Argument Functions +############################################################ + + +def parse_args(argv): + """Parse command line arguments.""" + + # Config location, can be overwritten with -c + l_config_path = CONFIG_PATH + l_template_dir = TEMPLATE_DIR + o_template_dir = None + save = None + path = None + name = None + proxy = None + mode = None + location = None + default = False + verbose = False + + # Define command line options + try: + opts, argv = getopt.getopt(argv, "vm:c:p:r:l:n:t:o:ds", ["version", "help"]) + except getopt.GetoptError as err: + print("[ERR]", str(err), file=sys.stderr) + print("Type --help for help", file=sys.stderr) + sys.exit(2) + + # Get command line options + for opt, arg in opts: + if opt == "--version": + print_version() + sys.exit() + elif opt == "--help": + print_help() + sys.exit() + # Verbose + elif opt == "-v": + verbose = True + # Config file overwrite + elif opt == "-c": + l_config_path = arg + # Vhost document root path + elif opt == "-p": + path = arg + # Vhost reverse proxy (ADDR:PORT) + elif opt == "-r": + proxy = arg + # Mode overwrite + elif opt == "-m": + mode = arg + # Location for reverse proxy + elif opt == "-l": + location = arg + # Vhost name + elif opt == "-n": + name = arg + # Global template dir + elif opt == "-t": + l_template_dir = arg + # Local template dir + elif opt == "-o": + o_template_dir = arg + # Save? + elif opt == "-d": + default = True + elif opt == "-s": + save = True + + return ( + l_config_path, + l_template_dir, + o_template_dir, + path, + proxy, + mode, + location, + name, + default, + save, + verbose, + ) + + +def validate_args_req(name, docroot, proxy, mode, location): + """Validate required arguments.""" + # Validate required command line options are set + if docroot is None and proxy is None: + print("[ERR] -p or -r is required", file=sys.stderr) + print("Type --help for help", file=sys.stderr) + sys.exit(1) + if docroot is not None and proxy is not None: + print("[ERR] -p and -r are mutually exclusive", file=sys.stderr) + print("Type --help for help", file=sys.stderr) + sys.exit(1) + + # Check proxy string + if proxy is not None: + if location is None: + print("[ERR] When specifying -r, -l is also required.", file=sys.stderr) + sys.exit(1) + + # Regex: HOSTNAME/IP:PORT + regex = re.compile("(^http(s)?://[-_.a-zA-Z0-9]+:[0-9]+$)", re.IGNORECASE) + if not regex.match(proxy): + print( + "[ERR] Invalid proxy argument string: '%s', should be: %s or %s." + % (proxy, "http(s)://HOST:PORT", "http(s)://IP:PORT"), + file=sys.stderr, + ) + sys.exit(1) + + port = int(re.sub("^.*:", "", proxy)) + if port < 1 or port > 65535: + print( + "[ERR] Invalid reverse proxy port range: '%d', should between 1 and 65535" % (port), + file=sys.stderr, + ) + sys.exit(1) + + # Check mode string + if mode is not None: + if mode not in ("plain", "ssl", "both", "redir"): + print( + "[ERR] Invalid -m mode string: '%s', should be: %s, %s, %s or %s" + % (mode, "plain", "ssl", "both", "redir"), + file=sys.stderr, + ) + sys.exit(1) + + # Check normal server settings + if docroot is not None: + if location is not None: + print("[WARN] -l is ignored when using normal vhost (-p)", file=sys.stderr) + + if name is None: + print("[ERR] -n is required", file=sys.stderr) + print("Type --help for help", file=sys.stderr) + sys.exit(1) + + regex = re.compile("(^[-_.a-zA-Z0-9]+$)", re.IGNORECASE) + if not regex.match(name): + print("[ERR] Invalid name:", name, file=sys.stderr) + sys.exit(1) + + +def validate_args_opt(config_path, tpl_dir): + """Validate optional arguments.""" + + if not os.path.isfile(config_path): + print("[WARN] Config file not found:", config_path, file=sys.stderr) + + if not os.path.isdir(tpl_dir): + print("[ERR] Template path does not exist:", tpl_dir, file=sys.stderr) + print("Type --help for help", file=sys.stderr) + sys.exit(1) + + # Validate global templates + tpl_file = os.path.join(tpl_dir, TEMPLATES["apache22"]) + if not os.path.isfile(tpl_file): + print("[ERR] Apache 2.2 template file does not exist:", tpl_file, file=sys.stderr) + print("Type --help for help", file=sys.stderr) + sys.exit(1) + + tpl_file = os.path.join(tpl_dir, TEMPLATES["apache24"]) + if not os.path.isfile(tpl_file): + print("[ERR] Apache 2.4 template file does not exist:", tpl_file, file=sys.stderr) + print("Type --help for help", file=sys.stderr) + sys.exit(1) + + tpl_file = os.path.join(tpl_dir, TEMPLATES["nginx"]) + if not os.path.isfile(tpl_file): + print("[ERR] Nginx template file does not exist:", tpl_file, file=sys.stderr) + print("Type --help for help", file=sys.stderr) + sys.exit(1) + + +############################################################ +# Config File Functions +############################################################ + + +def validate_config(config): + """Validate some important keys in config dict.""" + + # Validate server type + valid_hosts = list(TEMPLATES.keys()) + if config["server"] not in valid_hosts: + print("[ERR] httpd.server must be 'apache22', 'apache24' or 'nginx'", file=sys.stderr) + print("[ERR] Your configuration is:", config["server"], file=sys.stderr) + sys.exit(1) + + +# # Validate if log dir can be created +# log_dir = config['vhost']['log']['dir']['path'] +# if config['vhost']['log']['dir']['create']: +# if not os.path.isdir(log_dir): +# if not os.access(os.path.dirname(log_dir), os.W_OK): +# print('[ERR] log directory does not exist and cannot be created:', log_dir, +# file=sys.stderr) +# sys.exit(1) + + +############################################################ +# Get vHost Skeleton placeholders +############################################################ + + +def vhost_get_port(config, ssl): + """Get listen port.""" + if ssl: + if config["server"] == "nginx": + return to_str(config["vhost"]["ssl_port"]) + " ssl" + return to_str(config["vhost"]["ssl_port"]) + + return to_str(config["vhost"]["port"]) + + +def vhost_get_http_proto(config, ssl): + """Get HTTP protocol. Only relevant for Apache 2.4/Nginx and SSL.""" + + if config["server"] == "apache24": + if ssl and config["vhost"]["ssl"]["http2"]: + return "h2 http/1.1" + return "http/1.1" + + if config["server"] == "nginx": + if ssl and config["vhost"]["ssl"]["http2"]: + return " http2" + + return "" + + +def vhost_get_default_server(config, default): + """ + Get vhost default directive which makes it the default vhost. + + :param dict config: Configuration dictionary + :param bool default: Default vhost + """ + if default: + if config["server"] == "nginx": + # The leading space is required here for the template to + # separate it from the port directive left to it. + return " default_server" + + if config["server"] in ("apache22", "apache24"): + return "_default_" + + else: + if config["server"] in ("apache22", "apache24"): + return "*" + + return "" + + +def vhost_get_server_name(config, server_name, default): + """Get server name.""" + + # Nginx uses: "server_name _;" as the default + if default and config["server"] == "nginx": + return "_" + + # Apache does not have any specialities. The first one takes precedence. + # The name will be the same as with every other vhost. + prefix = to_str(config["vhost"]["name"]["prefix"]) + suffix = to_str(config["vhost"]["name"]["suffix"]) + return prefix + server_name + suffix + + +def vhost_get_access_log(config, server_name): + """Get access log directive.""" + if config["vhost"]["log"]["access"]["stdout"]: + return STDOUT_ACCESS + + prefix = to_str(config["vhost"]["log"]["access"]["prefix"]) + name = prefix + server_name + "-access.log" + path = os.path.join(config["vhost"]["log"]["dir"]["path"], name) + return path + + +def vhost_get_error_log(config, server_name): + """Get error log directive.""" + if config["vhost"]["log"]["error"]["stderr"]: + return STDERR_ERROR + + prefix = to_str(config["vhost"]["log"]["error"]["prefix"]) + name = prefix + server_name + "-error.log" + path = os.path.join(config["vhost"]["log"]["dir"]["path"], name) + return path + + +############################################################ +# Get vHost Type (normal or reverse proxy +############################################################ + + +def vhost_get_vhost_docroot(config, template, docroot, proxy): + """Get document root directive.""" + if proxy is not None: + return "" + + return str_replace( + template["vhost_type"]["docroot"], + { + "__DOCUMENT_ROOT__": vhost_get_docroot_path(config, docroot, proxy), + "__INDEX__": vhost_get_index(config), + }, + ) + + +def vhost_get_vhost_rproxy(template, proxy, location): + """Get reverse proxy definition.""" + if proxy is not None: + return str_replace( + template["vhost_type"]["rproxy"], + { + "__LOCATION__": location, + "__PROXY_PROTO__": re.sub("://.*$", "", proxy), + "__PROXY_ADDR__": re.search("^.*://(.+):[0-9]+", proxy).group(1), + "__PROXY_PORT__": re.sub("^.*:", "", proxy), + }, + ) + return "" + + +############################################################ +# Get vHost Features +############################################################ + + +def vhost_get_vhost_ssl(config, template, server_name): + """Get ssl definition.""" + return str_replace( + template["features"]["ssl"], + { + "__SSL_PATH_CRT__": to_str(vhost_get_ssl_crt_path(config, server_name)), + "__SSL_PATH_KEY__": to_str(vhost_get_ssl_key_path(config, server_name)), + "__SSL_PROTOCOLS__": to_str(config["vhost"]["ssl"]["protocols"]), + "__SSL_HONOR_CIPHER_ORDER__": to_str(config["vhost"]["ssl"]["honor_cipher_order"]), + "__SSL_CIPHERS__": to_str(config["vhost"]["ssl"]["ciphers"]), + }, + ) + + +def vhost_get_vhost_redir(config, template, server_name): + """Get redirect to ssl definition.""" + return str_replace( + template["features"]["redirect"], + { + "__VHOST_NAME__": vhost_get_server_name(config, server_name, 0), + "__SSL_PORT__": to_str(config["vhost"]["ssl_port"]), + }, + ) + + +def vhost_get_ssl_crt_path(config, server_name): + """Get ssl crt path""" + + prefix = to_str(config["vhost"]["name"]["prefix"]) + suffix = to_str(config["vhost"]["name"]["suffix"]) + name = prefix + server_name + suffix + ".crt" + + path = to_str(config["vhost"]["ssl"]["dir_crt"]) + return os.path.join(path, name) + + +def vhost_get_ssl_key_path(config, server_name): + """Get ssl key path""" + prefix = to_str(config["vhost"]["name"]["prefix"]) + suffix = to_str(config["vhost"]["name"]["suffix"]) + name = prefix + server_name + suffix + ".key" + + path = to_str(config["vhost"]["ssl"]["dir_crt"]) + return os.path.join(path, name) + + +def vhost_get_docroot_path(config, docroot, proxy): + """Get path of document root.""" + if proxy is not None: + return "" + + suffix = to_str(config["vhost"]["docroot"]["suffix"]) + path = os.path.join(docroot, suffix) + return path + + +def vhost_get_index(config): + """Get index.""" + if "index" in config["vhost"] and config["vhost"]["index"]: + elem = config["vhost"]["index"] + else: + elem = DEFAULT_CONFIG["vhost"]["index"] + + return " ".join(elem) + + +def vhost_get_php_fpm(config, template, docroot, proxy): + """Get PHP FPM directive. If using reverse proxy, PHP-FPM will be disabled.""" + if proxy is not None: + return "" + + # Get PHP-FPM + php_fpm = "" + if config["vhost"]["php_fpm"]["enable"]: + php_fpm = str_replace( + template["features"]["php_fpm"], + { + "__PHP_ADDR__": to_str(config["vhost"]["php_fpm"]["address"]), + "__PHP_PORT__": to_str(config["vhost"]["php_fpm"]["port"]), + "__PHP_TIMEOUT__": to_str(config["vhost"]["php_fpm"]["timeout"]), + "__DOCUMENT_ROOT__": vhost_get_docroot_path(config, docroot, proxy), + }, + ) + return php_fpm + + +def vhost_get_aliases(config, template): + """Get virtual host alias directives.""" + aliases = [] + for item in config["vhost"]["alias"]: + # Add optional xdomain request if enabled + xdomain_request = "" + if "xdomain_request" in item: + if item["xdomain_request"]["enable"]: + xdomain_request = str_replace( + template["features"]["xdomain_request"], + {"__REGEX__": to_str(item["xdomain_request"]["origin"])}, + ) + # Replace everything + aliases.append( + str_replace( + template["features"]["alias"], + { + "__ALIAS__": to_str(item["alias"]), + "__PATH__": to_str(item["path"]), + "__XDOMAIN_REQ__": str_indent(xdomain_request, 4).rstrip(), + }, + ) + ) + # Join by OS independent newlines + return os.linesep.join(aliases) + + +def vhost_get_denies(config, template): + """Get virtual host deny alias directives.""" + denies = [] + for item in config["vhost"]["deny"]: + denies.append( + str_replace(template["features"]["deny"], {"__REGEX__": to_str(item["alias"])}) + ) + # Join by OS independent newlines + return os.linesep.join(denies) + + +def vhost_get_server_status(config, template): + """Get virtual host server status directivs.""" + status = "" + if config["vhost"]["server_status"]["enable"]: + status = template["features"]["server_status"] + + return str_replace(status, {"__REGEX__": to_str(config["vhost"]["server_status"]["alias"])}) + + +def vhost_get_custom_section(config): + """Get virtual host custom directives.""" + return to_str(config["custom"]) + + +############################################################ +# vHost create +############################################################ + + +def get_vhost_plain(config, tpl, docroot, proxy, location, server_name, default): + """Get plain vhost""" + return str_replace( + tpl["vhost"], + { + "__PORT__": vhost_get_port(config, False), + "__HTTP_PROTO__": vhost_get_http_proto(config, False), + "__DEFAULT_VHOST__": vhost_get_default_server(config, default), + "__DOCUMENT_ROOT__": vhost_get_docroot_path(config, docroot, proxy), + "__VHOST_NAME__": vhost_get_server_name(config, server_name, default), + "__VHOST_DOCROOT__": str_indent( + vhost_get_vhost_docroot(config, tpl, docroot, proxy), 4 + ), + "__VHOST_RPROXY__": str_indent(vhost_get_vhost_rproxy(tpl, proxy, location), 4), + "__REDIRECT__": "", + "__SSL__": "", + "__INDEX__": vhost_get_index(config), + "__ACCESS_LOG__": vhost_get_access_log(config, server_name), + "__ERROR_LOG__": vhost_get_error_log(config, server_name), + "__PHP_FPM__": str_indent(vhost_get_php_fpm(config, tpl, docroot, proxy), 4), + "__ALIASES__": str_indent(vhost_get_aliases(config, tpl), 4), + "__DENIES__": str_indent(vhost_get_denies(config, tpl), 4), + "__SERVER_STATUS__": str_indent(vhost_get_server_status(config, tpl), 4), + "__CUSTOM__": str_indent(vhost_get_custom_section(config), 4), + }, + ) + + +def get_vhost_ssl(config, tpl, docroot, proxy, location, server_name, default): + """Get ssl vhost""" + return str_replace( + tpl["vhost"], + { + "__PORT__": vhost_get_port(config, True), + "__HTTP_PROTO__": vhost_get_http_proto(config, True), + "__DEFAULT_VHOST__": vhost_get_default_server(config, default), + "__DOCUMENT_ROOT__": vhost_get_docroot_path(config, docroot, proxy), + "__VHOST_NAME__": vhost_get_server_name(config, server_name, default), + "__VHOST_DOCROOT__": str_indent( + vhost_get_vhost_docroot(config, tpl, docroot, proxy), 4 + ), + "__VHOST_RPROXY__": str_indent(vhost_get_vhost_rproxy(tpl, proxy, location), 4), + "__REDIRECT__": "", + "__SSL__": str_indent(vhost_get_vhost_ssl(config, tpl, server_name), 4), + "__INDEX__": vhost_get_index(config), + "__ACCESS_LOG__": vhost_get_access_log(config, server_name + "_ssl"), + "__ERROR_LOG__": vhost_get_error_log(config, server_name + "_ssl"), + "__PHP_FPM__": str_indent(vhost_get_php_fpm(config, tpl, docroot, proxy), 4), + "__ALIASES__": str_indent(vhost_get_aliases(config, tpl), 4), + "__DENIES__": str_indent(vhost_get_denies(config, tpl), 4), + "__SERVER_STATUS__": str_indent(vhost_get_server_status(config, tpl), 4), + "__CUSTOM__": str_indent(vhost_get_custom_section(config), 4), + }, + ) + + +def get_vhost_redir(config, tpl, docroot, proxy, server_name, default): + """Get redirect to ssl vhost""" + return str_replace( + tpl["vhost"], + { + "__PORT__": vhost_get_port(config, False), + "__HTTP_PROTO__": vhost_get_http_proto(config, False), + "__DEFAULT_VHOST__": vhost_get_default_server(config, default), + "__DOCUMENT_ROOT__": vhost_get_docroot_path(config, docroot, proxy), + "__VHOST_NAME__": vhost_get_server_name(config, server_name, default), + "__VHOST_DOCROOT__": "", + "__VHOST_RPROXY__": "", + "__REDIRECT__": str_indent(vhost_get_vhost_redir(config, tpl, server_name), 4), + "__SSL__": "", + "__INDEX__": "", + "__ACCESS_LOG__": vhost_get_access_log(config, server_name), + "__ERROR_LOG__": vhost_get_error_log(config, server_name), + "__PHP_FPM__": "", + "__ALIASES__": "", + "__DENIES__": "", + "__SERVER_STATUS__": "", + "__CUSTOM__": "", + }, + ) + + +def get_vhost(config, tpl, docroot, proxy, mode, location, server_name, default): + """Create the vhost.""" + + if mode == "ssl": + return get_vhost_ssl(config, tpl, docroot, proxy, location, server_name, default) + if mode == "both": + return get_vhost_ssl( + config, tpl, docroot, proxy, location, server_name, default + ) + get_vhost_plain(config, tpl, docroot, proxy, location, server_name, default) + + if mode == "redir": + return get_vhost_ssl( + config, tpl, docroot, proxy, location, server_name, default + ) + get_vhost_redir(config, tpl, docroot, proxy, server_name, default) + + return get_vhost_plain(config, tpl, docroot, proxy, location, server_name, default) + + +############################################################ +# Load configs and templates +############################################################ + + +def load_config(config_path): + """Load config and merge with defaults in case not found or something is missing.""" + + # Load configuration file + if os.path.isfile(config_path): + succ, config, err = load_yaml(config_path) + if not succ: + return (False, dict(), err) + else: + print("[WARN] config file not found", config_path, file=sys.stderr) + config = dict() + + # Merge config settings with program defaults (config takes precedence over defaults) + config = merge_yaml(DEFAULT_CONFIG, config) + + return (True, config, "") + + +def load_template(template_dir, o_template_dir, server): + """Load global and optional template file and merge them.""" + + # Load global template file + succ, template, err = load_yaml(os.path.join(template_dir, TEMPLATES[server])) + if not succ: + return ( + False, + dict(), + "(global template: " + os.path.join(template_dir, TEMPLATES[server]) + "): " + err, + ) + + # Load optional template file (if specified file and merge it) + if o_template_dir is not None: + # Template dir exists, but no file was added, give the user a warning, but do not abort + if not os.path.isfile(os.path.join(o_template_dir, TEMPLATES[server])): + print( + "[WARN] override template not found: ", + os.path.join(o_template_dir, TEMPLATES[server]), + file=sys.stderr, + ) + else: + succ, template2, err = load_yaml(os.path.join(o_template_dir, TEMPLATES[server])) + if not succ: + return ( + False, + dict(), + "(override template: " + + os.path.join(o_template_dir, TEMPLATES[server]) + + "): " + + err, + ) + template = merge_yaml(template, template2) + + return (True, template, "") + + +############################################################ +# Post actions +############################################################ + + +def apply_log_settings(config): + """ + This function will apply various settings for the log defines, including + creating the directory itself as well as handling log file output (access + and error) to stderr/stdout. + """ + # Symlink stdout to access logfile + if config["vhost"]["log"]["access"]["stdout"]: + succ, err = symlink("/dev/stdout", STDOUT_ACCESS, force=True) + if not succ: + return (False, err) + + # Symlink stderr to error logfile + if config["vhost"]["log"]["error"]["stderr"]: + succ, err = symlink("/dev/stderr", STDERR_ERROR, force=True) + if not succ: + return (False, err) + + # Create log dir + if config["vhost"]["log"]["dir"]["create"]: + if not os.path.isdir(config["vhost"]["log"]["dir"]["path"]): + try: + os.makedirs(config["vhost"]["log"]["dir"]["path"]) + except OSError as err: + return (False, "[ERR] Cannot create directory: " + str(err)) + + return (True, None) + + +############################################################ +# Main Function +############################################################ + + +def main(argv): + """Main entrypoint.""" + + # Get command line arguments + ( + config_path, + tpl_dir, + o_tpl_dir, + docroot, + proxy, + mode, + location, + name, + default, + save, + verbose, + ) = parse_args(argv) + + # Validate command line arguments This will abort the program on error + # This will abort the program on error + validate_args_req(name, docroot, proxy, mode, location) + validate_args_opt(config_path, tpl_dir) + + # Load config + succ, config, err = load_config(config_path) + if not succ: + print("[ERR] Error loading config", err, file=sys.stderr) + sys.exit(1) + + # Load template + succ, template, err = load_template(tpl_dir, o_tpl_dir, config["server"]) + if not succ: + print("[ERR] Error loading template", err, file=sys.stderr) + sys.exit(1) + + # Validate configuration file + # This will abort the program on error + validate_config(config) + + # Retrieve fully build vhost + vhost = get_vhost(config, template, docroot, proxy, mode, location, name, default) + + if verbose: + print( + "vhostgen: [%s] Adding: %s" + % ( + time.strftime("%Y-%m-%d %H:%M:%S"), + to_str(config["vhost"]["name"]["prefix"]) + + name + + to_str(config["vhost"]["name"]["suffix"]), + ) + ) + + if save: + if not os.path.isdir(config["conf_dir"]): + print("[ERR] output conf_dir does not exist:", config["conf_dir"], file=sys.stderr) + sys.exit(1) + if not os.access(config["conf_dir"], os.W_OK): + print( + "[ERR] directory does not have write permissions", + config["conf_dir"], + file=sys.stderr, + ) + sys.exit(1) + + vhost_path = os.path.join(config["conf_dir"], name + ".conf") + with open(vhost_path, "w") as outfile: + outfile.write(vhost) + + # Apply settings for logging (symlinks, mkdir) only in save mode + succ, err = apply_log_settings(config) + if not succ: + print(err, file=sys.stderr) + sys.exit(1) + else: + print(vhost) + + +############################################################ +# Main Entry Point +############################################################ + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/bin/vhost_gen.py b/bin/vhost_gen.py deleted file mode 100755 index b04bb0a..0000000 --- a/bin/vhost_gen.py +++ /dev/null @@ -1,927 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# -# The MIT License (MIT) -# -# Copyright (c) 2017 cytopia - -""" -vHost creator for Apache 2.2, Apache 2.4 and Nginx. -""" - -############################################################ -# Imports -############################################################ - -from __future__ import print_function -import os -import sys -import time -import re -import getopt -import itertools -import yaml - - -############################################################ -# Globals -############################################################ - -# Default paths -CONFIG_PATH = '/etc/vhost-gen/conf.yml' -TEMPLATE_DIR = '/etc/vhost-gen/templates' - -# stdout/stderr log paths -STDOUT_ACCESS = '/tmp/www-access.log' -STDERR_ERROR = '/tmp/www-error.log' - -# Default configuration -DEFAULT_CONFIG = { - 'server': 'nginx', - 'conf_dir': '/etc/nginx/conf.d', - 'custom': '', - 'vhost': { - 'port': '80', - 'ssl_port': '443', - 'name': { - 'prefix': '', - 'suffix': '' - }, - 'docroot': { - 'suffix': '' - }, - 'index': [ - 'index.php', - 'index.html', - 'index.htm' - ], - 'ssl': { - 'http2': True, - 'dir_crt': '', - 'dir_key': '', - 'honor_cipher_order': 'on', - 'ciphers': 'HIGH:!aNULL:!MD5', - 'protocols': 'TLSv1 TLSv1.1 TLSv1.2' - }, - 'log': { - 'access': { - 'prefix': '', - 'stdout': False - }, - 'error': { - 'prefix': '', - 'stderr': False - }, - 'dir': { - 'create': False, - 'path': '/var/log/nginx' - } - }, - 'php_fpm': { - 'enable': False, - 'address': '', - 'port': 9000, - 'timeout': 180 - }, - 'alias': [], - 'deny': [], - 'server_status': { - 'enable': False, - 'alias': '/server-status' - } - } -} - -# Available templates -TEMPLATES = { - 'apache22': 'apache22.yml', - 'apache24': 'apache24.yml', - 'nginx': 'nginx.yml' -} - - -############################################################ -# System Functions -############################################################ - -def print_help(): - """Show program help.""" - print(""" -Usage: vhost_gen.py -p|r -n [-l -c -t -o -d -s -v] - vhost_gen.py --help - vhost_gen.py --version - -vhost_gen.py will dynamically generate vhost configuration files -for Nginx, Apache 2.2 or Apache 2.4 depending on what you have set -in /etc/vhot-gen/conf.yml - -Required arguments: - -p|r You need to choose one of the mutually exclusive arguments. - -p: Path to document root/ - -r: http(s)://Host:Port for reverse proxy. - Depening on the choice, it will either generate a document serving - vhost or a reverse proxy vhost. - Note, when using -p, this can also have a suffix directory to be set - in conf.yml - -l Location path when using reverse proxy. - Note, this is not required for normal document root server (-p) - -n Name of vhost - Note, this can also have a prefix and/or suffix to be set in conf.yml - -Optional arguments: - -m Vhost generation mode. Possible values are: - -m plain: Only generate http version (default) - -m ssl: Only generate https version - -m both: Generate http and https version - -m redir: Generate https version and make http redirect to https - -c Path to global configuration file. - If not set, the default location is /etc/vhost-gen/conf.yml - If no config is found, a default is used with all features turned off. - -t Path to global vhost template directory. - If not set, the default location is /etc/vhost-gen/templates/ - If vhost template files are not found in this directory, the program will - abort. - -o Path to local vhost template directory. - This is used as a secondary template directory and definitions found here - will be merged with the ones found in the global template directory. - Note, definitions in local vhost teplate directory take precedence over - the ones found in the global template directory. - -d Make this vhost the default virtual host. - Note, this will also change the server_name directive of nginx to '_' - as well as discarding any prefix or suffix specified for the name. - Apache does not have any specialities, the first vhost takes precedence. - -s If specified, the generated vhost will be saved in the location found in - conf.yml. If not specified, vhost will be printed to stdout. - -v Be verbose. - -Misc arguments: - --help Show this help. - --version Show version. -""") - - -def print_version(): - """Show program version.""" - print('vhost_gen v0.16 (2019-12-31)') - print('cytopia ') - print('https://github.com/devilbox/vhost-gen') - print('The MIT License (MIT)') - - -############################################################ -# Wrapper Functions -############################################################ - -def str_replace(string, replacer): - """Generic string replace.""" - - # Replace all 'keys' with 'values' - for key, val in replacer.items(): - string = string.replace(key, val) - - return string - - -def str_indent(text, amount, char=' '): - """Indent every newline inside str by specified value.""" - padding = amount * char - return ''.join(padding+line for line in text.splitlines(True)) - - -def to_str(string): - """Dummy string retriever.""" - if string is None: - return '' - return str(string) - - -def load_yaml(path): - """Wrapper to load yaml file safely.""" - - try: - with open(path, 'r') as stream: - try: - data = yaml.safe_load(stream) - if data is None: - data = dict() - return (True, data, '') - except yaml.YAMLError as err: - return (False, dict(), str(err)) - except IOError: - return (False, dict(), 'File does not exist: '+path) - - -def merge_yaml(yaml1, yaml2): - """Merge two yaml strings. The secondary takes precedence.""" - return dict(itertools.chain(yaml1.items(), yaml2.items())) - - -def symlink(src, dst, force=False): - """ - Wrapper function to create a symlink with the addition of - being able to overwrite an already existing file. - """ - - if os.path.isdir(dst): - return (False, '[ERR] destination is a directory: '+dst) - - if force and os.path.exists(dst): - try: - os.remove(dst) - except OSError as err: - return (False, '[ERR] Cannot delete: '+dst+': '+str(err)) - - try: - os.symlink(src, dst) - except OSError as err: - return (False, '[ERR] Cannot create link: '+str(err)) - - return (True, None) - - -############################################################ -# Argument Functions -############################################################ - -def parse_args(argv): - """Parse command line arguments.""" - - # Config location, can be overwritten with -c - l_config_path = CONFIG_PATH - l_template_dir = TEMPLATE_DIR - o_template_dir = None - save = None - path = None - name = None - proxy = None - mode = None - location = None - default = False - verbose = False - - # Define command line options - try: - opts, argv = getopt.getopt(argv, 'vm:c:p:r:l:n:t:o:ds', ['version', 'help']) - except getopt.GetoptError as err: - print('[ERR]', str(err), file=sys.stderr) - print('Type --help for help', file=sys.stderr) - sys.exit(2) - - # Get command line options - for opt, arg in opts: - if opt == '--version': - print_version() - sys.exit() - elif opt == '--help': - print_help() - sys.exit() - # Verbose - elif opt == '-v': - verbose = True - # Config file overwrite - elif opt == '-c': - l_config_path = arg - # Vhost document root path - elif opt == '-p': - path = arg - # Vhost reverse proxy (ADDR:PORT) - elif opt == '-r': - proxy = arg - # Mode overwrite - elif opt == '-m': - mode = arg - # Location for reverse proxy - elif opt == '-l': - location = arg - # Vhost name - elif opt == '-n': - name = arg - # Global template dir - elif opt == '-t': - l_template_dir = arg - # Local template dir - elif opt == '-o': - o_template_dir = arg - # Save? - elif opt == '-d': - default = True - elif opt == '-s': - save = True - - return ( - l_config_path, l_template_dir, o_template_dir, path, proxy, mode, - location, name, default, save, verbose - ) - - -def validate_args_req(name, docroot, proxy, mode, location): - """Validate required arguments.""" - # Validate required command line options are set - if docroot is None and proxy is None: - print('[ERR] -p or -r is required', file=sys.stderr) - print('Type --help for help', file=sys.stderr) - sys.exit(1) - if docroot is not None and proxy is not None: - print('[ERR] -p and -r are mutually exclusive', file=sys.stderr) - print('Type --help for help', file=sys.stderr) - sys.exit(1) - - # Check proxy string - if proxy is not None: - if location is None: - print('[ERR] When specifying -r, -l is also required.', file=sys.stderr) - sys.exit(1) - - # Regex: HOSTNAME/IP:PORT - regex = re.compile('(^http(s)?://[-_.a-zA-Z0-9]+:[0-9]+$)', re.IGNORECASE) - if not regex.match(proxy): - print('[ERR] Invalid proxy argument string: \'%s\', should be: %s or %s.' - % (proxy, 'http(s)://HOST:PORT', 'http(s)://IP:PORT'), file=sys.stderr) - sys.exit(1) - - port = int(re.sub('^.*:', '', proxy)) - if port < 1 or port > 65535: - print('[ERR] Invalid reverse proxy port range: \'%d\', should between 1 and 65535' - % (port), file=sys.stderr) - sys.exit(1) - - # Check mode string - if mode is not None: - if mode not in ('plain', 'ssl', 'both', 'redir'): - print('[ERR] Invalid -m mode string: \'%s\', should be: %s, %s, %s or %s' - % (mode, 'plain', 'ssl', 'both', 'redir'), file=sys.stderr) - sys.exit(1) - - # Check normal server settings - if docroot is not None: - if location is not None: - print('[WARN] -l is ignored when using normal vhost (-p)', file=sys.stderr) - - if name is None: - print('[ERR] -n is required', file=sys.stderr) - print('Type --help for help', file=sys.stderr) - sys.exit(1) - - regex = re.compile('(^[-_.a-zA-Z0-9]+$)', re.IGNORECASE) - if not regex.match(name): - print('[ERR] Invalid name:', name, file=sys.stderr) - sys.exit(1) - - -def validate_args_opt(config_path, tpl_dir): - """Validate optional arguments.""" - - if not os.path.isfile(config_path): - print('[WARN] Config file not found:', config_path, file=sys.stderr) - - if not os.path.isdir(tpl_dir): - print('[ERR] Template path does not exist:', tpl_dir, file=sys.stderr) - print('Type --help for help', file=sys.stderr) - sys.exit(1) - - # Validate global templates - tpl_file = os.path.join(tpl_dir, TEMPLATES['apache22']) - if not os.path.isfile(tpl_file): - print('[ERR] Apache 2.2 template file does not exist:', tpl_file, file=sys.stderr) - print('Type --help for help', file=sys.stderr) - sys.exit(1) - - tpl_file = os.path.join(tpl_dir, TEMPLATES['apache24']) - if not os.path.isfile(tpl_file): - print('[ERR] Apache 2.4 template file does not exist:', tpl_file, file=sys.stderr) - print('Type --help for help', file=sys.stderr) - sys.exit(1) - - tpl_file = os.path.join(tpl_dir, TEMPLATES['nginx']) - if not os.path.isfile(tpl_file): - print('[ERR] Nginx template file does not exist:', tpl_file, file=sys.stderr) - print('Type --help for help', file=sys.stderr) - sys.exit(1) - - -############################################################ -# Config File Functions -############################################################ - -def validate_config(config): - """Validate some important keys in config dict.""" - - # Validate server type - valid_hosts = list(TEMPLATES.keys()) - if config['server'] not in valid_hosts: - print('[ERR] httpd.server must be \'apache22\', \'apache24\' or \'nginx\'', file=sys.stderr) - print('[ERR] Your configuration is:', config['server'], file=sys.stderr) - sys.exit(1) - -# # Validate if log dir can be created -# log_dir = config['vhost']['log']['dir']['path'] -# if config['vhost']['log']['dir']['create']: -# if not os.path.isdir(log_dir): -# if not os.access(os.path.dirname(log_dir), os.W_OK): -# print('[ERR] log directory does not exist and cannot be created:', log_dir, -# file=sys.stderr) -# sys.exit(1) - - -############################################################ -# Get vHost Skeleton placeholders -############################################################ - -def vhost_get_port(config, ssl): - """Get listen port.""" - if ssl: - if config['server'] == 'nginx': - return to_str(config['vhost']['ssl_port']) + ' ssl' - return to_str(config['vhost']['ssl_port']) - - return to_str(config['vhost']['port']) - - -def vhost_get_http_proto(config, ssl): - """Get HTTP protocol. Only relevant for Apache 2.4/Nginx and SSL.""" - - if config['server'] == 'apache24': - if ssl and config['vhost']['ssl']['http2']: - return 'h2 http/1.1' - return 'http/1.1' - - if config['server'] == 'nginx': - if ssl and config['vhost']['ssl']['http2']: - return ' http2' - - return '' - - -def vhost_get_default_server(config, default): - """ - Get vhost default directive which makes it the default vhost. - - :param dict config: Configuration dictionary - :param bool default: Default vhost - """ - if default: - if config['server'] == 'nginx': - # The leading space is required here for the template to - # separate it from the port directive left to it. - return ' default_server' - - if config['server'] in ('apache22', 'apache24'): - return '_default_' - - else: - if config['server'] in ('apache22', 'apache24'): - return '*' - - return '' - - -def vhost_get_server_name(config, server_name, default): - """Get server name.""" - - # Nginx uses: "server_name _;" as the default - if default and config['server'] == 'nginx': - return '_' - - # Apache does not have any specialities. The first one takes precedence. - # The name will be the same as with every other vhost. - prefix = to_str(config['vhost']['name']['prefix']) - suffix = to_str(config['vhost']['name']['suffix']) - return prefix + server_name + suffix - - -def vhost_get_access_log(config, server_name): - """Get access log directive.""" - if config['vhost']['log']['access']['stdout']: - return STDOUT_ACCESS - - prefix = to_str(config['vhost']['log']['access']['prefix']) - name = prefix + server_name + '-access.log' - path = os.path.join(config['vhost']['log']['dir']['path'], name) - return path - - -def vhost_get_error_log(config, server_name): - """Get error log directive.""" - if config['vhost']['log']['error']['stderr']: - return STDERR_ERROR - - prefix = to_str(config['vhost']['log']['error']['prefix']) - name = prefix + server_name + '-error.log' - path = os.path.join(config['vhost']['log']['dir']['path'], name) - return path - - -############################################################ -# Get vHost Type (normal or reverse proxy -############################################################ - -def vhost_get_vhost_docroot(config, template, docroot, proxy): - """Get document root directive.""" - if proxy is not None: - return '' - - return str_replace(template['vhost_type']['docroot'], { - '__DOCUMENT_ROOT__': vhost_get_docroot_path(config, docroot, proxy), - '__INDEX__': vhost_get_index(config) - }) - - -def vhost_get_vhost_rproxy(template, proxy, location): - """Get reverse proxy definition.""" - if proxy is not None: - return str_replace(template['vhost_type']['rproxy'], { - '__LOCATION__': location, - '__PROXY_PROTO__': re.sub('://.*$', '', proxy), - '__PROXY_ADDR__': re.search('^.*://(.+):[0-9]+', proxy).group(1), - '__PROXY_PORT__': re.sub('^.*:', '', proxy) - }) - return '' - - -############################################################ -# Get vHost Features -############################################################ - -def vhost_get_vhost_ssl(config, template, server_name): - """Get ssl definition.""" - return str_replace(template['features']['ssl'], { - '__SSL_PATH_CRT__': to_str(vhost_get_ssl_crt_path(config, server_name)), - '__SSL_PATH_KEY__': to_str(vhost_get_ssl_key_path(config, server_name)), - '__SSL_PROTOCOLS__': to_str(config['vhost']['ssl']['protocols']), - '__SSL_HONOR_CIPHER_ORDER__': to_str(config['vhost']['ssl']['honor_cipher_order']), - '__SSL_CIPHERS__': to_str(config['vhost']['ssl']['ciphers']) - }) - - -def vhost_get_vhost_redir(config, template, server_name): - """Get redirect to ssl definition.""" - return str_replace(template['features']['redirect'], { - '__VHOST_NAME__': vhost_get_server_name(config, server_name, 0), - '__SSL_PORT__': to_str(config['vhost']['ssl_port']) - }) - - -def vhost_get_ssl_crt_path(config, server_name): - """Get ssl crt path""" - - prefix = to_str(config['vhost']['name']['prefix']) - suffix = to_str(config['vhost']['name']['suffix']) - name = prefix + server_name + suffix + '.crt' - - path = to_str(config['vhost']['ssl']['dir_crt']) - return os.path.join(path, name) - - -def vhost_get_ssl_key_path(config, server_name): - """Get ssl key path""" - prefix = to_str(config['vhost']['name']['prefix']) - suffix = to_str(config['vhost']['name']['suffix']) - name = prefix + server_name + suffix + '.key' - - path = to_str(config['vhost']['ssl']['dir_crt']) - return os.path.join(path, name) - - -def vhost_get_docroot_path(config, docroot, proxy): - """Get path of document root.""" - if proxy is not None: - return '' - - suffix = to_str(config['vhost']['docroot']['suffix']) - path = os.path.join(docroot, suffix) - return path - - -def vhost_get_index(config): - """Get index.""" - if 'index' in config['vhost'] and config['vhost']['index']: - elem = config['vhost']['index'] - else: - elem = DEFAULT_CONFIG['vhost']['index'] - - return ' '.join(elem) - - -def vhost_get_php_fpm(config, template, docroot, proxy): - """Get PHP FPM directive. If using reverse proxy, PHP-FPM will be disabled.""" - if proxy is not None: - return '' - - # Get PHP-FPM - php_fpm = '' - if config['vhost']['php_fpm']['enable']: - php_fpm = str_replace(template['features']['php_fpm'], { - '__PHP_ADDR__': to_str(config['vhost']['php_fpm']['address']), - '__PHP_PORT__': to_str(config['vhost']['php_fpm']['port']), - '__PHP_TIMEOUT__': to_str(config['vhost']['php_fpm']['timeout']), - '__DOCUMENT_ROOT__': vhost_get_docroot_path(config, docroot, proxy) - }) - return php_fpm - - -def vhost_get_aliases(config, template): - """Get virtual host alias directives.""" - aliases = [] - for item in config['vhost']['alias']: - # Add optional xdomain request if enabled - xdomain_request = '' - if 'xdomain_request' in item: - if item['xdomain_request']['enable']: - xdomain_request = str_replace(template['features']['xdomain_request'], { - '__REGEX__': to_str(item['xdomain_request']['origin']) - }) - # Replace everything - aliases.append(str_replace(template['features']['alias'], { - '__ALIAS__': to_str(item['alias']), - '__PATH__': to_str(item['path']), - '__XDOMAIN_REQ__': str_indent(xdomain_request, 4).rstrip() - })) - # Join by OS independent newlines - return os.linesep.join(aliases) - - -def vhost_get_denies(config, template): - """Get virtual host deny alias directives.""" - denies = [] - for item in config['vhost']['deny']: - denies.append(str_replace(template['features']['deny'], { - '__REGEX__': to_str(item['alias']) - })) - # Join by OS independent newlines - return os.linesep.join(denies) - - -def vhost_get_server_status(config, template): - """Get virtual host server status directivs.""" - status = '' - if config['vhost']['server_status']['enable']: - status = template['features']['server_status'] - - return str_replace(status, { - '__REGEX__': to_str(config['vhost']['server_status']['alias']) - }) - - -def vhost_get_custom_section(config): - """Get virtual host custom directives.""" - return to_str(config['custom']) - - -############################################################ -# vHost create -############################################################ - -def get_vhost_plain(config, tpl, docroot, proxy, location, server_name, default): - """Get plain vhost""" - return str_replace(tpl['vhost'], { - '__PORT__': vhost_get_port(config, False), - '__HTTP_PROTO__': vhost_get_http_proto(config, False), - '__DEFAULT_VHOST__': vhost_get_default_server(config, default), - '__DOCUMENT_ROOT__': vhost_get_docroot_path(config, docroot, proxy), - '__VHOST_NAME__': vhost_get_server_name(config, server_name, default), - '__VHOST_DOCROOT__': str_indent(vhost_get_vhost_docroot(config, tpl, docroot, proxy), 4), - '__VHOST_RPROXY__': str_indent(vhost_get_vhost_rproxy(tpl, proxy, location), 4), - '__REDIRECT__': '', - '__SSL__': '', - '__INDEX__': vhost_get_index(config), - '__ACCESS_LOG__': vhost_get_access_log(config, server_name), - '__ERROR_LOG__': vhost_get_error_log(config, server_name), - '__PHP_FPM__': str_indent(vhost_get_php_fpm(config, tpl, docroot, proxy), 4), - '__ALIASES__': str_indent(vhost_get_aliases(config, tpl), 4), - '__DENIES__': str_indent(vhost_get_denies(config, tpl), 4), - '__SERVER_STATUS__': str_indent(vhost_get_server_status(config, tpl), 4), - '__CUSTOM__': str_indent(vhost_get_custom_section(config), 4) - }) - - -def get_vhost_ssl(config, tpl, docroot, proxy, location, server_name, default): - """Get ssl vhost""" - return str_replace(tpl['vhost'], { - '__PORT__': vhost_get_port(config, True), - '__HTTP_PROTO__': vhost_get_http_proto(config, True), - '__DEFAULT_VHOST__': vhost_get_default_server(config, default), - '__DOCUMENT_ROOT__': vhost_get_docroot_path(config, docroot, proxy), - '__VHOST_NAME__': vhost_get_server_name(config, server_name, default), - '__VHOST_DOCROOT__': str_indent(vhost_get_vhost_docroot(config, tpl, docroot, proxy), 4), - '__VHOST_RPROXY__': str_indent(vhost_get_vhost_rproxy(tpl, proxy, location), 4), - '__REDIRECT__': '', - '__SSL__': str_indent(vhost_get_vhost_ssl(config, tpl, server_name), 4), - '__INDEX__': vhost_get_index(config), - '__ACCESS_LOG__': vhost_get_access_log(config, server_name + '_ssl'), - '__ERROR_LOG__': vhost_get_error_log(config, server_name + '_ssl'), - '__PHP_FPM__': str_indent(vhost_get_php_fpm(config, tpl, docroot, proxy), 4), - '__ALIASES__': str_indent(vhost_get_aliases(config, tpl), 4), - '__DENIES__': str_indent(vhost_get_denies(config, tpl), 4), - '__SERVER_STATUS__': str_indent(vhost_get_server_status(config, tpl), 4), - '__CUSTOM__': str_indent(vhost_get_custom_section(config), 4) - }) - - -def get_vhost_redir(config, tpl, docroot, proxy, server_name, default): - """Get redirect to ssl vhost""" - return str_replace(tpl['vhost'], { - '__PORT__': vhost_get_port(config, False), - '__HTTP_PROTO__': vhost_get_http_proto(config, False), - '__DEFAULT_VHOST__': vhost_get_default_server(config, default), - '__DOCUMENT_ROOT__': vhost_get_docroot_path(config, docroot, proxy), - '__VHOST_NAME__': vhost_get_server_name(config, server_name, default), - '__VHOST_DOCROOT__': '', - '__VHOST_RPROXY__': '', - '__REDIRECT__': str_indent(vhost_get_vhost_redir(config, tpl, server_name), 4), - '__SSL__': '', - '__INDEX__': '', - '__ACCESS_LOG__': vhost_get_access_log(config, server_name), - '__ERROR_LOG__': vhost_get_error_log(config, server_name), - '__PHP_FPM__': '', - '__ALIASES__': '', - '__DENIES__': '', - '__SERVER_STATUS__': '', - '__CUSTOM__': '' - }) - - -def get_vhost(config, tpl, docroot, proxy, mode, location, server_name, default): - """Create the vhost.""" - - if mode == 'ssl': - return get_vhost_ssl(config, tpl, docroot, proxy, location, - server_name, default) - if mode == 'both': - return ( - get_vhost_ssl(config, tpl, docroot, proxy, location, - server_name, default) + - get_vhost_plain(config, tpl, docroot, proxy, location, - server_name, default) - ) - - if mode == 'redir': - return ( - get_vhost_ssl(config, tpl, docroot, proxy, location, - server_name, default) + - get_vhost_redir(config, tpl, docroot, proxy, server_name, default) - ) - - return get_vhost_plain(config, tpl, docroot, proxy, location, - server_name, default) - - -############################################################ -# Load configs and templates -############################################################ - - -def load_config(config_path): - """Load config and merge with defaults in case not found or something is missing.""" - - # Load configuration file - if os.path.isfile(config_path): - succ, config, err = load_yaml(config_path) - if not succ: - return (False, dict(), err) - else: - print('[WARN] config file not found', config_path, file=sys.stderr) - config = dict() - - # Merge config settings with program defaults (config takes precedence over defaults) - config = merge_yaml(DEFAULT_CONFIG, config) - - return (True, config, '') - - -def load_template(template_dir, o_template_dir, server): - """Load global and optional template file and merge them.""" - - # Load global template file - succ, template, err = load_yaml(os.path.join(template_dir, TEMPLATES[server])) - if not succ: - return ( - False, - dict(), - '(global template: '+os.path.join(template_dir, TEMPLATES[server])+'): ' + err - ) - - # Load optional template file (if specified file and merge it) - if o_template_dir is not None: - # Template dir exists, but no file was added, give the user a warning, but do not abort - if not os.path.isfile(os.path.join(o_template_dir, TEMPLATES[server])): - print( - '[WARN] override template not found: ', - os.path.join(o_template_dir, TEMPLATES[server]), - file=sys.stderr - ) - else: - succ, template2, err = load_yaml(os.path.join(o_template_dir, TEMPLATES[server])) - if not succ: - return ( - False, - dict(), - '(override template: '+os.path.join(o_template_dir, TEMPLATES[server])+'): '+err - ) - template = merge_yaml(template, template2) - - return (True, template, '') - - -############################################################ -# Post actions -############################################################ - -def apply_log_settings(config): - """ - This function will apply various settings for the log defines, including - creating the directory itself as well as handling log file output (access - and error) to stderr/stdout. - """ - # Symlink stdout to access logfile - if config['vhost']['log']['access']['stdout']: - succ, err = symlink('/dev/stdout', STDOUT_ACCESS, force=True) - if not succ: - return (False, err) - - # Symlink stderr to error logfile - if config['vhost']['log']['error']['stderr']: - succ, err = symlink('/dev/stderr', STDERR_ERROR, force=True) - if not succ: - return (False, err) - - # Create log dir - if config['vhost']['log']['dir']['create']: - if not os.path.isdir(config['vhost']['log']['dir']['path']): - try: - os.makedirs(config['vhost']['log']['dir']['path']) - except OSError as err: - return (False, '[ERR] Cannot create directory: '+str(err)) - - return (True, None) - - -############################################################ -# Main Function -############################################################ - -def main(argv): - """Main entrypoint.""" - - # Get command line arguments - (config_path, tpl_dir, o_tpl_dir, docroot, - proxy, mode, location, name, default, save, verbose) = parse_args(argv) - - # Validate command line arguments This will abort the program on error - # This will abort the program on error - validate_args_req(name, docroot, proxy, mode, location) - validate_args_opt(config_path, tpl_dir) - - # Load config - succ, config, err = load_config(config_path) - if not succ: - print('[ERR] Error loading config', err, file=sys.stderr) - sys.exit(1) - - # Load template - succ, template, err = load_template(tpl_dir, o_tpl_dir, config['server']) - if not succ: - print('[ERR] Error loading template', err, file=sys.stderr) - sys.exit(1) - - # Validate configuration file - # This will abort the program on error - validate_config(config) - - # Retrieve fully build vhost - vhost = get_vhost(config, template, docroot, proxy, mode, location, name, default) - - if verbose: - print('vhostgen: [%s] Adding: %s' % - (time.strftime("%Y-%m-%d %H:%M:%S"), - to_str(config['vhost']['name']['prefix']) + name + - to_str(config['vhost']['name']['suffix']))) - - if save: - if not os.path.isdir(config['conf_dir']): - print('[ERR] output conf_dir does not exist:', config['conf_dir'], - file=sys.stderr) - sys.exit(1) - if not os.access(config['conf_dir'], os.W_OK): - print('[ERR] directory does not have write permissions', config['conf_dir'], - file=sys.stderr) - sys.exit(1) - - vhost_path = os.path.join(config['conf_dir'], name+'.conf') - with open(vhost_path, 'w') as outfile: - outfile.write(vhost) - - # Apply settings for logging (symlinks, mkdir) only in save mode - succ, err = apply_log_settings(config) - if not succ: - print(err, file=sys.stderr) - sys.exit(1) - else: - print(vhost) - - -############################################################ -# Main Entry Point -############################################################ - -if __name__ == '__main__': - main(sys.argv[1:]) diff --git a/etc/conf.yml b/etc/conf.yml index d4c22ee..94f8b75 100644 --- a/etc/conf.yml +++ b/etc/conf.yml @@ -77,13 +77,13 @@ vhost: ssl_port: 443 # The virtual host name is specified as an command line argument - # to vhost_gen.py via '-n', however it is possible + # to vhost-gen via '-n', however it is possible # to prepend and/or append additional name strings. name: prefix: suffix: .loc # The document root directory is specified as an command line argument - # to vhost_gen.py via '-p', however it is possible + # to vhost-gen via '-p', however it is possible # to prepend another subdirectory here. docroot: suffix: htdocs diff --git a/examples/conf.apache22.yml b/examples/conf.apache22.yml index 7531e73..99c5c00 100644 --- a/examples/conf.apache22.yml +++ b/examples/conf.apache22.yml @@ -77,13 +77,13 @@ vhost: ssl_port: 443 # The virtual host name is specified as an command line argument - # to vhost_gen.py via '-n', however it is possible + # to vhost-gen via '-n', however it is possible # to prepend and/or append additional name strings. name: prefix: suffix: .loc # The document root directory is specified as an command line argument - # to vhost_gen.py via '-p', however it is possible + # to vhost-gen via '-p', however it is possible # to prepend another subdirectory here. docroot: suffix: htdocs diff --git a/examples/conf.apache24.yml b/examples/conf.apache24.yml index ba9d759..0426502 100644 --- a/examples/conf.apache24.yml +++ b/examples/conf.apache24.yml @@ -77,13 +77,13 @@ vhost: ssl_port: 443 # The virtual host name is specified as an command line argument - # to vhost_gen.py via '-n', however it is possible + # to vhost-gen via '-n', however it is possible # to prepend and/or append additional name strings. name: prefix: suffix: .loc # The document root directory is specified as an command line argument - # to vhost_gen.py via '-p', however it is possible + # to vhost-gen via '-p', however it is possible # to prepend another subdirectory here. docroot: suffix: htdocs diff --git a/examples/conf.nginx.yml b/examples/conf.nginx.yml index d4c22ee..94f8b75 100644 --- a/examples/conf.nginx.yml +++ b/examples/conf.nginx.yml @@ -77,13 +77,13 @@ vhost: ssl_port: 443 # The virtual host name is specified as an command line argument - # to vhost_gen.py via '-n', however it is possible + # to vhost-gen via '-n', however it is possible # to prepend and/or append additional name strings. name: prefix: suffix: .loc # The document root directory is specified as an command line argument - # to vhost_gen.py via '-p', however it is possible + # to vhost-gen via '-p', however it is possible # to prepend another subdirectory here. docroot: suffix: htdocs diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..4e2a1b6 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,8 @@ +[metadata] +description-file = README.md + +[bdist_wheel] +universal=1 + +[pycodestyle] +max-line-length = 100 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..dd36bdd --- /dev/null +++ b/setup.py @@ -0,0 +1,40 @@ +from setuptools import setup + +with open("README.md", "r") as fh: + long_description = fh.read() + +setup( + name="vhost-gen", + version="1.0.0", + description="Configurable vHost generator for Apache 2.2, Apache 2.4 and Nginx.", + license="MIT", + long_description=long_description, + long_description_content_type="text/markdown", + author="cytopia", + author_email="cytopia@everythingcli.org", + url="https://github.com/devilbox/vhost-gen", + install_requires=["pyyaml", "future"], + scripts=[ + "bin/vhost-gen" + ], + classifiers=[ + # How mature is this project? Common values are + # 3 - Alpha + # 4 - Beta + # 5 - Production/Stable + 'Development Status :: 5 - Production/Stable', + + # Indicate who your project is intended for + 'Intended Audience :: Developers', + 'Topic :: Software Development :: Build Tools', + + # License + "License :: OSI Approved :: MIT License", + + # Specify the Python versions you support here. In particular, ensure + # that you indicate whether you support Python 2, Python 3 or both. + "Programming Language :: Python :: 2", + "Programming Language :: Python :: 3", + "Operating System :: OS Independent", + ], + ) diff --git a/tests/check-errors-normal.sh b/tests/check-errors-normal.sh new file mode 100755 index 0000000..2e6a6fd --- /dev/null +++ b/tests/check-errors-normal.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +set -e +set -u +set -o pipefail + +GIT_PATH="$( cd "$(dirname "$0")/../" && pwd -P )" +BIN_PATH="${GIT_PATH}/bin" + +echo "------------------------------------------------------------" +echo "- Test: ${0}" +echo "------------------------------------------------------------" + +"${BIN_PATH}/vhost-gen" -p ./ -n name -t etc/templates/ >/dev/null +"${BIN_PATH}/vhost-gen" -p ./ -n name -t etc/templates/ -c etc/conf.yml >/dev/null +"${BIN_PATH}/vhost-gen" -p ./ -n name -t etc/templates/ -c examples/conf.nginx.yml >/dev/null +"${BIN_PATH}/vhost-gen" -p ./ -n name -t etc/templates/ -c examples/conf.apache22.yml >/dev/null +"${BIN_PATH}/vhost-gen" -p ./ -n name -t etc/templates/ -c examples/conf.apache24.yml >/dev/null diff --git a/tests/check-errors-reverse.sh b/tests/check-errors-reverse.sh new file mode 100755 index 0000000..55b5ea3 --- /dev/null +++ b/tests/check-errors-reverse.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +set -e +set -u +set -o pipefail + +GIT_PATH="$( cd "$(dirname "$0")/../" && pwd -P )" +BIN_PATH="${GIT_PATH}/bin" + +echo "------------------------------------------------------------" +echo "- Test: ${0}" +echo "------------------------------------------------------------" + +"${BIN_PATH}/vhost-gen" -r http://127.0.0.1:3000 -l / -n name -t etc/templates/ >/dev/null +"${BIN_PATH}/vhost-gen" -r http://127.0.0.1:3000 -l / -n name -t etc/templates/ -c etc/conf.yml >/dev/null +"${BIN_PATH}/vhost-gen" -r http://127.0.0.1:3000 -l / -n name -t etc/templates/ -c examples/conf.nginx.yml >/dev/null +"${BIN_PATH}/vhost-gen" -r http://127.0.0.1:3000 -l / -n name -t etc/templates/ -c examples/conf.apache22.yml >/dev/null +"${BIN_PATH}/vhost-gen" -r http://127.0.0.1:3000 -l / -n name -t etc/templates/ -c examples/conf.apache24.yml >/dev/null diff --git a/tests/check-errors-template-normal.sh b/tests/check-errors-template-normal.sh new file mode 100755 index 0000000..466309b --- /dev/null +++ b/tests/check-errors-template-normal.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +set -e +set -u +set -o pipefail + +GIT_PATH="$( cd "$(dirname "$0")/../" && pwd -P )" +BIN_PATH="${GIT_PATH}/bin" + +echo "------------------------------------------------------------" +echo "- Test: ${0}" +echo "------------------------------------------------------------" + +"${BIN_PATH}/vhost-gen" -p ./ -n name -t etc/templates/ | grep -v '__' +"${BIN_PATH}/vhost-gen" -p ./ -n name -t etc/templates/ -c etc/conf.yml | grep -v '__' +"${BIN_PATH}/vhost-gen" -p ./ -n name -t etc/templates/ -c examples/conf.nginx.yml | grep -v '__' +"${BIN_PATH}/vhost-gen" -p ./ -n name -t etc/templates/ -c examples/conf.apache22.yml | grep -v '__' +"${BIN_PATH}/vhost-gen" -p ./ -n name -t etc/templates/ -c examples/conf.apache24.yml | grep -v '__' diff --git a/tests/check-errors-template-reverse.sh b/tests/check-errors-template-reverse.sh new file mode 100755 index 0000000..a2d9df7 --- /dev/null +++ b/tests/check-errors-template-reverse.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +set -e +set -u +set -o pipefail + +GIT_PATH="$( cd "$(dirname "$0")/../" && pwd -P )" +BIN_PATH="${GIT_PATH}/bin" + +echo "------------------------------------------------------------" +echo "- Test: ${0}" +echo "------------------------------------------------------------" + +"${BIN_PATH}/vhost-gen" -r http://127.0.0.1:3000 -l / -n name -t etc/templates/ | grep -v '__' +"${BIN_PATH}/vhost-gen" -r http://127.0.0.1:3000 -l / -n name -t etc/templates/ -c etc/conf.yml | grep -v '__' +"${BIN_PATH}/vhost-gen" -r http://127.0.0.1:3000 -l / -n name -t etc/templates/ -c examples/conf.nginx.yml | grep -v '__' +"${BIN_PATH}/vhost-gen" -r http://127.0.0.1:3000 -l / -n name -t etc/templates/ -c examples/conf.apache22.yml | grep -v '__' +"${BIN_PATH}/vhost-gen" -r http://127.0.0.1:3000 -l / -n name -t etc/templates/ -c examples/conf.apache24.yml | grep -v '__'