├── .editorconfig ├── .flake8 ├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.md │ ├── easy-install.md │ ├── feature-request.md │ └── questions-about-using-bench.md ├── PULL_REQUEST_TEMPLATE.md ├── semantic.yml └── workflows │ ├── ci.yml │ ├── easy-install.yml │ ├── linters.yml │ └── release.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .releaserc ├── LICENSE ├── README.md ├── bench ├── __init__.py ├── app.py ├── bench.py ├── cli.py ├── commands │ ├── __init__.py │ ├── config.py │ ├── git.py │ ├── install.py │ ├── make.py │ ├── setup.py │ ├── update.py │ └── utils.py ├── config │ ├── __init__.py │ ├── common_site_config.py │ ├── lets_encrypt.py │ ├── nginx.py │ ├── procfile.py │ ├── production_setup.py │ ├── redis.py │ ├── site_config.py │ ├── supervisor.py │ ├── systemd.py │ └── templates │ │ ├── 502.html │ │ ├── Procfile │ │ ├── bench_manager_nginx.conf │ │ ├── frappe_sudoers │ │ ├── letsencrypt.cfg │ │ ├── nginx.conf │ │ ├── nginx_default.conf │ │ ├── redis_cache.conf │ │ ├── redis_queue.conf │ │ ├── supervisor.conf │ │ └── systemd │ │ ├── frappe-bench-frappe-default-worker.service │ │ ├── frappe-bench-frappe-long-worker.service │ │ ├── frappe-bench-frappe-schedule.service │ │ ├── frappe-bench-frappe-short-worker.service │ │ ├── frappe-bench-frappe-web.service │ │ ├── frappe-bench-node-socketio.service │ │ ├── frappe-bench-redis-cache.service │ │ ├── frappe-bench-redis-queue.service │ │ ├── frappe-bench-redis.target │ │ ├── frappe-bench-web.target │ │ ├── frappe-bench-workers.target │ │ └── frappe-bench.target ├── exceptions.py ├── patches │ ├── __init__.py │ ├── patches.txt │ └── v5 │ │ ├── __init__.py │ │ ├── fix_backup_cronjob.py │ │ ├── fix_user_permissions.py │ │ ├── set_live_reload_config.py │ │ └── update_archived_sites.py ├── playbooks │ ├── README.md │ ├── create_user.yml │ ├── macosx.yml │ ├── roles │ │ ├── bash_screen_wall │ │ │ ├── files │ │ │ │ └── screen_wall.sh │ │ │ └── tasks │ │ │ │ └── main.yml │ │ ├── bench │ │ │ └── tasks │ │ │ │ ├── change_ssh_port.yml │ │ │ │ ├── main.yml │ │ │ │ ├── setup_bench_production.yml │ │ │ │ ├── setup_erpnext.yml │ │ │ │ ├── setup_firewall.yml │ │ │ │ └── setup_inputrc.yml │ │ ├── common │ │ │ └── tasks │ │ │ │ ├── debian.yml │ │ │ │ ├── debian_family.yml │ │ │ │ ├── macos.yml │ │ │ │ ├── main.yml │ │ │ │ ├── redhat_family.yml │ │ │ │ └── ubuntu.yml │ │ ├── dns_caching │ │ │ ├── handlers │ │ │ │ └── main.yml │ │ │ └── tasks │ │ │ │ └── main.yml │ │ ├── fail2ban │ │ │ ├── defaults │ │ │ │ └── main.yml │ │ │ ├── handlers │ │ │ │ └── main.yml │ │ │ ├── tasks │ │ │ │ ├── configure_nginx_jail.yml │ │ │ │ └── main.yml │ │ │ └── templates │ │ │ │ ├── nginx-proxy-filter.conf.j2 │ │ │ │ └── nginx-proxy-jail.conf.j2 │ │ ├── frappe_selinux │ │ │ ├── files │ │ │ │ └── frappe_selinux.te │ │ │ └── tasks │ │ │ │ └── main.yml │ │ ├── locale │ │ │ ├── defaults │ │ │ │ └── main.yml │ │ │ └── tasks │ │ │ │ └── main.yml │ │ ├── logwatch │ │ │ ├── defaults │ │ │ │ └── main.yml │ │ │ ├── tasks │ │ │ │ └── main.yml │ │ │ └── templates │ │ │ │ └── logwatch.conf.j2 │ │ ├── mariadb │ │ │ ├── README.md │ │ │ ├── defaults │ │ │ │ └── main.yml │ │ │ ├── files │ │ │ │ ├── debian_mariadb_config.cnf │ │ │ │ └── mariadb_config.cnf │ │ │ ├── handlers │ │ │ │ └── main.yml │ │ │ ├── tasks │ │ │ │ ├── centos.yml │ │ │ │ ├── debian.yml │ │ │ │ ├── main.yml │ │ │ │ ├── mysql_secure_installation.yml │ │ │ │ ├── ubuntu-trusty.yml │ │ │ │ └── ubuntu-xenial_bionic.yml │ │ │ ├── templates │ │ │ │ ├── mariadb_centos.repo.j2 │ │ │ │ ├── mariadb_debian.list.j2 │ │ │ │ ├── mariadb_ubuntu.list.j2 │ │ │ │ └── my.cnf.j2 │ │ │ └── vars │ │ │ │ └── main.yml │ │ ├── nginx │ │ │ ├── README.md │ │ │ ├── defaults │ │ │ │ └── main.yml │ │ │ ├── handlers │ │ │ │ └── main.yml │ │ │ ├── meta │ │ │ │ └── main.yml │ │ │ ├── tasks │ │ │ │ ├── main.yml │ │ │ │ ├── setup-Debian.yml │ │ │ │ ├── setup-RedHat.yml │ │ │ │ └── vhosts.yml │ │ │ ├── templates │ │ │ │ ├── nginx.conf.j2 │ │ │ │ ├── nginx.repo.j2 │ │ │ │ └── vhosts.j2 │ │ │ ├── tests │ │ │ │ ├── inventory │ │ │ │ └── test.yml │ │ │ └── vars │ │ │ │ ├── Debian.yml │ │ │ │ └── RedHat.yml │ │ ├── nodejs │ │ │ ├── defaults │ │ │ │ └── main.yml │ │ │ └── tasks │ │ │ │ ├── debian_family.yml │ │ │ │ ├── main.yml │ │ │ │ └── redhat_family.yml │ │ ├── ntpd │ │ │ └── tasks │ │ │ │ └── main.yml │ │ ├── packer │ │ │ └── tasks │ │ │ │ ├── debian_family.yml │ │ │ │ ├── main.yml │ │ │ │ └── redhat_family.yml │ │ ├── psutil │ │ │ └── tasks │ │ │ │ └── main.yml │ │ ├── redis │ │ │ └── tasks │ │ │ │ └── main.yml │ │ ├── supervisor │ │ │ └── tasks │ │ │ │ └── main.yml │ │ ├── swap │ │ │ ├── defaults │ │ │ │ └── main.yml │ │ │ └── tasks │ │ │ │ └── main.yml │ │ ├── virtualbox │ │ │ ├── defaults │ │ │ │ └── main.yml │ │ │ ├── files │ │ │ │ └── virtualbox_centos.repo │ │ │ └── tasks │ │ │ │ ├── debian_family.yml │ │ │ │ ├── main.yml │ │ │ │ └── redhat_family.yml │ │ └── wkhtmltopdf │ │ │ └── tasks │ │ │ └── main.yml │ ├── site.yml │ └── vm_build.yml ├── tests │ ├── __init__.py │ ├── test_base.py │ ├── test_init.py │ ├── test_setup_production.py │ └── test_utils.py └── utils │ ├── __init__.py │ ├── app.py │ ├── bench.py │ ├── cli.py │ ├── render.py │ ├── system.py │ └── translation.py ├── completion.sh ├── docs ├── bench_custom_cmd.md ├── bench_usage.md ├── branch_details.md ├── commands_and_usage.md ├── contribution_guidelines.md ├── installation.md ├── release_policy.md └── releasing_frappe_apps.md ├── easy-install.py ├── pyproject.toml └── resources ├── help.png └── logo.png /.editorconfig: -------------------------------------------------------------------------------- 1 | # Root editor config file 2 | root = true 3 | 4 | # Common settings 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | charset = utf-8 10 | 11 | # python, js indentation settings 12 | [{*.py,*.js,*.vue,*.css,*.scss,*.html}] 13 | indent_style = tab 14 | indent_size = 4 15 | max_line_length = 99 16 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = 3 | E121, 4 | E126, 5 | E127, 6 | E128, 7 | E203, 8 | E225, 9 | E226, 10 | E231, 11 | E241, 12 | E251, 13 | E261, 14 | E265, 15 | E302, 16 | E303, 17 | E305, 18 | E402, 19 | E501, 20 | E741, 21 | W291, 22 | W292, 23 | W293, 24 | W391, 25 | W503, 26 | W504, 27 | F403, 28 | B007, 29 | B950, 30 | W191, 31 | E124, # closing bracket, irritating while writing QB code 32 | E131, # continuation line unaligned for hanging indent 33 | E123, # closing bracket does not match indentation of opening bracket's line 34 | E101, # ensured by use of black 35 | B009, # allow usage of getattr 36 | 37 | max-line-length = 200 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report a bug encountered while using bench 4 | labels: bug 5 | --- 6 | 7 | Issue: Bug report 8 | 9 | Please make sure your issue is reproducible on the latest bench version. The currently supported branches are: 10 | - PyPI [latest](https://pypi.org/project/frappe-bench/) 11 | - master (minor bug fixes) 12 | - v5.x (Merged with develop on every release) 13 | - develop (all updates) 14 | 15 | **Do the checklist before filing an issue:** 16 | - [ ] Can you replicate the issue on the supported bench versions? 17 | - [ ] Is this something you can debug and fix? Send a pull request! Bug fixes and documentation fixes are welcome 18 | 19 | **Describe the bug** :chart_with_downwards_trend: 20 | A clear and concise description of what the bug is. 21 | 22 | **To Reproduce** :page_with_curl: 23 | Steps to reproduce the behavior: 24 | 1. Go to '...' 25 | 2. Click on '....' 26 | 3. Scroll down to '....' 27 | 4. See error 28 | 29 | **Expected behavior** :chart_with_upwards_trend: 30 | A clear and concise description of what you expected to happen. 31 | 32 | **Screenshots** :crystal_ball: 33 | If applicable, add screenshots to help explain your problem. 34 | 35 | **OS (please complete the following information):** :cyclone: 36 | - [ ] Linux: `distro:version` 37 | - [ ] macOS: `version` 38 | - [ ] Windows `version` 39 | - [ ] Others? `haros:distro:version` 40 | 41 | **Version Information** 42 | 43 | Can be found out by running `bench version` in your respective bench folder. 44 | - Bench Branch: 45 | - Frappe Version: 46 | - ERPNext Version: 47 | 48 | **Additional context** :page_facing_up: 49 | Add any other context about the problem here. 50 | 51 | **Possible Solution** :bookmark_tabs: 52 | Any idea what might be causing the issue. Or if you have a proposed solution to the problem. 53 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/easy-install.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Easy Install 3 | about: Report a issue encountered or a suggestion for improving experience while using easy install to setup a "Bench + Frappe + ERPNext" environment 4 | labels: easy-install 5 | --- 6 | 7 | Issue: Easy Install 8 | 9 | **Do the checklist before filing an issue:** 10 | - [ ] Did you retain the logfile (path of logfile is shared while the script is run)? We definitely **need** the logfile to debug any easy install related issues. 11 | - [ ] Is this something you can debug and fix? Send a pull request! Bug fixes and documentation fixes are welcome 12 | 13 | **Distro Information (Required)** 14 | 19 | 20 | **Command Run (Required)** 21 | 24 | 25 | **Log File (Required)** 26 | 29 | 30 | **Screenshots** 31 | 34 | 35 | **Additional context** 36 | 39 | 40 | **Possible Solution** 41 | 44 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | label: feature-request 5 | --- 6 | 7 | Issue: Feature Request 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | 21 | 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/questions-about-using-bench.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question about using Bench/Frappe/Frappe Apps 3 | about: This is not the appropriate channel 4 | labels: invalid 5 | --- 6 | 7 | Please post on our forums: 8 | 9 | for questions about using `bench`, probably the best place to start is the [bench repo](https://github.com/frappe/bench) or [bench intro](https://frappe.io/bench) or [bench docs](https://frappe.io/docs/bench) 10 | 11 | for questions about using the `Frappe Framework`: ~~https://discuss.frappe.io~~ => [stackoverflow](https://stackoverflow.com/questions/tagged/frappe) tagged under `frappe` 12 | 13 | for questions about using `ERPNext`: https://discuss.erpnext.com 14 | 15 | For documentation issues, use the [Frappe Framework Documentation](https://frappe.io/docs/user/en) or the [developer cheetsheet](https://github.com/frappe/frappe/wiki/Developer-Cheatsheet) 16 | 17 | For a slightly outdated yet informative developer guide: https://www.youtube.com/playlist?list=PL3lFfCEoMxvzHtsZHFJ4T3n5yMM3nGJ1W 18 | 19 | > **Posts that are not bug reports or feature requests will not be addressed on this issue tracker.** -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 20 | 21 | What type of a PR is this? 22 | 23 | - [ ] Changes to Existing Features 24 | - [ ] New Feature Submissions 25 | - [ ] Bug Fix 26 | - [ ] Breaking Change (include change in API behaviours, etc.) 27 | 28 | --- 29 | 30 | > Please provide enough information so that others can review your pull request: 31 | 32 | 33 | 34 | --- 35 | 36 | > Explain the **details** for making this change. What existing problem does the pull request solve? The following checklist could help 37 | 38 | - [ ] Have you followed the guidelines in our Contributing document? 39 | - [ ] Have you checked to ensure there aren't other open [Pull Requests](../pulls) for the same update/change? 40 | - [ ] Have you lint your code locally prior to submission? 41 | - [ ] Have you successfully run tests with your changes locally? 42 | - [ ] Does your commit message have an explanation for your changes and why you'd like us to include them? 43 | - [ ] Docs have been added / updated 44 | - [ ] Tests for the changes have been added (for bug fixes / features) 45 | - [ ] Did you modify the existing test cases? If yes, why? 46 | 47 | --- 48 | 49 | 50 | 51 | > Screenshots/GIFs 52 | 53 | -------------------------------------------------------------------------------- /.github/semantic.yml: -------------------------------------------------------------------------------- 1 | # Always validate the PR title AND all the commits 2 | titleAndCommits: true 3 | 4 | # Allow use of Merge commits (eg on github: "Merge branch 'master' into feature/ride-unicorns") 5 | # this is only relevant when using commitsOnly: true (or titleAndCommits: true) 6 | allowMergeCommits: true 7 | 8 | # Allow use of Revert commits (eg on github: "Revert "feat: ride unicorns"") 9 | # this is only relevant when using commitsOnly: true (or titleAndCommits: true) 10 | allowRevertCommits: true 11 | 12 | # For allowed PR types: https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json 13 | # Tool Reference: https://github.com/zeke/semantic-pull-requests 14 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: 'CI' 2 | 3 | on: 4 | pull_request: 5 | workflow_dispatch: 6 | push: 7 | branches: [ develop ] 8 | 9 | concurrency: 10 | group: ci-develop-${{ github.event_name }}-${{ github.event.number }} 11 | cancel-in-progress: true 12 | 13 | permissions: 14 | contents: read 15 | 16 | jobs: 17 | base_setup: 18 | runs-on: ubuntu-latest 19 | timeout-minutes: 60 20 | 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | python-version: ['3.10', '3.11', '3.12', '3.13'] 25 | 26 | name: Base (${{ matrix.python-version }}) 27 | 28 | services: 29 | mariadb: 30 | image: mariadb:10.6 31 | env: 32 | MARIADB_ROOT_PASSWORD: travis 33 | ports: 34 | - 3306:3306 35 | options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 36 | 37 | steps: 38 | - uses: actions/checkout@v3 39 | - uses: actions/setup-python@v4 40 | with: 41 | python-version: ${{ matrix.python-version }} 42 | - uses: actions/setup-node@v3 43 | with: 44 | node-version: 18 45 | - run: | 46 | wget https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-2/wkhtmltox_0.12.6.1-2.jammy_amd64.deb; 47 | sudo apt install ./wkhtmltox_0.12.6.1-2.jammy_amd64.deb; 48 | sudo apt install redis-server 49 | 50 | python -m pip install -U --no-cache-dir --force-reinstall urllib3 pyOpenSSL ndg-httpsclient pyasn1 wheel setuptools pip; 51 | python -m pip install -U -e ${GITHUB_WORKSPACE}; 52 | 53 | - run: python ${GITHUB_WORKSPACE}/bench/tests/test_init.py TestBenchInit.basic 54 | 55 | production_setup: 56 | runs-on: ubuntu-latest 57 | timeout-minutes: 60 58 | 59 | strategy: 60 | matrix: 61 | python-version: ['3.10' ] 62 | 63 | name: Production (${{ matrix.python-version }}) 64 | 65 | services: 66 | mariadb: 67 | image: mariadb:10.6 68 | env: 69 | MARIADB_ROOT_PASSWORD: travis 70 | ports: 71 | - 3306:3306 72 | options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 73 | 74 | steps: 75 | - uses: actions/checkout@v3 76 | - uses: actions/setup-python@v4 77 | with: 78 | python-version: ${{ matrix.python-version }} 79 | - uses: actions/setup-node@v3 80 | with: 81 | node-version: 18 82 | - run: | 83 | wget https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-2/wkhtmltox_0.12.6.1-2.jammy_amd64.deb; 84 | sudo apt install ./wkhtmltox_0.12.6.1-2.jammy_amd64.deb; 85 | sudo apt install redis-server 86 | 87 | python -m pip install -U --no-cache-dir --force-reinstall urllib3 pyOpenSSL ndg-httpsclient pyasn1 wheel setuptools pip; 88 | python -m pip install -U -e ${GITHUB_WORKSPACE}; 89 | 90 | - run: python bench/tests/test_setup_production.py TestSetupProduction.production 91 | 92 | tests: 93 | runs-on: ubuntu-latest 94 | timeout-minutes: 60 95 | 96 | strategy: 97 | fail-fast: false 98 | matrix: 99 | python-version: ['3.10' ] 100 | 101 | name: Tests (${{ matrix.python-version }}) 102 | 103 | services: 104 | mariadb: 105 | image: mariadb:10.6 106 | env: 107 | MARIADB_ROOT_PASSWORD: travis 108 | ports: 109 | - 3306:3306 110 | options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 111 | 112 | steps: 113 | - uses: actions/checkout@v3 114 | - uses: actions/setup-python@v4 115 | with: 116 | python-version: ${{ matrix.python-version }} 117 | 118 | - uses: actions/setup-node@v3 119 | if: ${{ matrix.python-version == '3.10' }} 120 | with: 121 | node-version: 18 122 | 123 | - run: | 124 | wget https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-2/wkhtmltox_0.12.6.1-2.jammy_amd64.deb; 125 | sudo apt install ./wkhtmltox_0.12.6.1-2.jammy_amd64.deb; 126 | sudo apt install redis-server pkg-config 127 | 128 | python -m pip install -U --no-cache-dir --force-reinstall urllib3 pyOpenSSL ndg-httpsclient pyasn1 wheel setuptools pip; 129 | python -m pip install -U -e ${GITHUB_WORKSPACE}; 130 | 131 | - run: python -m unittest -v bench.tests.test_utils && python -m unittest -v bench.tests.test_init 132 | 133 | - uses: mxschmitt/action-tmate@v3 134 | if: ${{ failure() && contains( github.event.pull_request.labels.*.name, 'debug-gha') }} 135 | -------------------------------------------------------------------------------- /.github/workflows/easy-install.yml: -------------------------------------------------------------------------------- 1 | name: "Easy Install Test" 2 | 3 | on: 4 | pull_request: 5 | workflow_dispatch: 6 | push: 7 | branches: [develop] 8 | 9 | concurrency: 10 | group: easy-install-develop-${{ github.event_name }}-${{ github.event.number }} 11 | cancel-in-progress: true 12 | 13 | permissions: 14 | contents: read 15 | 16 | jobs: 17 | easy-install-setup: 18 | runs-on: ubuntu-latest 19 | timeout-minutes: 60 20 | 21 | name: Easy Install Test 22 | steps: 23 | - uses: actions/checkout@v3 24 | 25 | - uses: actions/setup-python@v4 26 | with: 27 | python-version: '3.12' 28 | 29 | - name: Perform production easy install 30 | run: | 31 | python3 ${GITHUB_WORKSPACE}/easy-install.py build --deploy --tag=custom-apps:latest --project=actions_test --email=test@frappe.io --image=custom-apps --version=latest --app=erpnext 32 | docker compose -p actions_test exec backend bench version --format json 33 | docker compose -p actions_test exec backend bench --site site1.localhost list-apps --format json 34 | result=$(curl -H "Host: site1.localhost" -sk https://127.0.0.1/api/method/ping | jq -r ."message") 35 | if [[ "$result" == "pong" ]]; then echo "New instance works fine"; else exit 1; fi 36 | docker compose -p actions_test down 37 | docker volume prune -f 38 | -------------------------------------------------------------------------------- /.github/workflows/linters.yml: -------------------------------------------------------------------------------- 1 | name: Linters 2 | 3 | on: 4 | pull_request: 5 | workflow_dispatch: 6 | push: 7 | branches: [ develop ] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | linter: 14 | name: 'Pre Commit Hooks' 15 | runs-on: ubuntu-latest 16 | if: github.event_name == 'pull_request' 17 | 18 | steps: 19 | - uses: actions/checkout@v3 20 | - uses: actions/setup-python@v4 21 | with: 22 | python-version: '3.12' 23 | - uses: pre-commit/action@v3.0.0 24 | with: 25 | extra_args: --files 'bench/' 26 | 27 | deps-vulnerable-check: 28 | name: 'Vulnerable Dependency Check' 29 | runs-on: ubuntu-latest 30 | 31 | steps: 32 | - uses: actions/setup-python@v4 33 | with: 34 | python-version: '3.12' 35 | - uses: actions/checkout@v3 36 | - name: 'Pip Audit' 37 | run: | 38 | pip install pip-audit 39 | pip-audit ${GITHUB_WORKSPACE} 40 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Generate Semantic Release and publish on PyPI 2 | on: 3 | push: 4 | branches: 5 | - v5.x 6 | jobs: 7 | release: 8 | name: Release 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | with: 13 | fetch-depth: 0 14 | - uses: actions/setup-node@v2 15 | with: 16 | node-version: 20 17 | - uses: actions/setup-python@v4 18 | with: 19 | python-version: '3.x' 20 | 21 | - name: Setup dependencies 22 | run: | 23 | npm install @semantic-release/git @semantic-release/exec --no-save 24 | python3 -m pip install wheel twine hatch==1.14.1 25 | 26 | - name: Create Release 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | PYPI_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 30 | PYPI_USERNAME: ${{ secrets.PYPI_USERNAME }} 31 | run: npx semantic-release 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # MAC OS 2 | .DS_Store 3 | 4 | # VS Code 5 | .vscode/ 6 | 7 | # Vim Gitignore 8 | ## Swap 9 | [._]*.s[a-v][a-z] 10 | [._]*.sw[a-p] 11 | [._]s[a-v][a-z] 12 | [._]sw[a-p] 13 | 14 | ## Session 15 | Session.vim 16 | 17 | ## Temporary 18 | .netrwhist 19 | *~ 20 | 21 | ## Auto-generated tag files 22 | tags 23 | 24 | # Python Gitignore 25 | ## Byte-compiled / optimized / DLL files 26 | __pycache__/ 27 | *.py[cod] 28 | *$py.class 29 | 30 | ## C extensions 31 | *.so 32 | 33 | ## Distribution / packaging 34 | .Python 35 | build/ 36 | develop-eggs/ 37 | dist/ 38 | downloads/ 39 | eggs/ 40 | .eggs/ 41 | lib/ 42 | lib64/ 43 | parts/ 44 | sdist/ 45 | var/ 46 | wheels/ 47 | *.egg-info/ 48 | .installed.cfg 49 | *.egg 50 | MANIFEST 51 | 52 | ## PyInstaller 53 | ## Usually these files are written by a python script from a template 54 | ## before PyInstaller builds the exe, so as to inject date/other infos into it. 55 | *.manifest 56 | *.spec 57 | 58 | ## Installer logs 59 | pip-log.txt 60 | pip-delete-this-directory.txt 61 | 62 | ## Unit test / coverage reports 63 | htmlcov/ 64 | .tox/ 65 | .coverage 66 | .coverage.* 67 | .cache 68 | nosetests.xml 69 | coverage.xml 70 | *.cover 71 | .hypothesis/ 72 | .pytest_cache/ 73 | 74 | ## Translations 75 | *.mo 76 | *.pot 77 | 78 | ## Django stuff: 79 | *.log 80 | .static_storage/ 81 | .media/ 82 | local_settings.py 83 | 84 | ## Flask stuff: 85 | instance/ 86 | .webassets-cache 87 | 88 | ## Scrapy stuff: 89 | .scrapy 90 | 91 | ## Sphinx documentation 92 | docs/_build/ 93 | 94 | ## PyBuilder 95 | target/ 96 | 97 | ## Jupyter Notebook 98 | .ipynb_checkpoints 99 | 100 | ## pyenv 101 | .python-version 102 | 103 | ## celery beat schedule file 104 | celerybeat-schedule 105 | 106 | ## SageMath parsed files 107 | *.sage.py 108 | 109 | ## Environments 110 | .env 111 | .venv 112 | env/ 113 | venv/ 114 | ENV/ 115 | env.bak/ 116 | venv.bak/ 117 | 118 | ## Spyder project settings 119 | .spyderproject 120 | .spyproject 121 | 122 | ## Rope project settings 123 | .ropeproject 124 | 125 | ## mkdocs documentation 126 | /site 127 | 128 | ## mypy 129 | .mypy_cache/ 130 | 131 | # Packer Gitignore 132 | ## Cache objects 133 | packer_cache/ 134 | *.checksum 135 | 136 | ## For built virtualmachines 137 | *.ova 138 | *.iso 139 | 140 | ## For built boxes 141 | *.box 142 | 143 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | exclude: '.git' 2 | default_stages: [commit] 3 | fail_fast: false 4 | 5 | repos: 6 | - repo: https://github.com/pre-commit/pre-commit-hooks 7 | rev: v4.3.0 8 | hooks: 9 | - id: trailing-whitespace 10 | files: "frappe.*" 11 | exclude: ".*json$|.*txt$|.*csv|.*md|.*svg" 12 | - id: check-yaml 13 | - id: check-merge-conflict 14 | - id: check-ast 15 | - id: check-json 16 | - id: check-toml 17 | - id: check-yaml 18 | - id: debug-statements 19 | 20 | - repo: https://github.com/asottile/pyupgrade 21 | rev: v2.34.0 22 | hooks: 23 | - id: pyupgrade 24 | args: ['--py37-plus'] 25 | 26 | - repo: https://github.com/adityahase/black 27 | rev: 9cb0a69f4d0030cdf687eddf314468b39ed54119 28 | hooks: 29 | - id: black 30 | additional_dependencies: ['click==8.0.4'] 31 | 32 | - repo: https://github.com/pycqa/flake8 33 | rev: 5.0.4 34 | hooks: 35 | - id: flake8 36 | additional_dependencies: ['flake8-bugbear',] 37 | args: ['--config', '.flake8'] 38 | -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | { 2 | "branches": ["v5.x"], 3 | "plugins": [ 4 | "@semantic-release/commit-analyzer", 5 | "@semantic-release/release-notes-generator", 6 | [ 7 | "@semantic-release/exec", { 8 | "prepareCmd": 'sed -ir -E "s/\"[0-9]+\.[0-9]+\.[0-9]+\"/\"${nextRelease.version}\"/" bench/__init__.py' 9 | } 10 | ], 11 | [ 12 | "@semantic-release/exec", { 13 | "prepareCmd": "hatch build -t sdist -t wheel" 14 | } 15 | ], 16 | [ 17 | "@semantic-release/git", { 18 | "assets": ["bench/__init__.py"], 19 | "message": "chore(release): Bumped to Version ${nextRelease.version}\n\n${nextRelease.notes}" 20 | } 21 | ], 22 | [ 23 | "@semantic-release/github", { 24 | "assets": [ 25 | {"path": "dist/*"}, 26 | ] 27 | } 28 | ], 29 | [ 30 | "@semantic-release/exec", { 31 | "publishCmd": "python -m twine upload dist/* -u $PYPI_USERNAME -p $PYPI_PASSWORD" 32 | } 33 | ] 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /bench/__init__.py: -------------------------------------------------------------------------------- 1 | VERSION = "5.0.0-dev" 2 | PROJECT_NAME = "frappe-bench" 3 | FRAPPE_VERSION = None 4 | current_path = None 5 | updated_path = None 6 | LOG_BUFFER = [] 7 | 8 | 9 | def set_frappe_version(bench_path="."): 10 | from .utils.app import get_current_frappe_version 11 | 12 | global FRAPPE_VERSION 13 | if not FRAPPE_VERSION: 14 | FRAPPE_VERSION = get_current_frappe_version(bench_path=bench_path) 15 | -------------------------------------------------------------------------------- /bench/commands/__init__.py: -------------------------------------------------------------------------------- 1 | # imports - third party imports 2 | import click 3 | 4 | # imports - module imports 5 | from bench.utils.cli import ( 6 | MultiCommandGroup, 7 | print_bench_version, 8 | use_experimental_feature, 9 | setup_verbosity, 10 | ) 11 | 12 | 13 | @click.group(cls=MultiCommandGroup) 14 | @click.option( 15 | "--version", 16 | is_flag=True, 17 | is_eager=True, 18 | callback=print_bench_version, 19 | expose_value=False, 20 | ) 21 | @click.option( 22 | "--use-feature", 23 | is_eager=True, 24 | callback=use_experimental_feature, 25 | expose_value=False, 26 | ) 27 | @click.option( 28 | "-v", 29 | "--verbose", 30 | is_flag=True, 31 | callback=setup_verbosity, 32 | expose_value=False, 33 | ) 34 | def bench_command(bench_path="."): 35 | import bench 36 | 37 | bench.set_frappe_version(bench_path=bench_path) 38 | 39 | 40 | from bench.commands.make import ( 41 | drop, 42 | exclude_app_for_update, 43 | get_app, 44 | include_app_for_update, 45 | init, 46 | new_app, 47 | pip, 48 | remove_app, 49 | validate_dependencies, 50 | ) 51 | 52 | bench_command.add_command(init) 53 | bench_command.add_command(drop) 54 | bench_command.add_command(get_app) 55 | bench_command.add_command(new_app) 56 | bench_command.add_command(remove_app) 57 | bench_command.add_command(exclude_app_for_update) 58 | bench_command.add_command(include_app_for_update) 59 | bench_command.add_command(pip) 60 | bench_command.add_command(validate_dependencies) 61 | 62 | 63 | from bench.commands.update import ( 64 | retry_upgrade, 65 | switch_to_branch, 66 | switch_to_develop, 67 | update, 68 | ) 69 | 70 | bench_command.add_command(update) 71 | bench_command.add_command(retry_upgrade) 72 | bench_command.add_command(switch_to_branch) 73 | bench_command.add_command(switch_to_develop) 74 | 75 | 76 | from bench.commands.utils import ( 77 | app_cache_helper, 78 | backup_all_sites, 79 | bench_src, 80 | disable_production, 81 | download_translations, 82 | find_benches, 83 | migrate_env, 84 | renew_lets_encrypt, 85 | restart, 86 | set_mariadb_host, 87 | set_nginx_port, 88 | set_redis_cache_host, 89 | set_redis_queue_host, 90 | set_redis_socketio_host, 91 | set_ssl_certificate, 92 | set_ssl_certificate_key, 93 | set_url_root, 94 | start, 95 | ) 96 | 97 | bench_command.add_command(start) 98 | bench_command.add_command(restart) 99 | bench_command.add_command(set_nginx_port) 100 | bench_command.add_command(set_ssl_certificate) 101 | bench_command.add_command(set_ssl_certificate_key) 102 | bench_command.add_command(set_url_root) 103 | bench_command.add_command(set_mariadb_host) 104 | bench_command.add_command(set_redis_cache_host) 105 | bench_command.add_command(set_redis_queue_host) 106 | bench_command.add_command(set_redis_socketio_host) 107 | bench_command.add_command(download_translations) 108 | bench_command.add_command(backup_all_sites) 109 | bench_command.add_command(renew_lets_encrypt) 110 | bench_command.add_command(disable_production) 111 | bench_command.add_command(bench_src) 112 | bench_command.add_command(find_benches) 113 | bench_command.add_command(migrate_env) 114 | bench_command.add_command(app_cache_helper) 115 | 116 | from bench.commands.setup import setup 117 | 118 | bench_command.add_command(setup) 119 | 120 | 121 | from bench.commands.config import config 122 | 123 | bench_command.add_command(config) 124 | 125 | from bench.commands.git import remote_reset_url, remote_set_url, remote_urls 126 | 127 | bench_command.add_command(remote_set_url) 128 | bench_command.add_command(remote_reset_url) 129 | bench_command.add_command(remote_urls) 130 | 131 | from bench.commands.install import install 132 | 133 | bench_command.add_command(install) 134 | -------------------------------------------------------------------------------- /bench/commands/config.py: -------------------------------------------------------------------------------- 1 | # imports - module imports 2 | from bench.config.common_site_config import update_config, put_config 3 | 4 | # imports - third party imports 5 | import click 6 | 7 | 8 | @click.group(help="Change bench configuration") 9 | def config(): 10 | pass 11 | 12 | 13 | @click.command( 14 | "restart_supervisor_on_update", 15 | help="Enable/Disable auto restart of supervisor processes", 16 | ) 17 | @click.argument("state", type=click.Choice(["on", "off"])) 18 | def config_restart_supervisor_on_update(state): 19 | update_config({"restart_supervisor_on_update": state == "on"}) 20 | 21 | 22 | @click.command( 23 | "restart_systemd_on_update", help="Enable/Disable auto restart of systemd units" 24 | ) 25 | @click.argument("state", type=click.Choice(["on", "off"])) 26 | def config_restart_systemd_on_update(state): 27 | update_config({"restart_systemd_on_update": state == "on"}) 28 | 29 | 30 | @click.command( 31 | "dns_multitenant", help="Enable/Disable bench multitenancy on running bench update" 32 | ) 33 | @click.argument("state", type=click.Choice(["on", "off"])) 34 | def config_dns_multitenant(state): 35 | update_config({"dns_multitenant": state == "on"}) 36 | 37 | 38 | @click.command( 39 | "serve_default_site", help="Configure nginx to serve the default site on port 80" 40 | ) 41 | @click.argument("state", type=click.Choice(["on", "off"])) 42 | def config_serve_default_site(state): 43 | update_config({"serve_default_site": state == "on"}) 44 | 45 | 46 | @click.command("rebase_on_pull", help="Rebase repositories on pulling") 47 | @click.argument("state", type=click.Choice(["on", "off"])) 48 | def config_rebase_on_pull(state): 49 | update_config({"rebase_on_pull": state == "on"}) 50 | 51 | 52 | @click.command("http_timeout", help="Set HTTP timeout") 53 | @click.argument("seconds", type=int) 54 | def config_http_timeout(seconds): 55 | update_config({"http_timeout": seconds}) 56 | 57 | 58 | @click.command("set-common-config", help="Set value in common config") 59 | @click.option("configs", "-c", "--config", multiple=True, type=(str, str)) 60 | def set_common_config(configs): 61 | import ast 62 | 63 | common_site_config = {} 64 | for key, value in configs: 65 | if value in ("true", "false"): 66 | value = value.title() 67 | try: 68 | value = ast.literal_eval(value) 69 | except ValueError: 70 | pass 71 | 72 | common_site_config[key] = value 73 | 74 | update_config(common_site_config, bench_path=".") 75 | 76 | 77 | @click.command( 78 | "remove-common-config", help="Remove specific keys from current bench's common config" 79 | ) 80 | @click.argument("keys", nargs=-1) 81 | def remove_common_config(keys): 82 | from bench.bench import Bench 83 | 84 | common_site_config = Bench(".").conf 85 | for key in keys: 86 | if key in common_site_config: 87 | del common_site_config[key] 88 | 89 | put_config(common_site_config) 90 | 91 | 92 | config.add_command(config_restart_supervisor_on_update) 93 | config.add_command(config_restart_systemd_on_update) 94 | config.add_command(config_dns_multitenant) 95 | config.add_command(config_rebase_on_pull) 96 | config.add_command(config_serve_default_site) 97 | config.add_command(config_http_timeout) 98 | config.add_command(set_common_config) 99 | config.add_command(remove_common_config) 100 | -------------------------------------------------------------------------------- /bench/commands/git.py: -------------------------------------------------------------------------------- 1 | # imports - standard imports 2 | import os 3 | import subprocess 4 | 5 | # imports - module imports 6 | from bench.bench import Bench 7 | from bench.app import get_repo_dir 8 | from bench.utils import set_git_remote_url 9 | from bench.utils.app import get_remote 10 | 11 | # imports - third party imports 12 | import click 13 | 14 | 15 | @click.command('remote-set-url', help="Set app remote url") 16 | @click.argument('git-url') 17 | def remote_set_url(git_url): 18 | set_git_remote_url(git_url) 19 | 20 | 21 | @click.command('remote-reset-url', help="Reset app remote url to frappe official") 22 | @click.argument('app') 23 | def remote_reset_url(app): 24 | git_url = f"https://github.com/frappe/{app}.git" 25 | set_git_remote_url(git_url) 26 | 27 | 28 | @click.command('remote-urls', help="Show apps remote url") 29 | def remote_urls(): 30 | for app in Bench(".").apps: 31 | repo_dir = get_repo_dir(app) 32 | 33 | if os.path.exists(os.path.join(repo_dir, '.git')): 34 | remote = get_remote(app) 35 | remote_url = subprocess.check_output(['git', 'config', '--get', f'remote.{remote}.url'], cwd=repo_dir).strip() 36 | print(f"{app}\t{remote_url}") 37 | 38 | -------------------------------------------------------------------------------- /bench/commands/install.py: -------------------------------------------------------------------------------- 1 | # imports - module imports 2 | from bench.utils import run_playbook 3 | from bench.utils.system import setup_sudoers 4 | 5 | # imports - third party imports 6 | import click 7 | 8 | 9 | extra_vars = {"production": True} 10 | 11 | 12 | @click.group(help="Install system dependencies for setting up Frappe environment") 13 | def install(): 14 | pass 15 | 16 | 17 | @click.command( 18 | "prerequisites", 19 | help="Installs pre-requisite libraries, essential tools like b2zip, htop, screen, vim, x11-fonts, python libs, cups and Redis", 20 | ) 21 | def install_prerequisites(): 22 | run_playbook("site.yml", tag="common, redis") 23 | 24 | 25 | @click.command( 26 | "mariadb", help="Install and setup MariaDB of specified version and root password" 27 | ) 28 | @click.option("--mysql_root_password", "--mysql-root-password", 29 | "--mariadb_root_password", "--mariadb-root-password", default="") 30 | @click.option("--version", default="10.3") 31 | def install_mariadb(mysql_root_password, version): 32 | if mysql_root_password: 33 | extra_vars.update( 34 | { 35 | "mysql_root_password": mysql_root_password, 36 | } 37 | ) 38 | 39 | extra_vars.update({"mariadb_version": version}) 40 | 41 | run_playbook("site.yml", extra_vars=extra_vars, tag="mariadb") 42 | 43 | 44 | @click.command("wkhtmltopdf", help="Installs wkhtmltopdf v0.12.3 for linux") 45 | def install_wkhtmltopdf(): 46 | run_playbook("site.yml", extra_vars=extra_vars, tag="wkhtmltopdf") 47 | 48 | 49 | @click.command("nodejs", help="Installs Node.js v8") 50 | def install_nodejs(): 51 | run_playbook("site.yml", extra_vars=extra_vars, tag="nodejs") 52 | 53 | 54 | @click.command("psutil", help="Installs psutil via pip") 55 | def install_psutil(): 56 | run_playbook("site.yml", extra_vars=extra_vars, tag="psutil") 57 | 58 | 59 | @click.command( 60 | "supervisor", 61 | help="Installs supervisor. If user is specified, sudoers is setup for that user", 62 | ) 63 | @click.option("--user") 64 | def install_supervisor(user=None): 65 | run_playbook("site.yml", extra_vars=extra_vars, tag="supervisor") 66 | if user: 67 | setup_sudoers(user) 68 | 69 | 70 | @click.command( 71 | "nginx", help="Installs NGINX. If user is specified, sudoers is setup for that user" 72 | ) 73 | @click.option("--user") 74 | def install_nginx(user=None): 75 | run_playbook("site.yml", extra_vars=extra_vars, tag="nginx") 76 | if user: 77 | setup_sudoers(user) 78 | 79 | 80 | @click.command("virtualbox", help="Installs virtualbox") 81 | def install_virtualbox(): 82 | run_playbook("vm_build.yml", tag="virtualbox") 83 | 84 | 85 | @click.command("packer", help="Installs Oracle virtualbox and packer 1.2.1") 86 | def install_packer(): 87 | run_playbook("vm_build.yml", tag="packer") 88 | 89 | 90 | @click.command( 91 | "fail2ban", 92 | help="Install fail2ban, an intrusion prevention software framework that protects computer servers from brute-force attacks", 93 | ) 94 | @click.option( 95 | "--maxretry", 96 | default=6, 97 | help="Number of matches (i.e. value of the counter) which triggers ban action on the IP.", 98 | ) 99 | @click.option( 100 | "--bantime", 101 | default=600, 102 | help="The counter is set to zero if no match is found within 'findtime' seconds.", 103 | ) 104 | @click.option( 105 | "--findtime", 106 | default=600, 107 | help='Duration (in seconds) for IP to be banned for. Negative number for "permanent" ban.', 108 | ) 109 | def install_failtoban(**kwargs): 110 | extra_vars.update(kwargs) 111 | run_playbook("site.yml", extra_vars=extra_vars, tag="fail2ban") 112 | 113 | 114 | install.add_command(install_prerequisites) 115 | install.add_command(install_mariadb) 116 | install.add_command(install_wkhtmltopdf) 117 | install.add_command(install_nodejs) 118 | install.add_command(install_psutil) 119 | install.add_command(install_supervisor) 120 | install.add_command(install_nginx) 121 | install.add_command(install_failtoban) 122 | install.add_command(install_virtualbox) 123 | install.add_command(install_packer) 124 | -------------------------------------------------------------------------------- /bench/commands/update.py: -------------------------------------------------------------------------------- 1 | # imports - third party imports 2 | import click 3 | 4 | # imports - module imports 5 | from bench.app import pull_apps 6 | from bench.utils.bench import post_upgrade, patch_sites, build_assets 7 | 8 | 9 | @click.command( 10 | "update", 11 | help="Performs an update operation on current bench. Without any flags will backup, pull, setup requirements, build, run patches and restart bench. Using specific flags will only do certain tasks instead of all", 12 | ) 13 | @click.option("--pull", is_flag=True, help="Pull updates for all the apps in bench") 14 | @click.option("--apps", type=str) 15 | @click.option("--patch", is_flag=True, help="Run migrations for all sites in the bench") 16 | @click.option("--build", is_flag=True, help="Build JS and CSS assets for the bench") 17 | @click.option( 18 | "--requirements", 19 | is_flag=True, 20 | help="Update requirements. If run alone, equivalent to `bench setup requirements`", 21 | ) 22 | @click.option( 23 | "--restart-supervisor", is_flag=True, help="Restart supervisor processes after update" 24 | ) 25 | @click.option( 26 | "--restart-systemd", is_flag=True, help="Restart systemd units after update" 27 | ) 28 | @click.option( 29 | "--no-backup", 30 | is_flag=True, 31 | help="If this flag is set, sites won't be backed up prior to updates. Note: This is not recommended in production.", 32 | ) 33 | @click.option( 34 | "--no-compile", 35 | is_flag=True, 36 | help="[DEPRECATED] This flag doesn't do anything now.", 37 | ) 38 | @click.option("--force", is_flag=True, help="Forces major version upgrades") 39 | @click.option( 40 | "--reset", 41 | is_flag=True, 42 | help="Hard resets git branch's to their new states overriding any changes and overriding rebase on pull", 43 | ) 44 | def update( 45 | pull, 46 | apps, 47 | patch, 48 | build, 49 | requirements, 50 | restart_supervisor, 51 | restart_systemd, 52 | no_backup, 53 | no_compile, 54 | force, 55 | reset, 56 | ): 57 | from bench.utils.bench import update 58 | 59 | update( 60 | pull=pull, 61 | apps=apps, 62 | patch=patch, 63 | build=build, 64 | requirements=requirements, 65 | restart_supervisor=restart_supervisor, 66 | restart_systemd=restart_systemd, 67 | backup=not no_backup, 68 | compile=not no_compile, 69 | force=force, 70 | reset=reset, 71 | ) 72 | 73 | 74 | @click.command("retry-upgrade", help="Retry a failed upgrade") 75 | @click.option("--version", default=5) 76 | def retry_upgrade(version): 77 | pull_apps() 78 | patch_sites() 79 | build_assets() 80 | post_upgrade(version - 1, version) 81 | 82 | 83 | @click.command( 84 | "switch-to-branch", 85 | help="Switch all apps to specified branch, or specify apps separated by space", 86 | ) 87 | @click.argument("branch") 88 | @click.argument("apps", nargs=-1) 89 | @click.option("--upgrade", is_flag=True) 90 | def switch_to_branch(branch, apps, upgrade=False): 91 | from bench.utils.app import switch_to_branch 92 | 93 | switch_to_branch(branch=branch, apps=list(apps), upgrade=upgrade) 94 | 95 | 96 | @click.command("switch-to-develop") 97 | def switch_to_develop(upgrade=False): 98 | "Switch frappe and erpnext to develop branch" 99 | from bench.utils.app import switch_to_develop 100 | 101 | switch_to_develop(apps=["frappe", "erpnext"]) 102 | -------------------------------------------------------------------------------- /bench/commands/utils.py: -------------------------------------------------------------------------------- 1 | # imports - standard imports 2 | import os 3 | 4 | # imports - third party imports 5 | import click 6 | 7 | 8 | @click.command("start", help="Start Frappe development processes") 9 | @click.option("--no-dev", is_flag=True, default=False) 10 | @click.option( 11 | "--no-prefix", 12 | is_flag=True, 13 | default=False, 14 | help="Hide process name from bench start log", 15 | ) 16 | @click.option("--concurrency", "-c", type=str) 17 | @click.option("--procfile", "-p", type=str) 18 | @click.option("--man", "-m", help="Process Manager of your choice ;)") 19 | def start(no_dev, concurrency, procfile, no_prefix, man): 20 | from bench.utils.system import start 21 | 22 | start( 23 | no_dev=no_dev, 24 | concurrency=concurrency, 25 | procfile=procfile, 26 | no_prefix=no_prefix, 27 | procman=man, 28 | ) 29 | 30 | 31 | @click.command("restart", help="Restart supervisor processes or systemd units") 32 | @click.option("--web", is_flag=True, default=False) 33 | @click.option("--supervisor", is_flag=True, default=False) 34 | @click.option("--systemd", is_flag=True, default=False) 35 | def restart(web, supervisor, systemd): 36 | from bench.bench import Bench 37 | 38 | if not systemd and not web: 39 | supervisor = True 40 | 41 | Bench(".").reload(web, supervisor, systemd) 42 | 43 | 44 | @click.command("set-nginx-port", help="Set NGINX port for site") 45 | @click.argument("site") 46 | @click.argument("port", type=int) 47 | def set_nginx_port(site, port): 48 | from bench.config.site_config import set_nginx_port 49 | 50 | set_nginx_port(site, port) 51 | 52 | 53 | @click.command("set-ssl-certificate", help="Set SSL certificate path for site") 54 | @click.argument("site") 55 | @click.argument("ssl-certificate-path") 56 | def set_ssl_certificate(site, ssl_certificate_path): 57 | from bench.config.site_config import set_ssl_certificate 58 | 59 | set_ssl_certificate(site, ssl_certificate_path) 60 | 61 | 62 | @click.command("set-ssl-key", help="Set SSL certificate private key path for site") 63 | @click.argument("site") 64 | @click.argument("ssl-certificate-key-path") 65 | def set_ssl_certificate_key(site, ssl_certificate_key_path): 66 | from bench.config.site_config import set_ssl_certificate_key 67 | 68 | set_ssl_certificate_key(site, ssl_certificate_key_path) 69 | 70 | 71 | @click.command("set-url-root", help="Set URL root for site") 72 | @click.argument("site") 73 | @click.argument("url-root") 74 | def set_url_root(site, url_root): 75 | from bench.config.site_config import set_url_root 76 | 77 | set_url_root(site, url_root) 78 | 79 | 80 | @click.command("set-mariadb-host", help="Set MariaDB host for bench") 81 | @click.argument("host") 82 | def set_mariadb_host(host): 83 | from bench.utils.bench import set_mariadb_host 84 | 85 | set_mariadb_host(host) 86 | 87 | 88 | @click.command("set-redis-cache-host", help="Set Redis cache host for bench") 89 | @click.argument("host") 90 | def set_redis_cache_host(host): 91 | """ 92 | Usage: bench set-redis-cache-host localhost:6379/1 93 | """ 94 | from bench.utils.bench import set_redis_cache_host 95 | 96 | set_redis_cache_host(host) 97 | 98 | 99 | @click.command("set-redis-queue-host", help="Set Redis queue host for bench") 100 | @click.argument("host") 101 | def set_redis_queue_host(host): 102 | """ 103 | Usage: bench set-redis-queue-host localhost:6379/2 104 | """ 105 | from bench.utils.bench import set_redis_queue_host 106 | 107 | set_redis_queue_host(host) 108 | 109 | 110 | @click.command("set-redis-socketio-host", help="Set Redis socketio host for bench") 111 | @click.argument("host") 112 | def set_redis_socketio_host(host): 113 | """ 114 | Usage: bench set-redis-socketio-host localhost:6379/3 115 | """ 116 | from bench.utils.bench import set_redis_socketio_host 117 | 118 | set_redis_socketio_host(host) 119 | 120 | 121 | @click.command("download-translations", help="Download latest translations") 122 | def download_translations(): 123 | from bench.utils.translation import download_translations_p 124 | 125 | download_translations_p() 126 | 127 | 128 | @click.command( 129 | "renew-lets-encrypt", help="Sets Up latest cron and Renew Let's Encrypt certificate" 130 | ) 131 | def renew_lets_encrypt(): 132 | from bench.config.lets_encrypt import renew_certs 133 | 134 | renew_certs() 135 | 136 | 137 | @click.command("backup-all-sites", help="Backup all sites in current bench") 138 | def backup_all_sites(): 139 | from bench.utils.system import backup_all_sites 140 | 141 | backup_all_sites(bench_path=".") 142 | 143 | 144 | @click.command( 145 | "disable-production", help="Disables production environment for the bench." 146 | ) 147 | def disable_production(): 148 | from bench.config.production_setup import disable_production 149 | 150 | disable_production(bench_path=".") 151 | 152 | 153 | @click.command( 154 | "src", help="Prints bench source folder path, which can be used as: cd `bench src`" 155 | ) 156 | def bench_src(): 157 | from bench.cli import src 158 | 159 | print(os.path.dirname(src)) 160 | 161 | 162 | @click.command("find", help="Finds benches recursively from location") 163 | @click.argument("location", default="") 164 | def find_benches(location): 165 | from bench.utils import find_benches 166 | 167 | find_benches(directory=location) 168 | 169 | 170 | @click.command( 171 | "migrate-env", help="Migrate Virtual Environment to desired Python Version" 172 | ) 173 | @click.argument("python", type=str) 174 | @click.option("--no-backup", "backup", is_flag=True, default=True) 175 | def migrate_env(python, backup=True): 176 | from bench.utils.bench import migrate_env 177 | 178 | migrate_env(python=python, backup=backup) 179 | 180 | 181 | @click.command("app-cache", help="View or remove items belonging to bench get-app cache") 182 | @click.option("--clear", is_flag=True, default=False, help="Remove all items") 183 | @click.option( 184 | "--remove-app", 185 | default="", 186 | help="Removes all items that match provided app name", 187 | ) 188 | @click.option( 189 | "--remove-key", 190 | default="", 191 | help="Removes all items that matches provided cache key", 192 | ) 193 | def app_cache_helper(clear=False, remove_app="", remove_key=""): 194 | from bench.utils.bench import cache_helper 195 | 196 | cache_helper(clear, remove_app, remove_key) 197 | -------------------------------------------------------------------------------- /bench/config/__init__.py: -------------------------------------------------------------------------------- 1 | """Module for setting up system and respective bench configurations""" 2 | 3 | 4 | def env(): 5 | from jinja2 import Environment, PackageLoader 6 | 7 | return Environment(loader=PackageLoader("bench.config")) 8 | -------------------------------------------------------------------------------- /bench/config/common_site_config.py: -------------------------------------------------------------------------------- 1 | # imports - standard imports 2 | import getpass 3 | import json 4 | import os 5 | 6 | default_config = { 7 | "restart_supervisor_on_update": False, 8 | "restart_systemd_on_update": False, 9 | "serve_default_site": True, 10 | "rebase_on_pull": False, 11 | "frappe_user": getpass.getuser(), 12 | "shallow_clone": True, 13 | "background_workers": 1, 14 | "use_redis_auth": False, 15 | "live_reload": True, 16 | } 17 | 18 | DEFAULT_MAX_REQUESTS = 5000 19 | 20 | 21 | def setup_config(bench_path, additional_config=None): 22 | make_pid_folder(bench_path) 23 | bench_config = get_config(bench_path) 24 | bench_config.update(default_config) 25 | bench_config.update(get_gunicorn_workers()) 26 | update_config_for_frappe(bench_config, bench_path) 27 | if additional_config: 28 | bench_config.update(additional_config) 29 | 30 | put_config(bench_config, bench_path) 31 | 32 | 33 | def get_config(bench_path): 34 | return get_common_site_config(bench_path) 35 | 36 | 37 | def get_common_site_config(bench_path): 38 | config_path = get_config_path(bench_path) 39 | if not os.path.exists(config_path): 40 | return {} 41 | with open(config_path) as f: 42 | return json.load(f) 43 | 44 | 45 | def put_config(config, bench_path="."): 46 | config_path = get_config_path(bench_path) 47 | with open(config_path, "w") as f: 48 | return json.dump(config, f, indent=1, sort_keys=True) 49 | 50 | 51 | def update_config(new_config, bench_path="."): 52 | config = get_config(bench_path=bench_path) 53 | config.update(new_config) 54 | put_config(config, bench_path=bench_path) 55 | 56 | 57 | def get_config_path(bench_path): 58 | return os.path.join(bench_path, "sites", "common_site_config.json") 59 | 60 | 61 | def get_gunicorn_workers(): 62 | """This function will return the maximum workers that can be started depending upon 63 | number of cpu's present on the machine""" 64 | import multiprocessing 65 | 66 | return {"gunicorn_workers": multiprocessing.cpu_count() * 2 + 1} 67 | 68 | 69 | def compute_max_requests_jitter(max_requests: int) -> int: 70 | return int(max_requests * 0.1) 71 | 72 | 73 | def get_default_max_requests(worker_count: int): 74 | """Get max requests and jitter config based on number of available workers.""" 75 | 76 | if worker_count <= 1: 77 | # If there's only one worker then random restart can cause spikes in response times and 78 | # can be annoying. Hence not enabled by default. 79 | return 0 80 | return DEFAULT_MAX_REQUESTS 81 | 82 | 83 | def update_config_for_frappe(config, bench_path): 84 | ports = make_ports(bench_path) 85 | 86 | for key in ("redis_cache", "redis_queue", "redis_socketio"): 87 | if key not in config: 88 | config[key] = f"redis://127.0.0.1:{ports[key]}" 89 | 90 | for key in ("webserver_port", "socketio_port", "file_watcher_port"): 91 | if key not in config: 92 | config[key] = ports[key] 93 | 94 | 95 | def make_ports(bench_path): 96 | from urllib.parse import urlparse 97 | 98 | benches_path = os.path.dirname(os.path.abspath(bench_path)) 99 | 100 | default_ports = { 101 | "webserver_port": 8000, 102 | "socketio_port": 9000, 103 | "file_watcher_port": 6787, 104 | "redis_queue": 11000, 105 | "redis_socketio": 13000, 106 | "redis_cache": 13000, 107 | } 108 | 109 | # collect all existing ports 110 | existing_ports = {} 111 | for folder in os.listdir(benches_path): 112 | bench_path = os.path.join(benches_path, folder) 113 | if os.path.isdir(bench_path): 114 | bench_config = get_config(bench_path) 115 | for key in list(default_ports.keys()): 116 | value = bench_config.get(key) 117 | 118 | # extract port from redis url 119 | if value and (key in ("redis_cache", "redis_queue", "redis_socketio")): 120 | value = urlparse(value).port 121 | 122 | if value: 123 | existing_ports.setdefault(key, []).append(value) 124 | 125 | # new port value = max of existing port value + 1 126 | ports = {} 127 | for key, value in list(default_ports.items()): 128 | existing_value = existing_ports.get(key, []) 129 | if existing_value: 130 | value = max(existing_value) + 1 131 | 132 | ports[key] = value 133 | 134 | # Backward compatbility: always keep redis_cache and redis_socketio port same 135 | # Note: not required from v15 136 | ports["redis_socketio"] = ports["redis_cache"] 137 | 138 | return ports 139 | 140 | 141 | def make_pid_folder(bench_path): 142 | pids_path = os.path.join(bench_path, "config", "pids") 143 | if not os.path.exists(pids_path): 144 | os.makedirs(pids_path) 145 | -------------------------------------------------------------------------------- /bench/config/lets_encrypt.py: -------------------------------------------------------------------------------- 1 | # imports - standard imports 2 | import os 3 | 4 | # imports - third party imports 5 | import click 6 | 7 | # imports - module imports 8 | import bench 9 | from bench.config.nginx import make_nginx_conf 10 | from bench.config.production_setup import service 11 | from bench.config.site_config import get_domains, remove_domain, update_site_config 12 | from bench.bench import Bench 13 | from bench.utils import exec_cmd, which 14 | from bench.utils.bench import update_common_site_config 15 | from bench.exceptions import CommandFailedError 16 | 17 | 18 | def setup_letsencrypt(site, custom_domain, bench_path, interactive): 19 | 20 | site_path = os.path.join(bench_path, "sites", site, "site_config.json") 21 | if not os.path.exists(os.path.dirname(site_path)): 22 | print("No site named " + site) 23 | return 24 | 25 | if custom_domain: 26 | domains = get_domains(site, bench_path) 27 | for d in domains: 28 | if isinstance(d, dict) and d["domain"] == custom_domain: 29 | print(f"SSL for Domain {custom_domain} already exists") 30 | return 31 | 32 | if custom_domain not in domains: 33 | print(f"No custom domain named {custom_domain} set for site") 34 | return 35 | 36 | if interactive: 37 | click.confirm( 38 | "Running this will stop the nginx service temporarily causing your sites to go offline\n" 39 | "Do you want to continue?", 40 | abort=True, 41 | ) 42 | 43 | if not Bench(bench_path).conf.get("dns_multitenant"): 44 | print("You cannot setup SSL without DNS Multitenancy") 45 | return 46 | 47 | create_config(site, custom_domain) 48 | run_certbot_and_setup_ssl(site, custom_domain, bench_path, interactive) 49 | setup_crontab() 50 | 51 | 52 | def create_config(site, custom_domain): 53 | config = ( 54 | bench.config.env() 55 | .get_template("letsencrypt.cfg") 56 | .render(domain=custom_domain or site) 57 | ) 58 | config_path = f"/etc/letsencrypt/configs/{custom_domain or site}.cfg" 59 | create_dir_if_missing(config_path) 60 | 61 | with open(config_path, "w") as f: 62 | f.write(config) 63 | 64 | 65 | def run_certbot_and_setup_ssl(site, custom_domain, bench_path, interactive=True): 66 | service("nginx", "stop") 67 | 68 | try: 69 | interactive = "" if interactive else "-n" 70 | exec_cmd( 71 | f"{get_certbot_path()} {interactive} --config /etc/letsencrypt/configs/{custom_domain or site}.cfg certonly" 72 | ) 73 | except CommandFailedError: 74 | service("nginx", "start") 75 | print("There was a problem trying to setup SSL for your site") 76 | return 77 | 78 | ssl_path = f"/etc/letsencrypt/live/{custom_domain or site}/" 79 | ssl_config = { 80 | "ssl_certificate": os.path.join(ssl_path, "fullchain.pem"), 81 | "ssl_certificate_key": os.path.join(ssl_path, "privkey.pem"), 82 | } 83 | 84 | if custom_domain: 85 | remove_domain(site, custom_domain, bench_path) 86 | domains = get_domains(site, bench_path) 87 | ssl_config["domain"] = custom_domain 88 | domains.append(ssl_config) 89 | update_site_config(site, {"domains": domains}, bench_path=bench_path) 90 | else: 91 | update_site_config(site, ssl_config, bench_path=bench_path) 92 | 93 | make_nginx_conf(bench_path) 94 | service("nginx", "start") 95 | 96 | 97 | def setup_crontab(): 98 | from crontab import CronTab 99 | 100 | job_command = ( 101 | f'{get_certbot_path()} renew -a nginx --post-hook "systemctl reload nginx"' 102 | ) 103 | job_comment = "Renew lets-encrypt every month" 104 | print(f"Setting Up cron job to {job_comment}") 105 | 106 | system_crontab = CronTab(user="root") 107 | 108 | for job in system_crontab.find_comment(comment=job_comment): # Removes older entries 109 | system_crontab.remove(job) 110 | 111 | job = system_crontab.new(command=job_command, comment=job_comment) 112 | job.setall("0 0 */1 * *") # Run at 00:00 every day-of-month 113 | system_crontab.write() 114 | 115 | 116 | def create_dir_if_missing(path): 117 | if not os.path.exists(os.path.dirname(path)): 118 | os.makedirs(os.path.dirname(path)) 119 | 120 | 121 | def get_certbot_path(): 122 | try: 123 | return which("certbot", raise_err=True) 124 | except FileNotFoundError: 125 | raise CommandFailedError( 126 | "Certbot is not installed on your system. Please visit https://certbot.eff.org/instructions for installation instructions, then try again." 127 | ) 128 | 129 | 130 | def renew_certs(): 131 | # Needs to be run with sudo 132 | click.confirm( 133 | "Running this will stop the nginx service temporarily causing your sites to go offline\n" 134 | "Do you want to continue?", 135 | abort=True, 136 | ) 137 | 138 | setup_crontab() 139 | 140 | service("nginx", "stop") 141 | exec_cmd(f"{get_certbot_path()} renew") 142 | service("nginx", "start") 143 | 144 | 145 | def setup_wildcard_ssl(domain, email, bench_path, exclude_base_domain): 146 | def _get_domains(domain): 147 | domain_list = [domain] 148 | 149 | if not domain.startswith("*."): 150 | # add wildcard caracter to domain if missing 151 | domain_list.append(f"*.{domain}") 152 | else: 153 | # include base domain based on flag 154 | domain_list.append(domain.replace("*.", "")) 155 | 156 | if exclude_base_domain: 157 | domain_list.remove(domain.replace("*.", "")) 158 | 159 | return domain_list 160 | 161 | if not Bench(bench_path).conf.get("dns_multitenant"): 162 | print("You cannot setup SSL without DNS Multitenancy") 163 | return 164 | 165 | domain_list = _get_domains(domain.strip()) 166 | 167 | email_param = "" 168 | if email: 169 | email_param = f"--email {email}" 170 | 171 | try: 172 | exec_cmd( 173 | f"{get_certbot_path()} certonly --manual --preferred-challenges=dns {email_param} \ 174 | --server https://acme-v02.api.letsencrypt.org/directory \ 175 | --agree-tos -d {' -d '.join(domain_list)}" 176 | ) 177 | 178 | except CommandFailedError: 179 | print("There was a problem trying to setup SSL") 180 | return 181 | 182 | ssl_path = f"/etc/letsencrypt/live/{domain}/" 183 | ssl_config = { 184 | "wildcard": { 185 | "domain": domain, 186 | "ssl_certificate": os.path.join(ssl_path, "fullchain.pem"), 187 | "ssl_certificate_key": os.path.join(ssl_path, "privkey.pem"), 188 | } 189 | } 190 | 191 | update_common_site_config(ssl_config) 192 | setup_crontab() 193 | 194 | make_nginx_conf(bench_path) 195 | print("Restrting Nginx service") 196 | service("nginx", "restart") 197 | -------------------------------------------------------------------------------- /bench/config/procfile.py: -------------------------------------------------------------------------------- 1 | import os 2 | import platform 3 | 4 | import click 5 | 6 | import bench 7 | from bench.bench import Bench 8 | from bench.utils import which 9 | 10 | 11 | def setup_procfile(bench_path, yes=False, skip_redis=False, skip_web=False, skip_watch=None, skip_socketio=False, skip_schedule=False, with_coverage=False): 12 | if skip_watch is None: 13 | # backwards compatibilty; may be eventually removed 14 | skip_watch = os.environ.get("CI") 15 | config = Bench(bench_path).conf 16 | procfile_path = os.path.join(bench_path, "Procfile") 17 | 18 | is_mac = platform.system() == "Darwin" 19 | if not yes and os.path.exists(procfile_path): 20 | click.confirm( 21 | "A Procfile already exists and this will overwrite it. Do you want to continue?", 22 | abort=True, 23 | ) 24 | 25 | procfile = ( 26 | bench.config.env() 27 | .get_template("Procfile") 28 | .render( 29 | node=which("node") or which("nodejs"), 30 | webserver_port=config.get("webserver_port"), 31 | skip_redis=skip_redis, 32 | skip_web=skip_web, 33 | skip_watch=skip_watch, 34 | skip_socketio=skip_socketio, 35 | skip_schedule=skip_schedule, 36 | with_coverage=with_coverage, 37 | workers=config.get("workers", {}), 38 | is_mac=is_mac, 39 | ) 40 | ) 41 | 42 | with open(procfile_path, "w") as f: 43 | f.write(procfile) 44 | -------------------------------------------------------------------------------- /bench/config/production_setup.py: -------------------------------------------------------------------------------- 1 | # imports - standard imports 2 | import contextlib 3 | import os 4 | import logging 5 | import sys 6 | 7 | # imports - module imports 8 | import bench 9 | from bench.config.nginx import make_nginx_conf 10 | from bench.config.supervisor import ( 11 | generate_supervisor_config, 12 | check_supervisord_config, 13 | ) 14 | from bench.config.systemd import generate_systemd_config 15 | from bench.bench import Bench 16 | from bench.utils import exec_cmd, which, get_bench_name, get_cmd_output, log 17 | from bench.utils.system import fix_prod_setup_perms 18 | from bench.exceptions import CommandFailedError 19 | 20 | logger = logging.getLogger(bench.PROJECT_NAME) 21 | 22 | 23 | def setup_production_prerequisites(): 24 | """Installs ansible, fail2banc, NGINX and supervisor""" 25 | if not which("ansible"): 26 | exec_cmd(f"sudo {sys.executable} -m pip install ansible") 27 | if not which("fail2ban-client"): 28 | exec_cmd("bench setup role fail2ban") 29 | if not which("nginx"): 30 | exec_cmd("bench setup role nginx") 31 | if not which("supervisord"): 32 | exec_cmd("bench setup role supervisor") 33 | 34 | 35 | def setup_production(user, bench_path=".", yes=False): 36 | print("Setting Up prerequisites...") 37 | setup_production_prerequisites() 38 | 39 | conf = Bench(bench_path).conf 40 | 41 | if conf.get("restart_supervisor_on_update") and conf.get("restart_systemd_on_update"): 42 | raise Exception( 43 | "You cannot use supervisor and systemd at the same time. Modify your common_site_config accordingly." 44 | ) 45 | 46 | if conf.get("restart_systemd_on_update"): 47 | print("Setting Up systemd...") 48 | generate_systemd_config(bench_path=bench_path, user=user, yes=yes) 49 | else: 50 | print("Setting Up supervisor...") 51 | check_supervisord_config(user=user) 52 | generate_supervisor_config(bench_path=bench_path, user=user, yes=yes) 53 | 54 | print("Setting Up NGINX...") 55 | make_nginx_conf(bench_path=bench_path, yes=yes) 56 | fix_prod_setup_perms(bench_path, frappe_user=user) 57 | remove_default_nginx_configs() 58 | 59 | bench_name = get_bench_name(bench_path) 60 | nginx_conf = f"/etc/nginx/conf.d/{bench_name}.conf" 61 | 62 | print("Setting Up symlinks and reloading services...") 63 | if conf.get("restart_supervisor_on_update"): 64 | supervisor_conf_extn = "ini" if is_centos7() else "conf" 65 | supervisor_conf = os.path.join( 66 | get_supervisor_confdir(), f"{bench_name}.{supervisor_conf_extn}" 67 | ) 68 | 69 | # Check if symlink exists, If not then create it. 70 | if not os.path.islink(supervisor_conf): 71 | os.symlink( 72 | os.path.abspath(os.path.join(bench_path, "config", "supervisor.conf")), 73 | supervisor_conf, 74 | ) 75 | 76 | if not os.path.islink(nginx_conf): 77 | os.symlink( 78 | os.path.abspath(os.path.join(bench_path, "config", "nginx.conf")), nginx_conf 79 | ) 80 | 81 | if conf.get("restart_supervisor_on_update"): 82 | reload_supervisor() 83 | 84 | if os.environ.get("NO_SERVICE_RESTART"): 85 | return 86 | 87 | reload_nginx() 88 | 89 | 90 | def disable_production(bench_path="."): 91 | bench_name = get_bench_name(bench_path) 92 | conf = Bench(bench_path).conf 93 | 94 | # supervisorctl 95 | supervisor_conf_extn = "ini" if is_centos7() else "conf" 96 | supervisor_conf = os.path.join( 97 | get_supervisor_confdir(), f"{bench_name}.{supervisor_conf_extn}" 98 | ) 99 | 100 | if os.path.islink(supervisor_conf): 101 | os.unlink(supervisor_conf) 102 | 103 | if conf.get("restart_supervisor_on_update"): 104 | reload_supervisor() 105 | 106 | # nginx 107 | nginx_conf = f"/etc/nginx/conf.d/{bench_name}.conf" 108 | 109 | if os.path.islink(nginx_conf): 110 | os.unlink(nginx_conf) 111 | 112 | reload_nginx() 113 | 114 | 115 | def service(service_name, service_option): 116 | if os.path.basename(which("systemctl") or "") == "systemctl" and is_running_systemd(): 117 | exec_cmd(f"sudo systemctl {service_option} {service_name}") 118 | 119 | elif os.path.basename(which("service") or "") == "service": 120 | exec_cmd(f"sudo service {service_name} {service_option}") 121 | 122 | else: 123 | # look for 'service_manager' and 'service_manager_command' in environment 124 | service_manager = os.environ.get("BENCH_SERVICE_MANAGER") 125 | if service_manager: 126 | service_manager_command = ( 127 | os.environ.get("BENCH_SERVICE_MANAGER_COMMAND") 128 | or f"{service_manager} {service_option} {service}" 129 | ) 130 | exec_cmd(service_manager_command) 131 | 132 | else: 133 | log( 134 | f"No service manager found: '{service_name} {service_option}' failed to execute", 135 | level=2, 136 | ) 137 | 138 | 139 | def get_supervisor_confdir(): 140 | possiblities = ( 141 | "/etc/supervisor/conf.d", 142 | "/etc/supervisor.d/", 143 | "/etc/supervisord/conf.d", 144 | "/etc/supervisord.d", 145 | ) 146 | for possiblity in possiblities: 147 | if os.path.exists(possiblity): 148 | return possiblity 149 | 150 | 151 | def remove_default_nginx_configs(): 152 | default_nginx_configs = [ 153 | "/etc/nginx/conf.d/default.conf", 154 | "/etc/nginx/sites-enabled/default", 155 | ] 156 | 157 | for conf_file in default_nginx_configs: 158 | if os.path.exists(conf_file): 159 | os.unlink(conf_file) 160 | 161 | 162 | def is_centos7(): 163 | return ( 164 | os.path.exists("/etc/redhat-release") 165 | and get_cmd_output( 166 | r"cat /etc/redhat-release | sed 's/Linux\ //g' | cut -d' ' -f3 | cut -d. -f1" 167 | ).strip() 168 | == "7" 169 | ) 170 | 171 | 172 | def is_running_systemd(): 173 | with open("/proc/1/comm") as f: 174 | comm = f.read().strip() 175 | if comm == "init": 176 | return False 177 | elif comm == "systemd": 178 | return True 179 | return False 180 | 181 | 182 | def reload_supervisor(): 183 | supervisorctl = which("supervisorctl") 184 | 185 | with contextlib.suppress(CommandFailedError): 186 | # first try reread/update 187 | exec_cmd(f"{supervisorctl} reread") 188 | exec_cmd(f"{supervisorctl} update") 189 | return 190 | with contextlib.suppress(CommandFailedError): 191 | # something is wrong, so try reloading 192 | exec_cmd(f"{supervisorctl} reload") 193 | return 194 | with contextlib.suppress(CommandFailedError): 195 | # then try restart for centos 196 | service("supervisord", "restart") 197 | return 198 | with contextlib.suppress(CommandFailedError): 199 | # else try restart for ubuntu / debian 200 | service("supervisor", "restart") 201 | return 202 | 203 | 204 | def reload_nginx(): 205 | exec_cmd(f"sudo {which('nginx')} -t") 206 | service("nginx", "reload") 207 | -------------------------------------------------------------------------------- /bench/config/redis.py: -------------------------------------------------------------------------------- 1 | # imports - standard imports 2 | import os 3 | import re 4 | import subprocess 5 | 6 | # imports - module imports 7 | import bench 8 | 9 | 10 | def generate_config(bench_path): 11 | from urllib.parse import urlparse 12 | from bench.bench import Bench 13 | 14 | config = Bench(bench_path).conf 15 | redis_version = get_redis_version() 16 | 17 | ports = {} 18 | for key in ("redis_cache", "redis_queue"): 19 | ports[key] = urlparse(config[key]).port 20 | 21 | write_redis_config( 22 | template_name="redis_queue.conf", 23 | context={ 24 | "port": ports["redis_queue"], 25 | "bench_path": os.path.abspath(bench_path), 26 | "redis_version": redis_version, 27 | }, 28 | bench_path=bench_path, 29 | ) 30 | 31 | write_redis_config( 32 | template_name="redis_cache.conf", 33 | context={ 34 | "maxmemory": config.get("cache_maxmemory", get_max_redis_memory()), 35 | "port": ports["redis_cache"], 36 | "redis_version": redis_version, 37 | }, 38 | bench_path=bench_path, 39 | ) 40 | 41 | # make pids folder 42 | pid_path = os.path.join(bench_path, "config", "pids") 43 | if not os.path.exists(pid_path): 44 | os.makedirs(pid_path) 45 | 46 | # ACL feature is introduced in Redis 6.0 47 | if redis_version < 6.0: 48 | return 49 | 50 | # make ACL files 51 | acl_rq_path = os.path.join(bench_path, "config", "redis_queue.acl") 52 | acl_redis_cache_path = os.path.join(bench_path, "config", "redis_cache.acl") 53 | open(acl_rq_path, "a").close() 54 | open(acl_redis_cache_path, "a").close() 55 | 56 | 57 | def write_redis_config(template_name, context, bench_path): 58 | template = bench.config.env().get_template(template_name) 59 | 60 | if "config_path" not in context: 61 | context["config_path"] = os.path.abspath(os.path.join(bench_path, "config")) 62 | 63 | if "pid_path" not in context: 64 | context["pid_path"] = os.path.join(context["config_path"], "pids") 65 | 66 | with open(os.path.join(bench_path, "config", template_name), "w") as f: 67 | f.write(template.render(**context)) 68 | 69 | 70 | def get_redis_version(): 71 | import semantic_version 72 | 73 | version_string = subprocess.check_output("redis-server --version", shell=True) 74 | version_string = version_string.decode("utf-8").strip() 75 | # extract version number from string 76 | version = re.findall(r"\d+\.\d+", version_string) 77 | if not version: 78 | return None 79 | 80 | version = semantic_version.Version(version[0], partial=True) 81 | return float(f"{version.major}.{version.minor}") 82 | 83 | 84 | def get_max_redis_memory(): 85 | try: 86 | max_mem = os.sysconf("SC_PAGE_SIZE") * os.sysconf("SC_PHYS_PAGES") 87 | except ValueError: 88 | max_mem = int(subprocess.check_output(["sysctl", "-n", "hw.memsize"]).strip()) 89 | return max(50, int((max_mem / (1024.0**2)) * 0.05)) 90 | -------------------------------------------------------------------------------- /bench/config/site_config.py: -------------------------------------------------------------------------------- 1 | # imports - standard imports 2 | import json 3 | import os 4 | from collections import defaultdict 5 | 6 | 7 | def get_site_config(site, bench_path="."): 8 | config_path = os.path.join(bench_path, "sites", site, "site_config.json") 9 | if not os.path.exists(config_path): 10 | return {} 11 | with open(config_path) as f: 12 | return json.load(f) 13 | 14 | 15 | def put_site_config(site, config, bench_path="."): 16 | config_path = os.path.join(bench_path, "sites", site, "site_config.json") 17 | with open(config_path, "w") as f: 18 | return json.dump(config, f, indent=1) 19 | 20 | 21 | def update_site_config(site, new_config, bench_path="."): 22 | config = get_site_config(site, bench_path=bench_path) 23 | config.update(new_config) 24 | put_site_config(site, config, bench_path=bench_path) 25 | 26 | 27 | def set_nginx_port(site, port, bench_path=".", gen_config=True): 28 | set_site_config_nginx_property( 29 | site, {"nginx_port": port}, bench_path=bench_path, gen_config=gen_config 30 | ) 31 | 32 | 33 | def set_ssl_certificate(site, ssl_certificate, bench_path=".", gen_config=True): 34 | set_site_config_nginx_property( 35 | site, 36 | {"ssl_certificate": ssl_certificate}, 37 | bench_path=bench_path, 38 | gen_config=gen_config, 39 | ) 40 | 41 | 42 | def set_ssl_certificate_key(site, ssl_certificate_key, bench_path=".", gen_config=True): 43 | set_site_config_nginx_property( 44 | site, 45 | {"ssl_certificate_key": ssl_certificate_key}, 46 | bench_path=bench_path, 47 | gen_config=gen_config, 48 | ) 49 | 50 | 51 | def set_site_config_nginx_property(site, config, bench_path=".", gen_config=True): 52 | from bench.config.nginx import make_nginx_conf 53 | from bench.bench import Bench 54 | 55 | if site not in Bench(bench_path).sites: 56 | raise Exception("No such site") 57 | update_site_config(site, config, bench_path=bench_path) 58 | if gen_config: 59 | make_nginx_conf(bench_path=bench_path) 60 | 61 | 62 | def set_url_root(site, url_root, bench_path="."): 63 | update_site_config(site, {"host_name": url_root}, bench_path=bench_path) 64 | 65 | 66 | def add_domain(site, domain, ssl_certificate, ssl_certificate_key, bench_path="."): 67 | domains = get_domains(site, bench_path) 68 | for d in domains: 69 | if (isinstance(d, dict) and d["domain"] == domain) or d == domain: 70 | print(f"Domain {domain} already exists") 71 | return 72 | 73 | if ssl_certificate_key and ssl_certificate: 74 | domain = { 75 | "domain": domain, 76 | "ssl_certificate": ssl_certificate, 77 | "ssl_certificate_key": ssl_certificate_key, 78 | } 79 | 80 | domains.append(domain) 81 | update_site_config(site, {"domains": domains}, bench_path=bench_path) 82 | 83 | 84 | def remove_domain(site, domain, bench_path="."): 85 | domains = get_domains(site, bench_path) 86 | for i, d in enumerate(domains): 87 | if (isinstance(d, dict) and d["domain"] == domain) or d == domain: 88 | domains.remove(d) 89 | break 90 | 91 | update_site_config(site, {"domains": domains}, bench_path=bench_path) 92 | 93 | 94 | def sync_domains(site, domains, bench_path="."): 95 | """Checks if there is a change in domains. If yes, updates the domains list.""" 96 | changed = False 97 | existing_domains = get_domains_dict(get_domains(site, bench_path)) 98 | new_domains = get_domains_dict(domains) 99 | 100 | if set(existing_domains.keys()) != set(new_domains.keys()): 101 | changed = True 102 | 103 | else: 104 | for d in list(existing_domains.values()): 105 | if d != new_domains.get(d["domain"]): 106 | changed = True 107 | break 108 | 109 | if changed: 110 | # replace existing domains with this one 111 | update_site_config(site, {"domains": domains}, bench_path=".") 112 | 113 | return changed 114 | 115 | 116 | def get_domains(site, bench_path="."): 117 | return get_site_config(site, bench_path=bench_path).get("domains") or [] 118 | 119 | 120 | def get_domains_dict(domains): 121 | domains_dict = defaultdict(dict) 122 | for d in domains: 123 | if isinstance(d, str): 124 | domains_dict[d] = {"domain": d} 125 | 126 | elif isinstance(d, dict): 127 | domains_dict[d["domain"]] = d 128 | 129 | return domains_dict 130 | -------------------------------------------------------------------------------- /bench/config/supervisor.py: -------------------------------------------------------------------------------- 1 | # imports - standard imports 2 | import getpass 3 | import logging 4 | import os 5 | 6 | # imports - third party imports 7 | import click 8 | 9 | # imports - module imports 10 | import bench 11 | from bench.app import use_rq 12 | from bench.bench import Bench 13 | from bench.config.common_site_config import ( 14 | compute_max_requests_jitter, 15 | get_config, 16 | get_default_max_requests, 17 | get_gunicorn_workers, 18 | update_config, 19 | ) 20 | from bench.utils import get_bench_name, which 21 | 22 | logger = logging.getLogger(bench.PROJECT_NAME) 23 | 24 | 25 | def generate_supervisor_config(bench_path, user=None, yes=False, skip_redis=False): 26 | """Generate supervisor config for respective bench path""" 27 | if not user: 28 | user = getpass.getuser() 29 | 30 | config = Bench(bench_path).conf 31 | template = bench.config.env().get_template("supervisor.conf") 32 | bench_dir = os.path.abspath(bench_path) 33 | 34 | web_worker_count = config.get( 35 | "gunicorn_workers", get_gunicorn_workers()["gunicorn_workers"] 36 | ) 37 | max_requests = config.get( 38 | "gunicorn_max_requests", get_default_max_requests(web_worker_count) 39 | ) 40 | 41 | config = template.render( 42 | **{ 43 | "bench_dir": bench_dir, 44 | "sites_dir": os.path.join(bench_dir, "sites"), 45 | "user": user, 46 | "use_rq": use_rq(bench_path), 47 | "http_timeout": config.get("http_timeout", 120), 48 | "redis_server": which("redis-server"), 49 | "node": which("node") or which("nodejs"), 50 | "redis_cache_config": os.path.join(bench_dir, "config", "redis_cache.conf"), 51 | "redis_queue_config": os.path.join(bench_dir, "config", "redis_queue.conf"), 52 | "webserver_port": config.get("webserver_port", 8000), 53 | "gunicorn_workers": web_worker_count, 54 | "gunicorn_max_requests": max_requests, 55 | "gunicorn_max_requests_jitter": compute_max_requests_jitter(max_requests), 56 | "bench_name": get_bench_name(bench_path), 57 | "background_workers": config.get("background_workers") or 1, 58 | "bench_cmd": which("bench"), 59 | "skip_redis": skip_redis, 60 | "workers": config.get("workers", {}), 61 | "multi_queue_consumption": can_enable_multi_queue_consumption(bench_path), 62 | "supervisor_startretries": 10, 63 | } 64 | ) 65 | 66 | conf_path = os.path.join(bench_path, "config", "supervisor.conf") 67 | if not yes and os.path.exists(conf_path): 68 | click.confirm( 69 | "supervisor.conf already exists and this will overwrite it. Do you want to continue?", 70 | abort=True, 71 | ) 72 | 73 | with open(conf_path, "w") as f: 74 | f.write(config) 75 | 76 | update_config({"restart_supervisor_on_update": True}, bench_path=bench_path) 77 | update_config({"restart_systemd_on_update": False}, bench_path=bench_path) 78 | sync_socketio_port(bench_path) 79 | 80 | 81 | def get_supervisord_conf(): 82 | """Returns path of supervisord config from possible paths""" 83 | possibilities = ( 84 | "supervisord.conf", 85 | "etc/supervisord.conf", 86 | "/etc/supervisord.conf", 87 | "/etc/supervisor/supervisord.conf", 88 | "/etc/supervisord.conf", 89 | ) 90 | 91 | for possibility in possibilities: 92 | if os.path.exists(possibility): 93 | return possibility 94 | 95 | 96 | def sync_socketio_port(bench_path): 97 | # Backward compatbility: always keep redis_cache and redis_socketio port same 98 | common_config = get_config(bench_path=bench_path) 99 | 100 | socketio_port = common_config.get("redis_socketio") 101 | cache_port = common_config.get("redis_cache") 102 | if socketio_port and socketio_port != cache_port: 103 | update_config({"redis_socketio": cache_port}) 104 | 105 | 106 | def can_enable_multi_queue_consumption(bench_path: str) -> bool: 107 | try: 108 | from semantic_version import Version 109 | 110 | from bench.utils.app import get_current_version 111 | 112 | supported_version = Version(major=14, minor=18, patch=0) 113 | 114 | frappe_version = Version(get_current_version("frappe", bench_path=bench_path)) 115 | 116 | return frappe_version > supported_version 117 | except Exception: 118 | return False 119 | 120 | 121 | def check_supervisord_config(user=None): 122 | """From bench v5.x, we're moving to supervisor running as user""" 123 | # i don't think bench should be responsible for this but we're way past this now... 124 | # removed updating supervisord conf & reload in Aug 2022 - gavin@frappe.io 125 | import configparser 126 | 127 | if not user: 128 | user = getpass.getuser() 129 | 130 | supervisord_conf = get_supervisord_conf() 131 | section = "unix_http_server" 132 | updated_values = {"chmod": "0760", "chown": f"{user}:{user}"} 133 | supervisord_conf_changes = "" 134 | 135 | if not supervisord_conf: 136 | logger.log("supervisord.conf not found") 137 | return 138 | 139 | config = configparser.ConfigParser() 140 | config.read(supervisord_conf) 141 | 142 | if section not in config.sections(): 143 | config.add_section(section) 144 | action = f"Section {section} Added" 145 | logger.log(action) 146 | supervisord_conf_changes += "\n" + action 147 | 148 | for key, value in updated_values.items(): 149 | try: 150 | current_value = config.get(section, key) 151 | except configparser.NoOptionError: 152 | current_value = "" 153 | 154 | if current_value.strip() != value: 155 | config.set(section, key, value) 156 | action = ( 157 | f"Updated supervisord.conf: '{key}' changed from '{current_value}' to '{value}'" 158 | ) 159 | logger.log(action) 160 | supervisord_conf_changes += "\n" + action 161 | 162 | if not supervisord_conf_changes: 163 | logger.error("supervisord.conf not updated") 164 | contents = "\n".join(f"{x}={y}" for x, y in updated_values.items()) 165 | print( 166 | f"Update your {supervisord_conf} with the following values:\n[{section}]\n{contents}" 167 | ) 168 | -------------------------------------------------------------------------------- /bench/config/templates/502.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Sorry! We will be back soon. 6 | 68 | 69 | 70 |
71 |
72 | sad-face-avatar-boy-man-11Created with Sketch. 73 |
74 |
75 |

76 | Sorry!
77 | We will be back soon. 78 |

79 |

80 | Don't panic. It's not you, it's us.
81 | Most likely, our engineers are updating the code, 82 | and it should take a minute for the new code to load into memory.

83 | Try refreshing after a minute or two. 84 |

85 |
86 |
87 |
88 | 89 | 90 | -------------------------------------------------------------------------------- /bench/config/templates/Procfile: -------------------------------------------------------------------------------- 1 | {% if not skip_redis %} 2 | redis_cache: redis-server config/redis_cache.conf 3 | redis_queue: redis-server config/redis_queue.conf 4 | {% endif %} 5 | {% if not skip_web %} 6 | web: bench serve {% if with_coverage -%} --with-coverage {%- endif %} {% if webserver_port -%} --port {{ webserver_port }} {%- endif %} 7 | {% endif %} 8 | {% if not skip_socketio %} 9 | socketio: {{ node }} apps/frappe/socketio.js 10 | {% endif %} 11 | {% if not skip_watch %} 12 | watch: bench watch 13 | {% endif %} 14 | {% if not skip_schedule %} 15 | schedule: bench schedule 16 | {% endif %} 17 | worker: {{ 'OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES NO_PROXY=*' if is_mac else '' }} bench worker 1>> logs/worker.log 2>> logs/worker.error.log 18 | {% for worker_name, worker_details in workers.items() %} 19 | worker_{{ worker_name }}: {{ 'OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES NO_PROXY=*' if is_mac else '' }} bench worker --queue {{ worker_name }} 1>> logs/worker.log 2>> logs/worker.error.log 20 | {% endfor %} 21 | 22 | -------------------------------------------------------------------------------- /bench/config/templates/bench_manager_nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen {{ port }}; 3 | server_name {{ domain }}; 4 | root {{ sites_path }}; 5 | 6 | 7 | {% if ssl_certificate and ssl_certificate_key %} 8 | ssl on; 9 | ssl_certificate {{ ssl_certificate }}; 10 | ssl_certificate_key {{ ssl_certificate_key }}; 11 | ssl_session_timeout 5m; 12 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2; 13 | ssl_ciphers "EECDH+ECDSA+AESGCM EECDH+aRSA+AESGCM EECDH+ECDSA+SHA384 EECDH+ECDSA+SHA256 EECDH+aRSA+SHA384 EECDH+aRSA+SHA256 EECDH+aRSA+RC4 EECDH EDH+aRSA RC4 !aNULL !eNULL !LOW !3DES !MD5 !EXP !PSK !SRP !DSS"; 14 | ssl_prefer_server_ciphers on; 15 | {% endif %} 16 | 17 | location /assets { 18 | try_files $uri =404; 19 | } 20 | 21 | location ~ ^/protected/(.*) { 22 | internal; 23 | try_files /{{ bench_manager_site_name }}/$1 =404; 24 | } 25 | 26 | location /socket.io { 27 | proxy_http_version 1.1; 28 | proxy_set_header Upgrade $http_upgrade; 29 | proxy_set_header Connection "upgrade"; 30 | proxy_set_header X-Frappe-Site-Name {{ bench_manager_site_name }}; 31 | proxy_set_header Origin $scheme://$http_host; 32 | proxy_set_header Host {{ bench_manager_site_name }}; 33 | 34 | proxy_pass http://{{ bench_name }}-socketio-server; 35 | } 36 | 37 | location / { 38 | try_files /{{ bench_manager_site_name }}/public/$uri @webserver; 39 | } 40 | 41 | location @webserver { 42 | proxy_set_header X-Forwarded-For $remote_addr; 43 | proxy_set_header X-Forwarded-Proto $scheme; 44 | proxy_set_header X-Frappe-Site-Name {{ bench_manager_site_name }}; 45 | proxy_set_header Host {{ bench_manager_site_name }}; 46 | proxy_set_header X-Use-X-Accel-Redirect True; 47 | proxy_read_timeout {{ http_timeout or 120 }}; 48 | proxy_redirect off; 49 | 50 | proxy_pass http://{{ bench_name }}-frappe; 51 | } 52 | 53 | # error pages 54 | {% for error_code, error_page in error_pages.items() -%} 55 | 56 | error_page {{ error_code }} /{{ error_page.split('/')[-1] }}; 57 | location /{{ error_code }}.html { 58 | root {{ '/'.join(error_page.split('/')[:-1]) }}; 59 | internal; 60 | } 61 | 62 | {% endfor -%} 63 | 64 | # optimizations 65 | sendfile on; 66 | keepalive_timeout 15; 67 | client_max_body_size 50m; 68 | client_body_buffer_size 16K; 69 | client_header_buffer_size 1k; 70 | 71 | # enable gzip compresion 72 | # based on https://mattstauffer.co/blog/enabling-gzip-on-nginx-servers-including-laravel-forge 73 | gzip on; 74 | gzip_http_version 1.1; 75 | gzip_comp_level 5; 76 | gzip_min_length 256; 77 | gzip_proxied any; 78 | gzip_vary on; 79 | gzip_types 80 | application/atom+xml 81 | application/javascript 82 | application/json 83 | application/rss+xml 84 | application/vnd.ms-fontobject 85 | application/x-font-ttf 86 | application/font-woff 87 | application/x-web-app-manifest+json 88 | application/xhtml+xml 89 | application/xml 90 | font/opentype 91 | image/svg+xml 92 | image/x-icon 93 | text/css 94 | text/plain 95 | text/x-component 96 | ; 97 | # text/html is always compressed by HttpGzipModule 98 | } 99 | 100 | 101 | -------------------------------------------------------------------------------- /bench/config/templates/frappe_sudoers: -------------------------------------------------------------------------------- 1 | # This file is auto-generated by frappe/bench 2 | # To re-generate this file, run "bench setup sudoers" 3 | 4 | {% if service %} 5 | {{ user }} ALL = (root) {{ service }} 6 | {{ user }} ALL = (root) NOPASSWD: {{ service }} nginx * 7 | {% endif %} 8 | 9 | {% if systemctl %} 10 | {{ user }} ALL = (root) {{ systemctl }} 11 | {{ user }} ALL = (root) NOPASSWD: {{ systemctl }} * nginx 12 | {% endif %} 13 | 14 | {% if nginx %} 15 | {{ user }} ALL = (root) NOPASSWD: {{ nginx }} 16 | {% endif %} 17 | 18 | {{ user }} ALL = (root) NOPASSWD: {{ certbot }} 19 | Defaults:{{ user }} !requiretty 20 | 21 | -------------------------------------------------------------------------------- /bench/config/templates/letsencrypt.cfg: -------------------------------------------------------------------------------- 1 | # This is an example of the kind of things you can do in a configuration file. 2 | # All flags used by the client can be configured here. Run Certbot with 3 | # "--help" to learn more about the available options. 4 | 5 | # Use a 4096 bit RSA key instead of 2048 6 | rsa-key-size = 4096 7 | 8 | # Uncomment and update to register with the specified e-mail address 9 | #email = email@domain.com 10 | 11 | # Uncomment and update to generate certificates for the specified 12 | # domains. 13 | domains = {{ domain }} 14 | 15 | # Uncomment to use a text interface instead of ncurses 16 | text = True 17 | 18 | # Uncomment to use the standalone authenticator on port 443 19 | authenticator = standalone 20 | -------------------------------------------------------------------------------- /bench/config/templates/nginx_default.conf: -------------------------------------------------------------------------------- 1 | # For more information on configuration, see: 2 | # * Official English Documentation: http://nginx.org/en/docs/ 3 | # * Official Russian Documentation: http://nginx.org/ru/docs/ 4 | 5 | user nginx; 6 | worker_processes 1; 7 | 8 | error_log /var/log/nginx/error.log; 9 | #error_log /var/log/nginx/error.log notice; 10 | #error_log /var/log/nginx/error.log info; 11 | 12 | pid /run/nginx.pid; 13 | 14 | 15 | events { 16 | worker_connections 1024; 17 | } 18 | 19 | 20 | http { 21 | include /etc/nginx/mime.types; 22 | default_type application/octet-stream; 23 | 24 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 25 | '$status $body_bytes_sent "$http_referer" ' 26 | '"$http_user_agent" "$http_x_forwarded_for"'; 27 | 28 | access_log /var/log/nginx/access.log main; 29 | 30 | sendfile on; 31 | #tcp_nopush on; 32 | 33 | #keepalive_timeout 0; 34 | keepalive_timeout 65; 35 | 36 | server_names_hash_bucket_size 64; 37 | 38 | #gzip on; 39 | 40 | index index.html index.htm; 41 | 42 | # Load modular configuration files from the /etc/nginx/conf.d directory. 43 | # See http://nginx.org/en/docs/ngx_core_module.html#include 44 | # for more information. 45 | include /etc/nginx/conf.d/*.conf; 46 | } 47 | -------------------------------------------------------------------------------- /bench/config/templates/redis_cache.conf: -------------------------------------------------------------------------------- 1 | dbfilename redis_cache.rdb 2 | dir {{ pid_path }} 3 | pidfile {{ pid_path }}/redis_cache.pid 4 | bind 127.0.0.1 5 | port {{ port }} 6 | maxmemory {{ maxmemory }}mb 7 | maxmemory-policy allkeys-lru 8 | appendonly no 9 | {% if redis_version and redis_version >= 2.2 %} 10 | save "" 11 | {% endif %} 12 | {% if redis_version and redis_version >= 6.0 %} 13 | aclfile {{ config_path }}/redis_cache.acl 14 | {% endif %} 15 | -------------------------------------------------------------------------------- /bench/config/templates/redis_queue.conf: -------------------------------------------------------------------------------- 1 | dbfilename redis_queue.rdb 2 | dir {{ pid_path }} 3 | pidfile {{ pid_path }}/redis_queue.pid 4 | bind 127.0.0.1 5 | port {{ port }} 6 | {% if redis_version and redis_version >= 6.0 %} 7 | aclfile {{ config_path }}/redis_queue.acl 8 | {% endif %} 9 | -------------------------------------------------------------------------------- /bench/config/templates/supervisor.conf: -------------------------------------------------------------------------------- 1 | ; Notes: 2 | ; priority=1 --> Lower priorities indicate programs that start first and shut down last 3 | ; killasgroup=true --> send kill signal to child processes too 4 | 5 | ; graceful timeout should always be lower than stopwaitsecs to avoid orphan gunicorn workers. 6 | [program:{{ bench_name }}-frappe-web] 7 | command={{ bench_dir }}/env/bin/gunicorn -b 127.0.0.1:{{ webserver_port }} -w {{ gunicorn_workers }} --max-requests {{ gunicorn_max_requests }} --max-requests-jitter {{ gunicorn_max_requests_jitter }} -t {{ http_timeout }} --graceful-timeout 30 frappe.app:application --preload 8 | priority=4 9 | autostart=true 10 | autorestart=true 11 | stdout_logfile={{ bench_dir }}/logs/web.log 12 | stderr_logfile={{ bench_dir }}/logs/web.error.log 13 | stopwaitsecs=40 14 | killasgroup=true 15 | user={{ user }} 16 | directory={{ sites_dir }} 17 | startretries={{ supervisor_startretries }} 18 | 19 | [program:{{ bench_name }}-frappe-schedule] 20 | command={{ bench_cmd }} schedule 21 | priority=3 22 | autostart=true 23 | stdout_logfile={{ bench_dir }}/logs/schedule.log 24 | stderr_logfile={{ bench_dir }}/logs/schedule.error.log 25 | user={{ user }} 26 | directory={{ bench_dir }} 27 | startretries={{ supervisor_startretries }} 28 | 29 | {% if not multi_queue_consumption %} 30 | [program:{{ bench_name }}-frappe-default-worker] 31 | command={{ bench_cmd }} worker --queue default 32 | priority=4 33 | autostart=true 34 | autorestart=true 35 | stdout_logfile={{ bench_dir }}/logs/worker.log 36 | stderr_logfile={{ bench_dir }}/logs/worker.error.log 37 | user={{ user }} 38 | stopwaitsecs=1560 39 | directory={{ bench_dir }} 40 | killasgroup=true 41 | numprocs={{ background_workers }} 42 | process_name=%(program_name)s-%(process_num)d 43 | startretries={{ supervisor_startretries }} 44 | {% endif %} 45 | 46 | [program:{{ bench_name }}-frappe-short-worker] 47 | command={{ bench_cmd }} worker --queue short{{',default' if multi_queue_consumption else ''}} 48 | priority=4 49 | autostart=true 50 | autorestart=true 51 | stdout_logfile={{ bench_dir }}/logs/worker.log 52 | stderr_logfile={{ bench_dir }}/logs/worker.error.log 53 | user={{ user }} 54 | stopwaitsecs=360 55 | directory={{ bench_dir }} 56 | killasgroup=true 57 | numprocs={{ background_workers }} 58 | process_name=%(program_name)s-%(process_num)d 59 | startretries={{ supervisor_startretries }} 60 | 61 | [program:{{ bench_name }}-frappe-long-worker] 62 | command={{ bench_cmd }} worker --queue long{{',default,short' if multi_queue_consumption else ''}} 63 | priority=4 64 | autostart=true 65 | autorestart=true 66 | stdout_logfile={{ bench_dir }}/logs/worker.log 67 | stderr_logfile={{ bench_dir }}/logs/worker.error.log 68 | user={{ user }} 69 | stopwaitsecs=1560 70 | directory={{ bench_dir }} 71 | killasgroup=true 72 | numprocs={{ background_workers }} 73 | process_name=%(program_name)s-%(process_num)d 74 | startretries={{ supervisor_startretries }} 75 | 76 | {% for worker_name, worker_details in workers.items() %} 77 | [program:{{ bench_name }}-frappe-{{ worker_name }}-worker] 78 | command={{ bench_cmd }} worker --queue {{ worker_name }} 79 | priority=4 80 | autostart=true 81 | autorestart=true 82 | stdout_logfile={{ bench_dir }}/logs/worker.log 83 | stderr_logfile={{ bench_dir }}/logs/worker.error.log 84 | user={{ user }} 85 | stopwaitsecs={{ worker_details["timeout"] }} 86 | directory={{ bench_dir }} 87 | killasgroup=true 88 | numprocs={{ worker_details["background_workers"] or background_workers }} 89 | process_name=%(program_name)s-%(process_num)d 90 | startretries={{ supervisor_startretries }} 91 | {% endfor %} 92 | 93 | 94 | {% if not skip_redis %} 95 | [program:{{ bench_name }}-redis-cache] 96 | command={{ redis_server }} {{ redis_cache_config }} 97 | priority=1 98 | autostart=true 99 | autorestart=true 100 | stdout_logfile={{ bench_dir }}/logs/redis-cache.log 101 | stderr_logfile={{ bench_dir }}/logs/redis-cache.error.log 102 | user={{ user }} 103 | directory={{ sites_dir }} 104 | startretries={{ supervisor_startretries }} 105 | 106 | [program:{{ bench_name }}-redis-queue] 107 | command={{ redis_server }} {{ redis_queue_config }} 108 | priority=1 109 | autostart=true 110 | autorestart=true 111 | stdout_logfile={{ bench_dir }}/logs/redis-queue.log 112 | stderr_logfile={{ bench_dir }}/logs/redis-queue.error.log 113 | user={{ user }} 114 | directory={{ sites_dir }} 115 | startretries={{ supervisor_startretries }} 116 | {% endif %} 117 | 118 | {% if node %} 119 | [program:{{ bench_name }}-node-socketio] 120 | command={{ node }} {{ bench_dir }}/apps/frappe/socketio.js 121 | priority=4 122 | autostart=true 123 | autorestart=true 124 | stdout_logfile={{ bench_dir }}/logs/node-socketio.log 125 | stderr_logfile={{ bench_dir }}/logs/node-socketio.error.log 126 | user={{ user }} 127 | directory={{ bench_dir }} 128 | startretries={{ supervisor_startretries }} 129 | {% endif %} 130 | 131 | [group:{{ bench_name }}-web] 132 | programs={{ bench_name }}-frappe-web {%- if node -%} ,{{ bench_name }}-node-socketio {%- endif%} 133 | 134 | 135 | {% if multi_queue_consumption %} 136 | 137 | [group:{{ bench_name }}-workers] 138 | programs={{ bench_name }}-frappe-schedule,{{ bench_name }}-frappe-short-worker,{{ bench_name }}-frappe-long-worker{%- for worker_name in workers -%},{{ bench_name }}-frappe-{{ worker_name }}-worker{%- endfor %} 139 | 140 | {% else %} 141 | 142 | [group:{{ bench_name }}-workers] 143 | programs={{ bench_name }}-frappe-schedule,{{ bench_name }}-frappe-default-worker,{{ bench_name }}-frappe-short-worker,{{ bench_name }}-frappe-long-worker{%- for worker_name in workers -%},{{ bench_name }}-frappe-{{ worker_name }}-worker{%- endfor %} 144 | 145 | {% endif %} 146 | 147 | {% if not skip_redis %} 148 | [group:{{ bench_name }}-redis] 149 | programs={{ bench_name }}-redis-cache,{{ bench_name }}-redis-queue 150 | {% endif %} 151 | -------------------------------------------------------------------------------- /bench/config/templates/systemd/frappe-bench-frappe-default-worker.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description="{{ bench_name }}-frappe-default-worker %I" 3 | PartOf={{ bench_name }}-workers.target 4 | 5 | [Service] 6 | User={{ user }} 7 | Group={{ user }} 8 | Restart=always 9 | ExecStart={{ bench_cmd }} worker --queue default 10 | StandardOutput=file:{{ bench_dir }}/logs/worker.log 11 | StandardError=file:{{ bench_dir }}/logs/worker.error.log 12 | WorkingDirectory={{ bench_dir }} 13 | -------------------------------------------------------------------------------- /bench/config/templates/systemd/frappe-bench-frappe-long-worker.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description="{{ bench_name }}-frappe-short-worker %I" 3 | PartOf={{ bench_name }}-workers.target 4 | 5 | [Service] 6 | User={{ user }} 7 | Group={{ user }} 8 | Restart=always 9 | ExecStart={{ bench_cmd }} worker --queue long 10 | StandardOutput=file:{{ bench_dir }}/logs/worker.log 11 | StandardError=file:{{ bench_dir }}/logs/worker.error.log 12 | WorkingDirectory={{ bench_dir }} 13 | -------------------------------------------------------------------------------- /bench/config/templates/systemd/frappe-bench-frappe-schedule.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description="{{ bench_name }}-frappe-schedule" 3 | PartOf={{ bench_name }}-workers.target 4 | 5 | [Service] 6 | User={{ user }} 7 | Group={{ user }} 8 | Restart=always 9 | ExecStart={{ bench_cmd }} schedule 10 | StandardOutput=file:{{ bench_dir }}/logs/schedule.log 11 | StandardError=file:{{ bench_dir }}/logs/schedule.error.log 12 | WorkingDirectory={{ bench_dir }} 13 | -------------------------------------------------------------------------------- /bench/config/templates/systemd/frappe-bench-frappe-short-worker.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description="{{ bench_name }}-frappe-short-worker %I" 3 | PartOf={{ bench_name }}-workers.target 4 | 5 | [Service] 6 | User={{ user }} 7 | Group={{ user }} 8 | Restart=always 9 | ExecStart={{ bench_cmd }} worker --queue short 10 | StandardOutput=file:{{ bench_dir }}/logs/worker.log 11 | StandardError=file:{{ bench_dir }}/logs/worker.error.log 12 | WorkingDirectory={{ bench_dir }} 13 | -------------------------------------------------------------------------------- /bench/config/templates/systemd/frappe-bench-frappe-web.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description="{{ bench_name }}-frappe-web" 3 | PartOf={{ bench_name }}-web.target 4 | 5 | [Service] 6 | User={{ user }} 7 | Group={{ user }} 8 | Restart=always 9 | ExecStart={{ bench_dir }}/env/bin/gunicorn -b 127.0.0.1:{{ webserver_port }} -w {{ gunicorn_workers }} -t {{ http_timeout }} --max-requests {{ gunicorn_max_requests }} --max-requests-jitter {{ gunicorn_max_requests_jitter }} frappe.app:application --preload 10 | StandardOutput=file:{{ bench_dir }}/logs/web.log 11 | StandardError=file:{{ bench_dir }}/logs/web.error.log 12 | WorkingDirectory={{ sites_dir }} 13 | -------------------------------------------------------------------------------- /bench/config/templates/systemd/frappe-bench-node-socketio.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | After={{ bench_name }}-frappe-web.service 3 | Description="{{ bench_name }}-node-socketio" 4 | PartOf={{ bench_name }}-web.target 5 | 6 | [Service] 7 | User={{ user }} 8 | Group={{ user }} 9 | Restart=always 10 | ExecStart={{ node }} {{ bench_dir }}/apps/frappe/socketio.js 11 | StandardOutput=file:{{ bench_dir }}/logs/node-socketio.log 12 | StandardError=file:{{ bench_dir }}/logs/node-socketio.error.log 13 | WorkingDirectory={{ bench_dir }} 14 | -------------------------------------------------------------------------------- /bench/config/templates/systemd/frappe-bench-redis-cache.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description="{{ bench_name }}-redis-cache" 3 | PartOf={{ bench_name }}-redis.target 4 | 5 | [Service] 6 | User={{ user }} 7 | Group={{ user }} 8 | Restart=always 9 | ExecStart={{ redis_server }} {{ redis_cache_config }} 10 | StandardOutput=file:{{ bench_dir }}/logs/redis-cache.log 11 | StandardError=file:{{ bench_dir }}/logs/redis-cache.error.log 12 | WorkingDirectory={{ sites_dir }} 13 | -------------------------------------------------------------------------------- /bench/config/templates/systemd/frappe-bench-redis-queue.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description="{{ bench_name }}-redis-queue" 3 | PartOf={{ bench_name }}-redis.target 4 | 5 | [Service] 6 | User={{ user }} 7 | Group={{ user }} 8 | Restart=always 9 | ExecStart={{ redis_server }} {{ redis_queue_config }} 10 | StandardOutput=file:{{ bench_dir }}/logs/redis-queue.log 11 | StandardError=file:{{ bench_dir }}/logs/redis-queue.error.log 12 | WorkingDirectory={{ sites_dir }} 13 | -------------------------------------------------------------------------------- /bench/config/templates/systemd/frappe-bench-redis.target: -------------------------------------------------------------------------------- 1 | [Unit] 2 | After=network.target 3 | Wants={{ bench_name }}-redis-cache.service {{ bench_name }}-redis-queue.service 4 | 5 | [Install] 6 | WantedBy=multi-user.target 7 | -------------------------------------------------------------------------------- /bench/config/templates/systemd/frappe-bench-web.target: -------------------------------------------------------------------------------- 1 | [Unit] 2 | After=network.target 3 | Wants={{ bench_name }}-frappe-web.service {{ bench_name }}-node-socketio.service 4 | 5 | [Install] 6 | WantedBy=multi-user.target 7 | -------------------------------------------------------------------------------- /bench/config/templates/systemd/frappe-bench-workers.target: -------------------------------------------------------------------------------- 1 | [Unit] 2 | After=network.target 3 | Wants={{ worker_target_wants }} 4 | 5 | [Install] 6 | WantedBy=multi-user.target 7 | -------------------------------------------------------------------------------- /bench/config/templates/systemd/frappe-bench.target: -------------------------------------------------------------------------------- 1 | [Unit] 2 | After=network.target 3 | Requires={{ bench_name }}-web.target {{ bench_name }}-workers.target {{ bench_name }}-redis.target 4 | 5 | [Install] 6 | WantedBy=multi-user.target 7 | -------------------------------------------------------------------------------- /bench/exceptions.py: -------------------------------------------------------------------------------- 1 | class InvalidBranchException(Exception): 2 | pass 3 | 4 | 5 | class InvalidRemoteException(Exception): 6 | pass 7 | 8 | 9 | class PatchError(Exception): 10 | pass 11 | 12 | 13 | class CommandFailedError(Exception): 14 | pass 15 | 16 | 17 | class BenchNotFoundError(Exception): 18 | pass 19 | 20 | 21 | class ValidationError(Exception): 22 | pass 23 | 24 | 25 | class AppNotInstalledError(ValidationError): 26 | pass 27 | 28 | 29 | class CannotUpdateReleaseBench(ValidationError): 30 | pass 31 | 32 | 33 | class FeatureDoesNotExistError(CommandFailedError): 34 | pass 35 | 36 | class VersionNotFound(Exception): 37 | pass 38 | -------------------------------------------------------------------------------- /bench/patches/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import importlib 3 | 4 | 5 | def run(bench_path): 6 | source_patch_file = os.path.join( 7 | os.path.dirname(os.path.abspath(__file__)), "patches.txt" 8 | ) 9 | target_patch_file = os.path.join(os.path.abspath(bench_path), "patches.txt") 10 | 11 | with open(source_patch_file) as f: 12 | patches = [ 13 | p.strip() 14 | for p in f.read().splitlines() 15 | if p.strip() and not p.strip().startswith("#") 16 | ] 17 | 18 | executed_patches = [] 19 | if os.path.exists(target_patch_file): 20 | with open(target_patch_file) as f: 21 | executed_patches = f.read().splitlines() 22 | 23 | try: 24 | for patch in patches: 25 | if patch not in executed_patches: 26 | module = importlib.import_module(patch.split()[0]) 27 | execute = getattr(module, "execute") 28 | result = execute(bench_path) 29 | 30 | if not result: 31 | executed_patches.append(patch) 32 | 33 | finally: 34 | with open(target_patch_file, "w") as f: 35 | f.write("\n".join(executed_patches)) 36 | 37 | # end with an empty line 38 | f.write("\n") 39 | -------------------------------------------------------------------------------- /bench/patches/patches.txt: -------------------------------------------------------------------------------- 1 | bench.patches.v3.deprecate_old_config 2 | bench.patches.v3.celery_to_rq 3 | bench.patches.v3.redis_bind_ip 4 | bench.patches.v4.update_node 5 | bench.patches.v4.update_socketio 6 | bench.patches.v4.install_yarn #2 7 | bench.patches.v5.fix_user_permissions 8 | bench.patches.v5.fix_backup_cronjob 9 | bench.patches.v5.set_live_reload_config 10 | bench.patches.v5.update_archived_sites -------------------------------------------------------------------------------- /bench/patches/v5/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/bench/d771849e8ce16e85351904abbf0197c24bda8bfc/bench/patches/v5/__init__.py -------------------------------------------------------------------------------- /bench/patches/v5/fix_backup_cronjob.py: -------------------------------------------------------------------------------- 1 | from bench.config.common_site_config import get_config 2 | from crontab import CronTab 3 | 4 | 5 | def execute(bench_path): 6 | """ 7 | This patch fixes a cron job that would backup sites every minute per 6 hours 8 | """ 9 | 10 | user = get_config(bench_path=bench_path).get("frappe_user") 11 | user_crontab = CronTab(user=user) 12 | 13 | for job in user_crontab.find_comment("bench auto backups set for every 6 hours"): 14 | job.every(6).hours() 15 | user_crontab.write() 16 | -------------------------------------------------------------------------------- /bench/patches/v5/fix_user_permissions.py: -------------------------------------------------------------------------------- 1 | # imports - standard imports 2 | import getpass 3 | import os 4 | import subprocess 5 | 6 | # imports - module imports 7 | from bench.cli import change_uid_msg 8 | from bench.config.production_setup import get_supervisor_confdir, is_centos7, service 9 | from bench.config.common_site_config import get_config 10 | from bench.utils import exec_cmd, get_bench_name, get_cmd_output 11 | 12 | 13 | def is_sudoers_set(): 14 | """Check if bench sudoers is set""" 15 | cmd = ["sudo", "-n", "bench"] 16 | bench_warn = False 17 | 18 | with open(os.devnull, "wb") as f: 19 | return_code_check = not subprocess.call(cmd, stdout=f) 20 | 21 | if return_code_check: 22 | try: 23 | bench_warn = change_uid_msg in get_cmd_output(cmd, _raise=False) 24 | except subprocess.CalledProcessError: 25 | bench_warn = False 26 | finally: 27 | return_code_check = return_code_check and bench_warn 28 | 29 | return return_code_check 30 | 31 | 32 | def is_production_set(bench_path): 33 | """Check if production is set for current bench""" 34 | production_setup = False 35 | bench_name = get_bench_name(bench_path) 36 | 37 | supervisor_conf_extn = "ini" if is_centos7() else "conf" 38 | supervisor_conf_file_name = f"{bench_name}.{supervisor_conf_extn}" 39 | supervisor_conf = os.path.join(get_supervisor_confdir(), supervisor_conf_file_name) 40 | 41 | if os.path.exists(supervisor_conf): 42 | production_setup = production_setup or True 43 | 44 | nginx_conf = f"/etc/nginx/conf.d/{bench_name}.conf" 45 | 46 | if os.path.exists(nginx_conf): 47 | production_setup = production_setup or True 48 | 49 | return production_setup 50 | 51 | 52 | def execute(bench_path): 53 | """This patch checks if bench sudoers is set and regenerate supervisor and sudoers files""" 54 | user = get_config(".").get("frappe_user") or getpass.getuser() 55 | 56 | if is_sudoers_set(): 57 | if is_production_set(bench_path): 58 | exec_cmd(f"sudo bench setup supervisor --yes --user {user}") 59 | service("supervisord", "restart") 60 | 61 | exec_cmd(f"sudo bench setup sudoers {user}") 62 | -------------------------------------------------------------------------------- /bench/patches/v5/set_live_reload_config.py: -------------------------------------------------------------------------------- 1 | from bench.config.common_site_config import update_config 2 | 3 | 4 | def execute(bench_path): 5 | update_config({"live_reload": True}, bench_path) 6 | -------------------------------------------------------------------------------- /bench/patches/v5/update_archived_sites.py: -------------------------------------------------------------------------------- 1 | """ 2 | Deprecate archived_sites folder for consistency. This change is 3 | only for Frappe v14 benches. If not a v14 bench yet, skip this 4 | patch and try again later. 5 | 6 | 1. Rename folder `./archived_sites` to `./archived/sites` 7 | 2. Create a symlink `./archived_sites` => `./archived/sites` 8 | 9 | Corresponding changes in frappe/frappe via https://github.com/frappe/frappe/pull/15060 10 | """ 11 | import os 12 | from pathlib import Path 13 | 14 | import click 15 | from bench.utils.app import get_current_version 16 | from semantic_version import Version 17 | 18 | 19 | def execute(bench_path): 20 | frappe_version = Version(get_current_version("frappe")) 21 | 22 | if frappe_version.major < 14 or os.name != "posix": 23 | # Returning False means patch has been skipped 24 | return False 25 | 26 | pre_patch_dir = os.getcwd() 27 | old_directory = Path(bench_path, "archived_sites") 28 | new_directory = Path(bench_path, "archived", "sites") 29 | 30 | if not old_directory.exists(): 31 | return False 32 | 33 | if old_directory.is_symlink(): 34 | return True 35 | 36 | os.chdir(bench_path) 37 | 38 | if not os.path.exists(new_directory): 39 | os.makedirs(new_directory) 40 | 41 | old_directory.rename(new_directory) 42 | 43 | click.secho(f"Archived sites are now stored under {new_directory}") 44 | 45 | if not os.listdir(old_directory): 46 | os.rmdir(old_directory) 47 | 48 | os.symlink(new_directory, old_directory) 49 | 50 | click.secho(f"Symlink {old_directory} that points to {new_directory}") 51 | 52 | os.chdir(pre_patch_dir) 53 | -------------------------------------------------------------------------------- /bench/playbooks/README.md: -------------------------------------------------------------------------------- 1 | # Deploying a, developer/production-ready ERPNext website with Ansible 2 | 3 | ## Supported Platforms 4 | - Debian 8, 9 5 | - Ubuntu 14.04, 16.04 6 | - CentOS 7 7 | 8 | ## Notes for maintainers 9 | - For MariaDB playbooks refer https://github.com/PCextreme/ansible-role-mariadb 10 | - Any changes made in relation to a role should be dont inside the role and not outside it 11 | -------------------------------------------------------------------------------- /bench/playbooks/create_user.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - hosts: localhost 4 | become: yes 5 | become_user: root 6 | tasks: 7 | - name: Create user 8 | user: 9 | name: '{{ frappe_user }}' 10 | generate_ssh_key: yes 11 | 12 | - name: Set home folder perms 13 | file: 14 | path: '{{ user_directory }}' 15 | mode: 'o+rx' 16 | owner: '{{ frappe_user }}' 17 | group: '{{ frappe_user }}' 18 | recurse: yes 19 | 20 | - name: Set /tmp/.bench folder perms 21 | file: 22 | path: '{{ repo_path }}' 23 | owner: '{{ frappe_user }}' 24 | group: '{{ frappe_user }}' 25 | recurse: yes 26 | 27 | - name: Change default shell to bash 28 | shell: "chsh {{ frappe_user }} -s $(which bash)" 29 | ... 30 | -------------------------------------------------------------------------------- /bench/playbooks/macosx.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: localhost 3 | become: yes 4 | become_user: root 5 | 6 | vars: 7 | bench_repo_path: "/Users/{{ ansible_user_id }}/.bench" 8 | bench_path: "/Users/{{ ansible_user_id }}/frappe-bench" 9 | 10 | tasks: 11 | - name: install prequisites 12 | homebrew: 13 | name: 14 | - cmake 15 | - redis 16 | - mariadb 17 | - nodejs 18 | state: present 19 | 20 | - name: install wkhtmltopdf 21 | homebrew_cask: 22 | name: 23 | - wkhtmltopdf 24 | state: present 25 | 26 | - name: configure mariadb 27 | include_tasks: roles/mariadb/tasks/main.yml 28 | vars: 29 | mysql_conf_tpl: roles/mariadb/files/mariadb_config.cnf 30 | 31 | - name: Install MySQLdb in global env 32 | pip: name=mysql-python version=1.2.5 33 | 34 | # setup frappe-bench 35 | - include_tasks: includes/setup_bench.yml 36 | 37 | # setup development environment 38 | - include_tasks: includes/setup_dev_env.yml 39 | when: not production 40 | 41 | ... -------------------------------------------------------------------------------- /bench/playbooks/roles/bash_screen_wall/files/screen_wall.sh: -------------------------------------------------------------------------------- 1 | if [ $TERM != 'screen' ] 2 | then 3 | PS1='HEY! USE SCREEN '$PS1 4 | fi 5 | 6 | sw() { 7 | screen -x $1 || screen -S $1 8 | } 9 | -------------------------------------------------------------------------------- /bench/playbooks/roles/bash_screen_wall/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Setup bash screen wall 3 | copy: src=screen_wall.sh dest=/etc/profile.d/screen_wall.sh 4 | ... -------------------------------------------------------------------------------- /bench/playbooks/roles/bench/tasks/change_ssh_port.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Change ssh port 3 | gather_facts: false 4 | hosts: localhost 5 | user: root 6 | tasks: 7 | - name: change sshd config 8 | lineinfile: > 9 | dest=/etc/ssh/sshd_config 10 | regexp="^Port" 11 | line="Port {{ ssh_port }}" 12 | state=present 13 | 14 | - name: restart ssh 15 | service: name=sshd state=reloaded 16 | 17 | - name: Change ansible ssh port to 2332 18 | set_fact: 19 | ansible_ssh_port: '{{ ssh_port }}' 20 | ... -------------------------------------------------------------------------------- /bench/playbooks/roles/bench/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Check if /tmp/.bench exists 3 | stat: 4 | path: /tmp/.bench 5 | register: tmp_bench 6 | 7 | - name: Check if bench_repo_path exists 8 | stat: 9 | path: '{{ bench_repo_path }}' 10 | register: bench_repo_register 11 | 12 | - name: move /tmp/.bench if it exists 13 | command: 'cp -R /tmp/.bench {{ bench_repo_path }}' 14 | when: tmp_bench.stat.exists and not bench_repo_register.stat.exists 15 | 16 | - name: install bench 17 | pip: 18 | name: '{{ bench_repo_path }}' 19 | extra_args: '-e' 20 | become: yes 21 | become_user: root 22 | 23 | - name: Overwrite bench if required 24 | file: 25 | state: absent 26 | path: "{{ bench_path }}" 27 | when: overwrite 28 | 29 | - name: Check whether bench exists 30 | stat: 31 | path: "{{ bench_path }}" 32 | register: bench_stat 33 | 34 | - name: Fix permissions 35 | become_user: root 36 | command: chown {{ frappe_user }} -R {{ user_directory }} 37 | 38 | - name: python3 bench init for develop 39 | command: bench init {{ bench_path }} --frappe-path {{ frappe_repo_url }} --frappe-branch {{ frappe_branch }} --python {{ python }} 40 | args: 41 | creates: "{{ bench_path }}" 42 | when: not bench_stat.stat.exists and not production 43 | 44 | - name: python3 bench init for production 45 | command: bench init {{ bench_path }} --frappe-path {{ frappe_repo_url }} --frappe-branch {{ frappe_branch }} --python {{ python }} 46 | args: 47 | creates: "{{ bench_path }}" 48 | when: not bench_stat.stat.exists and production 49 | 50 | # setup common_site_config 51 | - name: setup config 52 | command: bench setup config 53 | args: 54 | creates: "{{ bench_path }}/sites/common_site_config.json" 55 | chdir: "{{ bench_path }}" 56 | 57 | - include_tasks: setup_inputrc.yml 58 | 59 | # Setup Procfile 60 | - name: Setup Procfile 61 | command: bench setup procfile 62 | args: 63 | creates: "{{ bench_path }}/Procfile" 64 | chdir: "{{ bench_path }}" 65 | 66 | # Setup Redis env for RQ 67 | - name: Setup Redis 68 | command: bench setup redis 69 | args: 70 | creates: "{{ bench_path }}/config/redis_socketio.conf" 71 | chdir: "{{ bench_path }}" 72 | 73 | # Setup an ERPNext site 74 | - include_tasks: setup_erpnext.yml 75 | when: not run_travis 76 | 77 | # Setup Bench for production environment 78 | - include_tasks: setup_bench_production.yml 79 | vars: 80 | bench_path: "{{ user_directory }}/{{ bench_name }}" 81 | when: not run_travis and production 82 | ... 83 | -------------------------------------------------------------------------------- /bench/playbooks/roles/bench/tasks/setup_bench_production.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Setup production 3 | become: yes 4 | become_user: root 5 | command: bench setup production {{ frappe_user }} --yes 6 | args: 7 | chdir: '{{ bench_path }}' 8 | 9 | - name: Setup Sudoers 10 | become: yes 11 | become_user: root 12 | command: bench setup sudoers {{ frappe_user }} 13 | args: 14 | chdir: '{{ bench_path }}' 15 | 16 | - name: Set correct permissions on bench.log 17 | file: 18 | path: '{{ bench_path }}/logs/bench.log' 19 | owner: '{{ frappe_user }}' 20 | group: '{{ frappe_user }}' 21 | become: yes 22 | become_user: root 23 | 24 | - name: Restart the bench 25 | command: bench restart 26 | args: 27 | chdir: '{{ bench_path }}' 28 | ... -------------------------------------------------------------------------------- /bench/playbooks/roles/bench/tasks/setup_erpnext.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Check if ERPNext App exists 3 | stat: path="{{ bench_path }}/apps/erpnext" 4 | register: app 5 | 6 | - name: Get the ERPNext app 7 | command: bench get-app erpnext {{ erpnext_repo_url }} --branch {{ erpnext_branch }} 8 | args: 9 | creates: "{{ bench_path }}/apps/erpnext" 10 | chdir: "{{ bench_path }}" 11 | when: not app.stat.exists and not without_erpnext 12 | 13 | - name: Check whether the site already exists 14 | stat: path="{{ bench_path }}/sites/{{ site }}" 15 | register: site_folder 16 | when: not without_site 17 | 18 | - name: Create a new site 19 | command: "bench new-site {{ site }} --admin-password '{{ admin_password }}' --mariadb-root-password '{{ mysql_root_password }}'" 20 | args: 21 | chdir: "{{ bench_path }}" 22 | when: not without_site and not site_folder.stat.exists 23 | 24 | - name: Install ERPNext to default site 25 | command: "bench --site {{ site }} install-app erpnext" 26 | args: 27 | chdir: "{{ bench_path }}" 28 | when: not without_site and not without_erpnext 29 | ... -------------------------------------------------------------------------------- /bench/playbooks/roles/bench/tasks/setup_firewall.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Setup Firewall 3 | user: root 4 | hosts: localhost 5 | 6 | tasks: 7 | # For CentOS 8 | - name: Enable SELinux 9 | selinux: policy=targeted state=permissive 10 | when: ansible_distribution == 'CentOS' 11 | 12 | - name: Install firewalld 13 | yum: name=firewalld state=present 14 | when: ansible_distribution == 'CentOS' 15 | 16 | - name: Enable Firewall 17 | service: name=firewalld state=started enabled=yes 18 | when: ansible_distribution == 'CentOS' 19 | 20 | - name: Add firewall rules 21 | firewalld: port={{ item }}/tcp permanent=true state=enabled 22 | with_items: 23 | - 80 24 | - 443 25 | - "{{ ssh_port }}" 26 | when: ansible_distribution == 'CentOS' 27 | 28 | - name: Restart Firewall 29 | service: name=firewalld state=restarted enabled=yes 30 | when: ansible_distribution == 'CentOS' 31 | 32 | # For Ubuntu / Debian 33 | - name: Install ufw 34 | apt: 35 | state: present 36 | force: yes 37 | pkg: 38 | - python-selinux 39 | - ufw 40 | when: ansible_distribution == 'Ubuntu' or ansible_distribution == 'Debian' 41 | 42 | - name: Enable Firewall 43 | ufw: state=enabled policy=deny 44 | when: ansible_distribution == 'Ubuntu' or ansible_distribution == 'Debian' 45 | 46 | - name: Add firewall rules 47 | ufw: rule=allow proto=tcp port={{ item }} 48 | with_items: 49 | - 80 50 | - 443 51 | - "{{ ssh_port }}" 52 | when: ansible_distribution == 'Ubuntu' or ansible_distribution == 'Debian' 53 | ... -------------------------------------------------------------------------------- /bench/playbooks/roles/bench/tasks/setup_inputrc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: insert/update inputrc for history 3 | blockinfile: 4 | dest: "{{ user_directory }}/.inputrc" 5 | create: yes 6 | block: | 7 | ## arrow up 8 | "\e[A":history-search-backward 9 | ## arrow down 10 | "\e[B":history-search-forward 11 | ... 12 | -------------------------------------------------------------------------------- /bench/playbooks/roles/common/tasks/debian.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Setup OpenSSL dependancy 4 | pip: name=pyOpenSSL version=16.2.0 5 | 6 | - name: install pillow prerequisites for Debian < 8 7 | apt: 8 | pkg: 9 | - libjpeg8-dev 10 | - libtiff4-dev 11 | - tcl8.5-dev 12 | - tk8.5-dev 13 | state: present 14 | when: ansible_distribution_version is version_compare('8', 'lt') 15 | 16 | - name: install pillow prerequisites for Debian 8 17 | apt: 18 | pkg: 19 | - libjpeg62-turbo-dev 20 | - libtiff5-dev 21 | - tcl8.5-dev 22 | - tk8.5-dev 23 | state: present 24 | when: ansible_distribution_version is version_compare('8', 'eq') 25 | 26 | - name: install pillow prerequisites for Debian 9 27 | apt: 28 | pkg: 29 | - libjpeg62-turbo-dev 30 | - libtiff5-dev 31 | - tcl8.5-dev 32 | - tk8.5-dev 33 | state: present 34 | when: ansible_distribution_version is version_compare('9', 'eq') 35 | 36 | 37 | - name: install pillow prerequisites for Debian >= 10 38 | apt: 39 | pkg: 40 | - libjpeg62-turbo-dev 41 | - libtiff5-dev 42 | - tcl8.6-dev 43 | - tk8.6-dev 44 | state: present 45 | when: ansible_distribution_version is version_compare('10', 'ge') 46 | 47 | - name: install pdf prerequisites debian 48 | apt: 49 | pkg: 50 | - libssl-dev 51 | state: present 52 | force: yes 53 | 54 | ... 55 | -------------------------------------------------------------------------------- /bench/playbooks/roles/common/tasks/debian_family.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Install prerequisites using apt-get 4 | become: yes 5 | become_user: root 6 | apt: 7 | pkg: 8 | - dnsmasq 9 | - fontconfig 10 | - git # Version control 11 | - htop # Server stats 12 | - libcrypto++-dev 13 | - libfreetype6-dev 14 | - liblcms2-dev 15 | - libwebp-dev 16 | - libxext6 17 | - libxrender1 18 | - libxslt1-dev 19 | - libxslt1.1 20 | - libffi-dev 21 | - ntp # Clock synchronization 22 | - postfix # Mail Server 23 | - python3-dev # Installing python developer suite 24 | - python-tk 25 | - screen # To aid ssh sessions with connectivity problems 26 | - vim # Is that supposed to be a question!? 27 | - xfonts-75dpi 28 | - xfonts-base 29 | - zlib1g-dev 30 | - apt-transport-https 31 | - libsasl2-dev 32 | - libldap2-dev 33 | - libcups2-dev 34 | - pv # Show progress during database restore 35 | state: present 36 | force: yes 37 | 38 | - include_tasks: debian.yml 39 | when: ansible_distribution == 'Debian' 40 | 41 | - include_tasks: ubuntu.yml 42 | when: ansible_distribution == 'Ubuntu' 43 | 44 | ... 45 | -------------------------------------------------------------------------------- /bench/playbooks/roles/common/tasks/macos.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - hosts: localhost 4 | become: yes 5 | become_user: root 6 | vars: 7 | bench_repo_path: "/Users/{{ ansible_user_id }}/.bench" 8 | bench_path: "/Users/{{ ansible_user_id }}/frappe-bench" 9 | tasks: 10 | # install pre-requisites 11 | - name: install prequisites 12 | homebrew: 13 | name: 14 | - cmake 15 | - redis 16 | - mariadb 17 | - nodejs 18 | state: present 19 | 20 | # install wkhtmltopdf 21 | - name: cask installs 22 | homebrew_cask: 23 | name: 24 | - wkhtmltopdf 25 | state: present 26 | 27 | - name: configure mariadb 28 | include_tasks: roles/mariadb/tasks/main.yml 29 | vars: 30 | mysql_conf_tpl: roles/mariadb/files/mariadb_config.cnf 31 | 32 | # setup frappe-bench 33 | - include_tasks: includes/setup_bench.yml 34 | 35 | # setup development environment 36 | - include_tasks: includes/setup_dev_env.yml 37 | when: not production 38 | 39 | ... -------------------------------------------------------------------------------- /bench/playbooks/roles/common/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Install's prerequisites, like fonts, image libraries, vim, screen, python3-dev 3 | 4 | - include_tasks: debian_family.yml 5 | when: ansible_os_family == 'Debian' 6 | 7 | - include_tasks: redhat_family.yml 8 | when: ansible_os_family == "RedHat" 9 | ... -------------------------------------------------------------------------------- /bench/playbooks/roles/common/tasks/redhat_family.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Install IUS repo for python 3.6 4 | become: yes 5 | become_user: root 6 | yum: 7 | name: https://repo.ius.io/ius-release-el7.rpm 8 | state: present 9 | 10 | - name: "Setup prerequisites using yum" 11 | become: yes 12 | become_user: root 13 | yum: 14 | name: 15 | - bzip2-devel 16 | - cronie 17 | - dnsmasq 18 | - freetype-devel 19 | - git 20 | - htop 21 | - lcms2-devel 22 | - libjpeg-devel 23 | - libtiff-devel 24 | - libffi-devel 25 | - libwebp-devel 26 | - libXext 27 | - libXrender 28 | - libzip-devel 29 | - libffi-devel 30 | - ntp 31 | - openssl-devel 32 | - postfix 33 | - python36u 34 | - python-devel 35 | - python-setuptools 36 | - python-pip 37 | - redis 38 | - screen 39 | - sudo 40 | - tcl-devel 41 | - tk-devel 42 | - vim 43 | - which 44 | - xorg-x11-fonts-75dpi 45 | - xorg-x11-fonts-Type1 46 | - zlib-devel 47 | - openssl-devel 48 | - openldap-devel 49 | - libselinux-python 50 | - cups-libs 51 | state: present 52 | ... 53 | -------------------------------------------------------------------------------- /bench/playbooks/roles/common/tasks/ubuntu.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: install pillow prerequisites for Ubuntu < 14.04 4 | apt: 5 | pkg: 6 | - libjpeg8-dev 7 | - libtiff4-dev 8 | - tcl8.5-dev 9 | - tk8.5-dev 10 | state: present 11 | force: yes 12 | when: ansible_distribution_version is version_compare('14.04', 'lt') 13 | 14 | - name: install pillow prerequisites for Ubuntu >= 14.04 15 | apt: 16 | pkg: 17 | - libjpeg8-dev 18 | - libtiff5-dev 19 | - tcl8.6-dev 20 | - tk8.6-dev 21 | state: present 22 | force: yes 23 | when: ansible_distribution_version is version_compare('14.04', 'ge') 24 | 25 | - name: install pdf prerequisites for Ubuntu < 18.04 26 | apt: 27 | pkg: 28 | - libssl-dev 29 | state: present 30 | force: yes 31 | when: ansible_distribution_version is version_compare('18.04', 'lt') 32 | 33 | - name: install pdf prerequisites for Ubuntu >= 18.04 34 | apt: 35 | pkg: 36 | - libssl1.1 37 | state: present 38 | force: yes 39 | when: ansible_distribution_version is version_compare('18.04', 'ge') 40 | 41 | ... 42 | -------------------------------------------------------------------------------- /bench/playbooks/roles/dns_caching/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: restart network manager 3 | service: name=NetworkManager state=restarted 4 | ... -------------------------------------------------------------------------------- /bench/playbooks/roles/dns_caching/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Check NetworkManager.conf exists 3 | stat: 4 | path: /etc/NetworkManager/NetworkManager.conf 5 | register: result 6 | 7 | - name: Unmask NetworkManager service 8 | command: systemctl unmask NetworkManager 9 | when: result.stat.exists 10 | 11 | - name: Add dnsmasq to network config 12 | lineinfile: > 13 | dest=/etc/NetworkManager/NetworkManager.conf 14 | regexp="dns=" 15 | line="dns=dnsmasq" 16 | state=present 17 | when: result.stat.exists 18 | notify: 19 | - restart network manager 20 | ... 21 | -------------------------------------------------------------------------------- /bench/playbooks/roles/fail2ban/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | fail2ban_nginx_access_log: /var/log/nginx/*access.log 3 | maxretry: 6 4 | bantime: 600 5 | findtime: 600 6 | -------------------------------------------------------------------------------- /bench/playbooks/roles/fail2ban/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: restart fail2ban 3 | service: name=fail2ban state=restarted -------------------------------------------------------------------------------- /bench/playbooks/roles/fail2ban/tasks/configure_nginx_jail.yml: -------------------------------------------------------------------------------- 1 | - name: Configure fail2ban jail options 2 | hosts: localhost 3 | become: yes 4 | become_user: root 5 | vars_files: 6 | - ../defaults/main.yml 7 | tasks: 8 | 9 | - name: Setup filter 10 | template: src="../templates/nginx-proxy-filter.conf.j2" dest="/etc/fail2ban/filter.d/nginx-proxy.conf" 11 | - name: Setup jail 12 | template: src="../templates/nginx-proxy-jail.conf.j2" dest="/etc/fail2ban/jail.d/nginx-proxy.conf" 13 | - name: restart service 14 | service: name=fail2ban state=restarted 15 | -------------------------------------------------------------------------------- /bench/playbooks/roles/fail2ban/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install fail2ban 3 | yum: name=fail2ban state=present 4 | when: ansible_distribution == 'CentOS' 5 | 6 | - name: Install fail2ban 7 | apt: name=fail2ban state=present 8 | when: ansible_distribution == 'Debian' or ansible_distribution == 'Ubuntu' 9 | 10 | - name: Enable fail2ban 11 | service: name=fail2ban enabled=yes 12 | 13 | - name: Create jail.d 14 | file: path=/etc/fail2ban/jail.d state=directory 15 | 16 | - name: Setup filters 17 | template: src="{{item}}-filter.conf.j2" dest="/etc/fail2ban/filter.d/{{item}}.conf" 18 | with_items: 19 | - nginx-proxy 20 | notify: 21 | - restart fail2ban 22 | 23 | - name: setup jails 24 | template: src="{{item}}-jail.conf.j2" dest="/etc/fail2ban/jail.d/{{item}}.conf" 25 | with_items: 26 | - nginx-proxy 27 | notify: 28 | - restart fail2ban 29 | -------------------------------------------------------------------------------- /bench/playbooks/roles/fail2ban/templates/nginx-proxy-filter.conf.j2: -------------------------------------------------------------------------------- 1 | # Block IPs trying to use server as proxy. 2 | [Definition] 3 | failregex = .*\" 400 4 | .*"[A-Z]* /(cms|muieblackcat|db|cpcommerce|cgi-bin|wp-login|joomla|awstatstotals|wp-content|wp-includes|pma|phpmyadmin|myadmin|mysql|mysqladmin|sqladmin|mypma|admin|xampp|mysqldb|pmadb|phpmyadmin1|phpmyadmin2).*" 4[\d][\d] 5 | .*".*supports_implicit_sdk_logging.*" 4[\d][\d] 6 | .*".*activities?advertiser_tracking_enabled.*" 4[\d][\d] 7 | .*".*/picture?type=normal.*" 4[\d][\d] 8 | .*".*/announce.php?info_hash=.*" 4[\d][\d] 9 | 10 | ignoreregex = -------------------------------------------------------------------------------- /bench/playbooks/roles/fail2ban/templates/nginx-proxy-jail.conf.j2: -------------------------------------------------------------------------------- 1 | ## block hosts trying to abuse our server as a forward proxy 2 | [nginx-proxy] 3 | enabled = true 4 | filter = nginx-proxy 5 | logpath = {{ fail2ban_nginx_access_log }} 6 | action = iptables-multiport[name=NoNginxProxy, port="http,https"] 7 | maxretry = {{ maxretry }} 8 | bantime = {{ bantime }} 9 | findtime = {{ findtime }} -------------------------------------------------------------------------------- /bench/playbooks/roles/frappe_selinux/files/frappe_selinux.te: -------------------------------------------------------------------------------- 1 | module frappe_selinux 1.0; 2 | 3 | require { 4 | type user_home_dir_t; 5 | type httpd_t; 6 | type user_home_t; 7 | type soundd_port_t; 8 | class tcp_socket name_connect; 9 | class lnk_file read; 10 | class dir { getattr search }; 11 | class file { read open }; 12 | } 13 | 14 | #============= httpd_t ============== 15 | 16 | #!!!! This avc is allowed in the current policy 17 | allow httpd_t soundd_port_t:tcp_socket name_connect; 18 | 19 | #!!!! This avc is allowed in the current policy 20 | allow httpd_t user_home_dir_t:dir search; 21 | 22 | #!!!! This avc is allowed in the current policy 23 | allow httpd_t user_home_t:dir { getattr search }; 24 | 25 | #!!!! This avc can be allowed using the boolean 'httpd_read_user_content' 26 | allow httpd_t user_home_t:file open; 27 | 28 | #!!!! This avc is allowed in the current policy 29 | allow httpd_t user_home_t:file read; 30 | 31 | #!!!! This avc is allowed in the current policy 32 | allow httpd_t user_home_t:lnk_file read; -------------------------------------------------------------------------------- /bench/playbooks/roles/frappe_selinux/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install deps 3 | yum: 4 | name: 5 | - policycoreutils-python 6 | - selinux-policy-devel 7 | state: present 8 | when: ansible_distribution == 'CentOS' 9 | 10 | - name: Check enabled SELinux modules 11 | shell: semanage module -l 12 | register: enabled_modules 13 | when: ansible_distribution == 'CentOS' 14 | 15 | - name: Copy frappe_selinux policy 16 | copy: src=frappe_selinux.te dest=/root/frappe_selinux.te 17 | register: dest_frappe_selinux_te 18 | when: ansible_distribution == 'CentOS' 19 | 20 | - name: Compile frappe_selinux policy 21 | shell: "make -f /usr/share/selinux/devel/Makefile frappe_selinux.pp && semodule -i frappe_selinux.pp" 22 | args: 23 | chdir: /root/ 24 | when: "ansible_distribution == 'CentOS' and enabled_modules.stdout.find('frappe_selinux') == -1 or dest_frappe_selinux_te.changed" 25 | ... -------------------------------------------------------------------------------- /bench/playbooks/roles/locale/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | locale_keymap: us 3 | locale_lang: en_US.utf8 4 | ... -------------------------------------------------------------------------------- /bench/playbooks/roles/locale/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Check current locale 3 | shell: localectl 4 | register: locale_test 5 | when: ansible_distribution == 'Centos' or ansible_distribution == 'Ubuntu' 6 | 7 | - name: Set Locale 8 | command: "localectl set-locale LANG={{ locale_lang }}" 9 | when: (ansible_distribution == 'Centos' or ansible_distribution == 'Ubuntu') and locale_test.stdout.find('LANG=locale_lang') == -1 10 | 11 | - name: Set keymap 12 | command: "localectl set-keymap {{ locale_keymap }}" 13 | when: "(ansible_distribution == 'Centos' or ansible_distribution == 'Ubuntu') and locale_test.stdout.find('Keymap:locale_keymap') == -1" 14 | 15 | - name: Set Locale as en_US 16 | lineinfile: dest=/etc/environment backup=yes line="{{ item }}" 17 | with_items: 18 | - "LC_ALL=en_US.UTF-8" 19 | - "LC_CTYPE=en_US.UTF-8" 20 | - "LANG=en_US.UTF-8" 21 | ... -------------------------------------------------------------------------------- /bench/playbooks/roles/logwatch/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | logwatch_emails: "{{ admin_emails }}" 3 | logwatch_detail: High 4 | ... -------------------------------------------------------------------------------- /bench/playbooks/roles/logwatch/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install logwatch 3 | yum: name=logwatch state=present 4 | when: ansible_distribution == 'CentOS' 5 | 6 | - name: Install logwatch on Ubuntu or Debian 7 | apt: name=logwatch state=present 8 | when: ansible_distribution == 'Debian' or ansible_distribution == 'Ubuntu' 9 | 10 | - name: Copy logwatch config 11 | template: src=logwatch.conf.j2 dest=/etc/logwatch/conf/logwatch.conf backup=yes 12 | when: admin_emails is defined 13 | ... -------------------------------------------------------------------------------- /bench/playbooks/roles/logwatch/templates/logwatch.conf.j2: -------------------------------------------------------------------------------- 1 | MailTo = {{ logwatch_emails }} 2 | Detail = {{ logwatch_detail }} -------------------------------------------------------------------------------- /bench/playbooks/roles/mariadb/README.md: -------------------------------------------------------------------------------- 1 | # Ansible Role: MariaDB 2 | 3 | Installs MariaDB 4 | 5 | ## Supported platforms 6 | 7 | ``` 8 | CentOS 6 & 7 9 | Ubuntu 14.04 10 | Ubuntu 16.04 11 | Debain 9 12 | ``` 13 | 14 | ## Post install 15 | 16 | Run `mariadb-secure-installation` 17 | 18 | ## Requirements 19 | 20 | None 21 | 22 | ## Role Variables 23 | 24 | MariaDB version: 25 | 26 | ``` 27 | mariadb_version: 10.2 28 | ``` 29 | 30 | Configuration template: 31 | 32 | ``` 33 | mysql_conf_tpl: change_me 34 | ``` 35 | 36 | Configuration filename: 37 | 38 | ``` 39 | mysql_conf_file: settings.cnf 40 | ``` 41 | 42 | ### Experimental unattended mariadb-secure-installation 43 | 44 | ``` 45 | ansible-playbook release.yml --extra-vars "mysql_secure_installation=true mysql_root_password=your_very_secret_password" 46 | ``` 47 | 48 | ## Dependencies 49 | 50 | None 51 | 52 | ## Example Playbook 53 | 54 | ``` 55 | - hosts: servers 56 | roles: 57 | - { role: mariadb } 58 | ``` 59 | 60 | ## Credits 61 | 62 | - [Attila van der Velde](https://github.com/vdvm) 63 | 64 | -------------------------------------------------------------------------------- /bench/playbooks/roles/mariadb/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | mysql_conf_tpl: change_me 3 | mysql_conf_file: settings.cnf 4 | 5 | mysql_secure_installation: false 6 | -------------------------------------------------------------------------------- /bench/playbooks/roles/mariadb/files/debian_mariadb_config.cnf: -------------------------------------------------------------------------------- 1 | [mysqld] 2 | innodb-file-format=barracuda 3 | innodb-file-per-table=1 4 | innodb-large-prefix=1 5 | character-set-client-handshake = FALSE 6 | character-set-server = utf8mb4 7 | collation-server = utf8mb4_unicode_ci 8 | max_allowed_packet=256M 9 | 10 | [mysql] 11 | default-character-set = utf8mb4 12 | 13 | [mysqldump] 14 | max_allowed_packet=256M 15 | -------------------------------------------------------------------------------- /bench/playbooks/roles/mariadb/files/mariadb_config.cnf: -------------------------------------------------------------------------------- 1 | [mysqld] 2 | 3 | # GENERAL # 4 | user = mysql 5 | default-storage-engine = InnoDB 6 | socket = /var/lib/mysql/mysql.sock 7 | pid-file = /var/lib/mysql/mysql.pid 8 | 9 | # MyISAM # 10 | key-buffer-size = 32M 11 | myisam-recover = FORCE,BACKUP 12 | 13 | # SAFETY # 14 | max-allowed-packet = 256M 15 | max-connect-errors = 1000000 16 | innodb = FORCE 17 | 18 | # DATA STORAGE # 19 | datadir = /var/lib/mysql/ 20 | 21 | # BINARY LOGGING # 22 | log-bin = /var/lib/mysql/mysql-bin 23 | expire-logs-days = 14 24 | sync-binlog = 1 25 | 26 | # REPLICATION # 27 | server-id = 1 28 | 29 | # CACHES AND LIMITS # 30 | tmp-table-size = 32M 31 | max-heap-table-size = 32M 32 | query-cache-type = 0 33 | query-cache-size = 0 34 | max-connections = 500 35 | thread-cache-size = 50 36 | open-files-limit = 65535 37 | table-definition-cache = 4096 38 | table-open-cache = 10240 39 | 40 | # INNODB # 41 | innodb-flush-method = O_DIRECT 42 | innodb-log-files-in-group = 2 43 | innodb-log-file-size = 512M 44 | innodb-flush-log-at-trx-commit = 1 45 | innodb-file-per-table = 1 46 | innodb-buffer-pool-size = {{ (ansible_memtotal_mb*0.685)|round|int }}M 47 | innodb-file-format = barracuda 48 | innodb-large-prefix = 1 49 | collation-server = utf8mb4_unicode_ci 50 | character-set-server = utf8mb4 51 | character-set-client-handshake = FALSE 52 | max_allowed_packet = 256M 53 | 54 | # LOGGING # 55 | log-error = /var/lib/mysql/mysql-error.log 56 | log-queries-not-using-indexes = 0 57 | slow-query-log = 1 58 | slow-query-log-file = /var/lib/mysql/mysql-slow.log 59 | 60 | [mysql] 61 | default-character-set = utf8mb4 62 | 63 | [mysqldump] 64 | max_allowed_packet=256M -------------------------------------------------------------------------------- /bench/playbooks/roles/mariadb/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: restart mariadb 3 | service: name=mariadb state=restarted 4 | -------------------------------------------------------------------------------- /bench/playbooks/roles/mariadb/tasks/centos.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Add repo file 3 | template: src=mariadb_centos.repo.j2 dest=/etc/yum.repos.d/mariadb.repo owner=root group=root mode=0644 4 | 5 | - name: Install MariaDB 6 | yum: 7 | name: 8 | - MariaDB-server 9 | - MariaDB-client 10 | enablerepo: mariadb 11 | state: present 12 | 13 | - name: Install MySQLdb Python package for secure installations. 14 | yum: 15 | name: 16 | - MySQL-python 17 | state: present 18 | when: mysql_secure_installation and mysql_root_password is defined 19 | -------------------------------------------------------------------------------- /bench/playbooks/roles/mariadb/tasks/debian.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Add apt key for mariadb for Debian <= 8 3 | apt_key: keyserver=hkp://keyserver.ubuntu.com:80 id=0xcbcb082a1bb943db state=present 4 | when: ansible_distribution_major_version is version_compare('8', 'le') 5 | 6 | - name: Install dirmngr for apt key for mariadb for Debian > 8 7 | apt: 8 | pkg: dirmngr 9 | state: present 10 | when: ansible_distribution_major_version is version_compare('8', 'gt') 11 | 12 | - name: Add apt key for mariadb for Debian > 8 13 | apt_key: keyserver=hkp://keyserver.ubuntu.com:80 id=0xF1656F24C74CD1D8 state=present 14 | when: ansible_distribution_major_version is version_compare('8', 'gt') 15 | 16 | - name: Add apt repository 17 | apt_repository: 18 | repo: 'deb [arch=amd64,i386] http://ams2.mirrors.digitalocean.com/mariadb/repo/{{ mariadb_version }}/debian {{ ansible_distribution_release }} main' 19 | state: present 20 | 21 | - name: Add apt repository 22 | apt_repository: 23 | repo: 'deb-src [arch=amd64,i386] http://ams2.mirrors.digitalocean.com/mariadb/repo/{{ mariadb_version }}/debian {{ ansible_distribution_release }} main' 24 | state: present 25 | 26 | - name: Unattended package installation 27 | shell: export DEBIAN_FRONTEND=noninteractive 28 | 29 | - name: apt-get install 30 | apt: 31 | pkg: 32 | - mariadb-server 33 | - mariadb-client 34 | - mariadb-common 35 | - libmariadbclient18 36 | - python3-mysqldb 37 | update_cache: yes 38 | state: present 39 | ... -------------------------------------------------------------------------------- /bench/playbooks/roles/mariadb/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - include_tasks: centos.yml 3 | when: ansible_distribution == 'CentOS' and ansible_distribution_major_version|int >= 6 4 | 5 | - include_tasks: ubuntu-trusty.yml 6 | when: ansible_distribution == 'Ubuntu' and ansible_distribution_version == '14.04' 7 | 8 | - include_tasks: ubuntu-xenial_bionic.yml 9 | when: ansible_distribution == 'Ubuntu' and ansible_distribution_major_version|int >= 16 10 | 11 | - name: Add configuration 12 | template: 13 | src: '{{ mysql_conf_tpl }}' 14 | dest: '{{ mysql_conf_dir[ansible_distribution] }}/{{ mysql_conf_file }}' 15 | owner: root 16 | group: root 17 | mode: 0644 18 | when: mysql_conf_tpl != 'change_me' and ansible_distribution != 'Debian' 19 | notify: restart mariadb 20 | 21 | - include_tasks: debian.yml 22 | when: ansible_distribution == 'Debian' 23 | 24 | - name: Add configuration 25 | template: 26 | src: '{{ mysql_conf_tpl }}' 27 | dest: '{{ mysql_conf_dir[ansible_distribution] }}/{{ mysql_conf_file }}' 28 | owner: root 29 | group: root 30 | mode: 0644 31 | when: mysql_conf_tpl != 'change_me' and ansible_distribution == 'Debian' 32 | notify: restart mariadb 33 | 34 | - name: Add additional conf for MariaDB 10.2 in mariadb.conf.d 35 | blockinfile: 36 | path: /etc/mysql/conf.d/settings.cnf 37 | block: | 38 | # Import all .cnf files from configuration directory 39 | !includedir /etc/mysql/mariadb.conf.d/ 40 | become: yes 41 | become_user: root 42 | when: ansible_distribution == 'Ubuntu' or ansible_distribution == 'Debian' 43 | 44 | - name: Add additional conf for MariaDB 10.2 in mariadb.conf.d 45 | blockinfile: 46 | path: /etc/mysql/mariadb.conf.d/erpnext.cnf 47 | block: | 48 | [mysqld] 49 | pid-file = /var/run/mysqld/mysqld.pid 50 | socket = /var/run/mysqld/mysqld.sock 51 | 52 | # setting appeared inside mysql but overwritten by mariadb inside mariadb.conf.d/xx-server.cnf valued as utf8mb4_general_ci 53 | 54 | collation-server = utf8mb4_unicode_ci 55 | create: yes 56 | become: yes 57 | become_user: root 58 | when: ansible_distribution == 'Ubuntu' or ansible_distribution == 'Debian' 59 | 60 | - name: Start and enable service 61 | service: 62 | name: mariadb 63 | state: started 64 | enabled: yes 65 | 66 | - debug: 67 | msg: "{{ mysql_root_password }}" 68 | 69 | - include_tasks: mysql_secure_installation.yml 70 | when: mysql_root_password is defined 71 | 72 | - debug: 73 | var: mysql_secure_installation 74 | when: mysql_secure_installation and mysql_root_password is defined 75 | 76 | ... 77 | -------------------------------------------------------------------------------- /bench/playbooks/roles/mariadb/tasks/mysql_secure_installation.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - debug: 4 | msg: "{{ mysql_root_password }}" 5 | 6 | # create root .my.cnf config file 7 | - name: Add .my.cnf 8 | template: src=my.cnf.j2 dest=/root/.my.cnf owner=root group=root mode=0600 9 | 10 | # Set root password 11 | # UPDATE mysql.user SET Password=PASSWORD('mysecret') WHERE User='root'; 12 | # FLUSH PRIVILEGES; 13 | 14 | - name: Set root Password 15 | mysql_user: login_password={{ mysql_root_password }} check_implicit_admin=yes name=root host={{ item }} password={{ mysql_root_password }} state=present 16 | with_items: 17 | - localhost 18 | - 127.0.0.1 19 | - ::1 20 | 21 | - name: Reload privilege tables 22 | command: 'mariadb -ne "{{ item }}"' 23 | with_items: 24 | - FLUSH PRIVILEGES 25 | changed_when: False 26 | when: run_travis is not defined 27 | 28 | - name: Remove anonymous users 29 | command: 'mariadb -ne "{{ item }}"' 30 | with_items: 31 | - DELETE FROM mysql.user WHERE User='' 32 | changed_when: False 33 | when: run_travis is not defined 34 | 35 | - name: Disallow root login remotely 36 | command: 'mariadb -ne "{{ item }}"' 37 | with_items: 38 | - DELETE FROM mysql.user WHERE User='root' AND Host NOT IN ('localhost', '127.0.0.1', '::1') 39 | changed_when: False 40 | when: run_travis is not defined 41 | 42 | - name: Remove test database and access to it 43 | command: 'mariadb -ne "{{ item }}"' 44 | with_items: 45 | - DROP DATABASE IF EXISTS test 46 | - DELETE FROM mysql.db WHERE Db='test' OR Db='test\\_%' 47 | changed_when: False 48 | when: run_travis is not defined 49 | 50 | - name: Reload privilege tables 51 | command: 'mariadb -ne "{{ item }}"' 52 | with_items: 53 | - FLUSH PRIVILEGES 54 | changed_when: False 55 | when: run_travis is not defined 56 | -------------------------------------------------------------------------------- /bench/playbooks/roles/mariadb/tasks/ubuntu-trusty.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Add repo file 3 | template: src=mariadb_ubuntu.list.j2 dest=/etc/apt/sources.list.d/mariadb.list owner=root group=root mode=0644 4 | register: mariadb_list 5 | 6 | - name: Add repo key 7 | apt_key: id=1BB943DB url=http://keyserver.ubuntu.com/pks/lookup?op=get&search=0xCBCB082A1BB943DB state=present 8 | register: mariadb_key 9 | 10 | - name: Update apt cache 11 | apt: update_cache=yes 12 | when: mariadb_list.changed == True or mariadb_key.changed == True 13 | 14 | - name: Unattended package installation 15 | shell: export DEBIAN_FRONTEND=noninteractive 16 | changed_when: false 17 | 18 | - name: Install MariaDB 19 | apt: 20 | pkg: 21 | - mariadb-server 22 | - mariadb-client 23 | - libmariadbclient18 24 | state: present 25 | 26 | - name: Install MySQLdb Python package for secure installations. 27 | apt: 28 | pkg: 29 | - python3-mysqldb 30 | state: present 31 | when: mysql_secure_installation and mysql_root_password is defined 32 | -------------------------------------------------------------------------------- /bench/playbooks/roles/mariadb/tasks/ubuntu-xenial_bionic.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Add repo file 3 | template: src=mariadb_ubuntu.list.j2 dest=/etc/apt/sources.list.d/mariadb.list owner=root group=root mode=0644 4 | register: mariadb_list 5 | 6 | - name: Add repo key 7 | apt_key: id=C74CD1D8 url=http://keyserver.ubuntu.com/pks/lookup?op=get&search=0xF1656F24C74CD1D8 state=present 8 | register: mariadb_key 9 | 10 | - name: Update apt cache 11 | apt: update_cache=yes 12 | when: mariadb_list.changed == True or mariadb_key.changed == True 13 | 14 | - name: Unattended package installation 15 | shell: export DEBIAN_FRONTEND=noninteractive 16 | changed_when: false 17 | 18 | - name: Install MariaDB 19 | apt: 20 | pkg: 21 | - mariadb-server 22 | - mariadb-client 23 | - libmariadbclient18 24 | state: present 25 | 26 | - name: Install MySQLdb Python package for secure installations. 27 | apt: 28 | pkg: 29 | - python3-mysqldb 30 | state: present 31 | when: mysql_secure_installation and mysql_root_password is defined 32 | -------------------------------------------------------------------------------- /bench/playbooks/roles/mariadb/templates/mariadb_centos.repo.j2: -------------------------------------------------------------------------------- 1 | # MariaDB CentOS {{ ansible_distribution_major_version|int }} repository list 2 | # http://mariadb.org/mariadb/repositories/ 3 | [mariadb] 4 | name = MariaDB 5 | baseurl = http://yum.mariadb.org/{{ mariadb_version }}/centos{{ ansible_distribution_major_version|int }}-amd64 6 | gpgkey=https://yum.mariadb.org/RPM-GPG-KEY-MariaDB 7 | gpgcheck=1 8 | -------------------------------------------------------------------------------- /bench/playbooks/roles/mariadb/templates/mariadb_debian.list.j2: -------------------------------------------------------------------------------- 1 | # MariaDB {{ mariadb_version }} Debian {{ ansible_distribution_release | title }} repository list 2 | # http://mariadb.org/mariadb/repositories/ 3 | deb http://ams2.mirrors.digitalocean.com/mariadb/repo/{{ mariadb_version }}/debian {{ ansible_distribution_release | lower }} main 4 | deb-src http://ams2.mirrors.digitalocean.com/mariadb/repo/{{ mariadb_version }}/debian {{ ansible_distribution_release | lower }} main 5 | -------------------------------------------------------------------------------- /bench/playbooks/roles/mariadb/templates/mariadb_ubuntu.list.j2: -------------------------------------------------------------------------------- 1 | # MariaDB Ubuntu {{ ansible_distribution_release | title }} repository list 2 | # http://mariadb.org/mariadb/repositories/ 3 | deb http://ams2.mirrors.digitalocean.com/mariadb/repo/{{ mariadb_version }}/ubuntu {{ ansible_distribution_release | lower }} main 4 | deb-src http://ams2.mirrors.digitalocean.com/mariadb/repo/{{ mariadb_version }}/ubuntu {{ ansible_distribution_release | lower }} main 5 | -------------------------------------------------------------------------------- /bench/playbooks/roles/mariadb/templates/my.cnf.j2: -------------------------------------------------------------------------------- 1 | [client] 2 | user=root 3 | password={{ mysql_root_password }} 4 | -------------------------------------------------------------------------------- /bench/playbooks/roles/mariadb/vars/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | mysql_conf_dir: 3 | "CentOS": /etc/my.cnf.d 4 | "Ubuntu": /etc/mysql/conf.d 5 | "Debian": /etc/mysql/conf.d 6 | mysql_conf_tpl: files/mariadb_config.cnf 7 | mysql_secure_installation: True 8 | ... 9 | -------------------------------------------------------------------------------- /bench/playbooks/roles/nginx/README.md: -------------------------------------------------------------------------------- 1 | # Ansible Role: Nginx 2 | 3 | [![Build Status](https://travis-ci.org/geerlingguy/ansible-role-nginx.svg?branch=master)](https://travis-ci.org/geerlingguy/ansible-role-nginx) 4 | 5 | Installs Nginx on RedHat/CentOS or Debian/Ubuntu linux servers. 6 | 7 | This role installs and configures the latest version of Nginx from the Nginx yum repository (on RedHat-based systems) or via apt (on Debian-based systems). You will likely need to do extra setup work after this role has installed Nginx, like adding your own [virtualhost].conf file inside `/etc/nginx/conf.d/`, describing the location and options to use for your particular website. 8 | 9 | ## Requirements 10 | 11 | None. 12 | 13 | ## Role Variables 14 | 15 | Available variables are listed below, along with default values (see `defaults/main.yml`): 16 | 17 | nginx_vhosts: [] 18 | 19 | A list of vhost definitions (server blocks) for Nginx virtual hosts. If left empty, you will need to supply your own virtual host configuration. See the commented example in `defaults/main.yml` for available server options. If you have a large number of customizations required for your server definition(s), you're likely better off managing the vhost configuration file yourself, leaving this variable set to `[]`. 20 | 21 | nginx_remove_default_vhost: false 22 | 23 | Whether to remove the 'default' virtualhost configuration supplied by Nginx. Useful if you want the base `/` URL to be directed at one of your own virtual hosts configured in a separate .conf file. 24 | 25 | nginx_upstreams: [] 26 | 27 | If you are configuring Nginx as a load balancer, you can define one or more upstream sets using this variable. In addition to defining at least one upstream, you would need to configure one of your server blocks to proxy requests through the defined upstream (e.g. `proxy_pass http://myapp1;`). See the commented example in `defaults/main.yml` for more information. 28 | 29 | nginx_user: "nginx" 30 | 31 | The user under which Nginx will run. Defaults to `nginx` for RedHat, and `www-data` for Debian. 32 | 33 | nginx_worker_processes: "1" 34 | nginx_worker_connections: "1024" 35 | 36 | `nginx_worker_processes` should be set to the number of cores present on your machine. Connections (find this number with `grep processor /proc/cpuinfo | wc -l`). `nginx_worker_connections` is the number of connections per process. Set this higher to handle more simultaneous connections (and remember that a connection will be used for as long as the keepalive timeout duration for every client!). 37 | 38 | nginx_error_log: "/var/log/nginx/error.log warn" 39 | nginx_access_log: "/var/log/nginx/access.log main buffer=16k" 40 | 41 | Configuration of the default error and access logs. Set to `off` to disable a log entirely. 42 | 43 | nginx_sendfile: "on" 44 | nginx_tcp_nopush: "on" 45 | nginx_tcp_nodelay: "on" 46 | 47 | TCP connection options. See [this blog post](https://t37.net/nginx-optimization-understanding-sendfile-tcp_nodelay-and-tcp_nopush.html) for more information on these directives. 48 | 49 | nginx_keepalive_timeout: "65" 50 | nginx_keepalive_requests: "100" 51 | 52 | Nginx keepalive settings. Timeout should be set higher (10s+) if you have more polling-style traffic (AJAX-powered sites especially), or lower (<10s) if you have a site where most users visit a few pages and don't send any further requests. 53 | 54 | nginx_client_max_body_size: "64m" 55 | 56 | This value determines the largest file upload possible, as uploads are passed through Nginx before hitting a backend like `php-fpm`. If you get an error like `client intended to send too large body`, it means this value is set too low. 57 | 58 | nginx_proxy_cache_path: "" 59 | 60 | Set as the `proxy_cache_path` directive in the `nginx.conf` file. By default, this will not be configured (if left as an empty string), but if you wish to use Nginx as a reverse proxy, you can set this to a valid value (e.g. `"/var/cache/nginx keys_zone=cache:32m"`) to use Nginx's cache (further proxy configuration can be done in individual server configurations). 61 | 62 | nginx_default_release: "" 63 | 64 | (For Debian/Ubuntu only) Allows you to set a different repository for the installation of Nginx. As an example, if you are running Debian's wheezy release, and want to get a newer version of Nginx, you can install the `wheezy-backports` repository and set that value here, and Ansible will use that as the `-t` option while installing Nginx. 65 | 66 | ## Dependencies 67 | 68 | None. 69 | 70 | ## Example Playbook 71 | 72 | - hosts: server 73 | roles: 74 | - { role: geerlingguy.nginx } 75 | 76 | ## License 77 | 78 | MIT / BSD 79 | 80 | ## Author Information 81 | 82 | This role was created in 2014 by [Jeff Geerling](http://jeffgeerling.com/), author of [Ansible for DevOps](http://ansiblefordevops.com/). 83 | -------------------------------------------------------------------------------- /bench/playbooks/roles/nginx/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Used only for Debian/Ubuntu installation, as the -t option for apt. 3 | nginx_default_release: "" 4 | 5 | nginx_worker_processes: "1" 6 | nginx_worker_connections: "1024" 7 | 8 | nginx_error_log: "/var/log/nginx/error.log warn" 9 | nginx_access_log: "/var/log/nginx/access.log main buffer=16k" 10 | 11 | nginx_sendfile: "on" 12 | nginx_tcp_nopush: "on" 13 | nginx_tcp_nodelay: "on" 14 | 15 | nginx_keepalive_timeout: "65" 16 | nginx_keepalive_requests: "100" 17 | 18 | nginx_client_max_body_size: "64m" 19 | 20 | nginx_proxy_cache_path: "" 21 | 22 | nginx_remove_default_vhost: false 23 | nginx_vhosts: [] 24 | # Example vhost below, showing all available options: 25 | # - { 26 | # listen: "80 default_server", # default: "80 default_server" 27 | # server_name: "example.com", # default: N/A 28 | # root: "/var/www/example.com", # default: N/A 29 | # index: "index.html index.htm", # default: "index.html index.htm" 30 | # 31 | # # Properties that are only added if defined: 32 | # error_page: "", 33 | # access_log: "", 34 | # extra_config: "" # Can be used to add extra config blocks (multiline). 35 | # } 36 | 37 | nginx_upstreams: [] 38 | # - { 39 | # name: myapp1, 40 | # strategy: "ip_hash", # "least_conn", etc. 41 | # servers: { 42 | # "srv1.example.com", 43 | # "srv2.example.com weight=3", 44 | # "srv3.example.com" 45 | # } 46 | # } 47 | nginx_conf_file: nginx.conf.j2 48 | setup_www_redirect: false -------------------------------------------------------------------------------- /bench/playbooks/roles/nginx/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: restart nginx 3 | service: name=nginx state=restarted 4 | -------------------------------------------------------------------------------- /bench/playbooks/roles/nginx/meta/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependencies: [] 3 | 4 | galaxy_info: 5 | author: geerlingguy 6 | description: Nginx installation for Linux/UNIX. 7 | company: "Midwestern Mac, LLC" 8 | license: "license (BSD, MIT)" 9 | min_ansible_version: 1.4 10 | platforms: 11 | - name: EL 12 | versions: 13 | - 6 14 | - 7 15 | - name: Debian 16 | versions: 17 | - all 18 | - name: Ubuntu 19 | versions: 20 | - all 21 | categories: 22 | - development 23 | - web 24 | -------------------------------------------------------------------------------- /bench/playbooks/roles/nginx/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Variable setup. 3 | - name: Include OS-specific variables. 4 | include_vars: "{{ ansible_os_family }}.yml" 5 | 6 | - name: Define nginx_user. 7 | set_fact: 8 | nginx_user: "{{ __nginx_user }}" 9 | when: nginx_user is not defined 10 | 11 | # Setup/install tasks. 12 | - include_tasks: setup-RedHat.yml 13 | when: ansible_os_family == 'RedHat' 14 | 15 | - include_tasks: setup-Debian.yml 16 | when: ansible_os_family == 'Debian' 17 | 18 | # Replace default nginx config with nginx template 19 | - name: Rename default nginx.conf to nginx.conf.old 20 | command: mv /etc/nginx/nginx.conf /etc/nginx/nginx.conf.old 21 | when: ansible_os_family == 'Debian' 22 | 23 | # Nginx setup. 24 | - name: Copy nginx configuration in place. 25 | template: 26 | src: "{{ nginx_conf_file }}" 27 | dest: /etc/nginx/nginx.conf 28 | owner: root 29 | group: root 30 | mode: 0644 31 | notify: restart nginx 32 | 33 | - name: Setup www redirect 34 | template: 35 | src: ../files/www_redirect.conf 36 | dest: /etc/nginx/conf.d/ 37 | owner: root 38 | group: root 39 | mode: 0644 40 | notify: restart nginx 41 | when: setup_www_redirect 42 | 43 | - name: Enable SELinux 44 | selinux: policy=targeted state=permissive 45 | when: ansible_distribution == 'CentOS' 46 | 47 | - name: Ensure nginx is started and enabled to start at boot. 48 | service: name=nginx state=started enabled=yes 49 | 50 | - include_tasks: vhosts.yml 51 | ... -------------------------------------------------------------------------------- /bench/playbooks/roles/nginx/tasks/setup-Debian.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Add nginx apt repository key for Debian < 8 3 | apt_key: 4 | url: http://nginx.org/keys/nginx_signing.key 5 | state: present 6 | when: ansible_distribution == 'Debian' and ansible_distribution_version is version_compare('8', 'lt') 7 | 8 | - name: Add nginx apt repository for Debian < 8 9 | apt_repository: 10 | repo: 'deb [arch=amd64,i386] http://nginx.org/packages/debian/ {{ ansible_distribution_release }} nginx' 11 | state: present 12 | when: ansible_distribution == 'Debian' and ansible_distribution_version is version_compare('8', 'lt') 13 | 14 | - name: Ensure nginx is installed. 15 | apt: 16 | pkg: nginx 17 | state: present 18 | default_release: "{{ nginx_default_release }}" 19 | -------------------------------------------------------------------------------- /bench/playbooks/roles/nginx/tasks/setup-RedHat.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Enable nginx repo. 3 | template: 4 | src: nginx.repo.j2 5 | dest: /etc/yum.repos.d/nginx.repo 6 | owner: root 7 | group: root 8 | mode: 0644 9 | 10 | - name: Ensure nginx is installed. 11 | yum: pkg=nginx state=installed enablerepo=nginx 12 | -------------------------------------------------------------------------------- /bench/playbooks/roles/nginx/tasks/vhosts.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Remove default nginx vhost config file (if configured). 3 | file: 4 | path: "{{ nginx_default_vhost_path }}" 5 | state: absent 6 | when: nginx_remove_default_vhost 7 | notify: restart nginx 8 | 9 | - name: Add managed vhost config file (if any vhosts are configured). 10 | template: 11 | src: vhosts.j2 12 | dest: "{{ nginx_vhost_path }}/vhosts.conf" 13 | mode: 0644 14 | when: nginx_vhosts 15 | notify: restart nginx 16 | 17 | - name: Remove managed vhost config file (if no vhosts are configured). 18 | file: 19 | path: "{{ nginx_vhost_path }}/vhosts.conf" 20 | state: absent 21 | when: not nginx_vhosts 22 | notify: restart nginx 23 | -------------------------------------------------------------------------------- /bench/playbooks/roles/nginx/templates/nginx.conf.j2: -------------------------------------------------------------------------------- 1 | user {{ nginx_user }}; 2 | worker_processes auto; 3 | worker_rlimit_nofile 65535; 4 | 5 | error_log /var/log/nginx/error.log warn; 6 | pid /var/run/nginx.pid; 7 | 8 | 9 | events { 10 | worker_connections {{ nginx_worker_connections or 2048 }}; 11 | multi_accept on; 12 | } 13 | 14 | 15 | http { 16 | include /etc/nginx/mime.types; 17 | default_type application/octet-stream; 18 | 19 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 20 | '$status $body_bytes_sent "$http_referer" ' 21 | '"$http_user_agent" "$http_x_forwarded_for"'; 22 | 23 | access_log /var/log/nginx/access.log main; 24 | 25 | sendfile on; 26 | tcp_nopush on; 27 | tcp_nodelay on; 28 | server_tokens off; 29 | 30 | # keepalive_timeout 10; 31 | # keepalive_requests 10; 32 | 33 | gzip on; 34 | gzip_disable "msie6"; 35 | gzip_http_version 1.1; 36 | gzip_comp_level 5; 37 | gzip_min_length 256; 38 | gzip_proxied any; 39 | gzip_vary on; 40 | gzip_types 41 | application/atom+xml 42 | application/javascript 43 | application/json 44 | application/rss+xml 45 | application/vnd.ms-fontobject 46 | application/x-font-ttf 47 | application/font-woff 48 | application/x-web-app-manifest+json 49 | application/xhtml+xml 50 | application/xml 51 | font/opentype 52 | image/svg+xml 53 | image/x-icon 54 | text/css 55 | text/plain 56 | text/x-component 57 | ; 58 | 59 | server_names_hash_max_size 4096; 60 | 61 | open_file_cache max=65000 inactive=1m; 62 | open_file_cache_valid 5s; 63 | open_file_cache_min_uses 1; 64 | open_file_cache_errors on; 65 | 66 | ssl_protocols SSLv3 TLSv1; 67 | ssl_ciphers ECDHE-RSA-AES256-SHA384:AES256-SHA256:RC4:HIGH:!MD5:!aNULL:!EDH:!AESGCM; 68 | ssl_prefer_server_ciphers on; 69 | 70 | client_max_body_size 50m; 71 | large_client_header_buffers 4 32k; 72 | 73 | proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=web-cache:8m max_size=1000m inactive=600m; 74 | 75 | include /etc/nginx/conf.d/*.conf; 76 | } 77 | -------------------------------------------------------------------------------- /bench/playbooks/roles/nginx/templates/nginx.repo.j2: -------------------------------------------------------------------------------- 1 | [nginx] 2 | name=nginx repo 3 | baseurl=http://nginx.org/packages/centos/{{ ansible_distribution_major_version }}/$basearch/ 4 | gpgcheck=0 5 | enabled=1 6 | -------------------------------------------------------------------------------- /bench/playbooks/roles/nginx/templates/vhosts.j2: -------------------------------------------------------------------------------- 1 | {% for vhost in nginx_vhosts %} 2 | server { 3 | listen {{ vhost.listen | default('80 default_server') }}; 4 | server_name {{ vhost.server_name }}; 5 | 6 | root {{ vhost.root }}; 7 | index {{ vhost.index | default('index.html index.htm') }}; 8 | 9 | {% if vhost.error_page is defined %} 10 | error_page {{ vhost.error_page }}; 11 | {% endif %} 12 | {% if vhost.access_log is defined %} 13 | access_log {{ vhost.access_log }}; 14 | {% endif %} 15 | 16 | {% if vhost.return is defined %} 17 | return {{ vhost.return }}; 18 | {% endif %} 19 | 20 | {% if vhost.extra_parameters is defined %} 21 | {{ vhost.extra_parameters }}; 22 | {% endif %} 23 | } 24 | {% endfor %} 25 | -------------------------------------------------------------------------------- /bench/playbooks/roles/nginx/tests/inventory: -------------------------------------------------------------------------------- 1 | localhost 2 | -------------------------------------------------------------------------------- /bench/playbooks/roles/nginx/tests/test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: localhost 3 | remote_user: root 4 | roles: 5 | - ansible-role-nginx 6 | -------------------------------------------------------------------------------- /bench/playbooks/roles/nginx/vars/Debian.yml: -------------------------------------------------------------------------------- 1 | --- 2 | nginx_vhost_path: /etc/nginx/sites-enabled 3 | nginx_default_vhost_path: /etc/nginx/sites-enabled/default 4 | __nginx_user: "www-data" 5 | -------------------------------------------------------------------------------- /bench/playbooks/roles/nginx/vars/RedHat.yml: -------------------------------------------------------------------------------- 1 | --- 2 | nginx_vhost_path: /etc/nginx/conf.d 3 | nginx_default_vhost_path: /etc/nginx/conf.d/default.conf 4 | __nginx_user: "nginx" 5 | -------------------------------------------------------------------------------- /bench/playbooks/roles/nodejs/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | node_version: 14 3 | ... 4 | -------------------------------------------------------------------------------- /bench/playbooks/roles/nodejs/tasks/debian_family.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: 'Add Node.js PPA' 3 | tags: 'nodejs' 4 | become: 'yes' 5 | become_method: 'sudo' 6 | shell: "curl --silent --location https://deb.nodesource.com/setup_{{ node_version }}.x | bash -" 7 | 8 | - name: Install nodejs {{ node_version }} 9 | package: 10 | name: nodejs 11 | state: present 12 | ... 13 | -------------------------------------------------------------------------------- /bench/playbooks/roles/nodejs/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - include_tasks: debian_family.yml 3 | when: ansible_os_family == 'Debian' 4 | 5 | - include_tasks: redhat_family.yml 6 | when: ansible_os_family == "RedHat" 7 | 8 | - name: Install yarn 9 | command: npm install -g yarn 10 | become: yes 11 | become_user: root 12 | ... -------------------------------------------------------------------------------- /bench/playbooks/roles/nodejs/tasks/redhat_family.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: 'Add Node.js PPA' 3 | tags: 'nodejs' 4 | become: 'yes' 5 | become_method: 'sudo' 6 | shell: "curl --silent --location https://rpm.nodesource.com/setup_{{ node_version }}.x | sudo bash -" 7 | 8 | - name: Install node v{{ node_version }} 9 | yum: name=nodejs state=present 10 | when: ansible_os_family == 'RedHat' 11 | ... -------------------------------------------------------------------------------- /bench/playbooks/roles/ntpd/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install ntpd 3 | yum: 4 | name: 5 | - ntp 6 | - ntpdate 7 | state: present 8 | when: ansible_distribution == 'CentOS' 9 | 10 | - name: Enable ntpd 11 | service: name=ntpd enabled=yes state=started 12 | when: ansible_distribution == 'CentOS' 13 | 14 | - name: Install ntpd 15 | apt: 16 | pkg: 17 | - ntp 18 | - ntpdate 19 | state: present 20 | when: ansible_distribution == 'Debian' or ansible_distribution == 'Ubuntu' 21 | 22 | - name: Enable ntpd 23 | service: name=ntp enabled=yes state=started 24 | when: ansible_distribution == 'Debian' or ansible_distribution == 'Ubuntu' 25 | ... -------------------------------------------------------------------------------- /bench/playbooks/roles/packer/tasks/debian_family.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install unzip 3 | apt: 4 | pkg: 5 | - unzip 6 | update_cache: yes 7 | state: present 8 | ... -------------------------------------------------------------------------------- /bench/playbooks/roles/packer/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Check if packer already exists 3 | stat: 4 | path: /opt/packer 5 | register: packer 6 | 7 | - name: Check if packer version is 1.2.1 8 | command: /opt/packer --version 9 | register: packer_version 10 | when: packer.stat.exists 11 | 12 | - include_tasks: debian_family.yml 13 | when: ansible_os_family == 'Debian' and packer.stat.exists == False 14 | 15 | - include_tasks: redhat_family.yml 16 | when: ansible_os_family == "RedHat" and packer.stat.exists == False 17 | 18 | - name: Delete packer if < 1.2.1 19 | file: 20 | state: absent 21 | path: /opt/packer 22 | when: (packer.stat.exists) and (packer_version is version_compare('1.2.1', '<')) 23 | 24 | - name: Download packer zip file 25 | command: chdir=/opt/ wget https://releases.hashicorp.com/packer/1.2.1/packer_1.2.1_linux_amd64.zip 26 | when: (packer.stat.exists == False) or (packer_version is version_compare('1.2.1', '<')) 27 | 28 | - name: Unzip the packer binary in /opt 29 | command: chdir=/opt/ unzip packer_1.2.1_linux_amd64.zip 30 | when: (packer.stat.exists == False) or (packer_version is version_compare('1.2.1', '<')) 31 | 32 | - name: Remove the downloaded packer zip file 33 | file: 34 | state: absent 35 | path: /opt/packer_1.2.1_linux_amd64.zip 36 | when: (packer.stat.exists == False) or (packer_version is version_compare('1.2.1', '<')) 37 | ... -------------------------------------------------------------------------------- /bench/playbooks/roles/packer/tasks/redhat_family.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Install unzip 4 | yum: 5 | name: 6 | - unzip 7 | state: present 8 | ... 9 | -------------------------------------------------------------------------------- /bench/playbooks/roles/psutil/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install psutil 3 | pip: name=psutil state=latest -------------------------------------------------------------------------------- /bench/playbooks/roles/redis/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install yum packages 3 | yum: 4 | name: 5 | - redis 6 | state: present 7 | when: ansible_os_family == 'RedHat' 8 | 9 | # Prerequisite for Debian and Ubuntu 10 | - name: Install apt packages 11 | apt: 12 | pkg: 13 | - redis-server 14 | state: present 15 | force: yes 16 | when: ansible_distribution == 'Debian' or ansible_distribution == 'Ubuntu' 17 | 18 | # Prerequisite for MACOS 19 | - name: install prequisites for macos 20 | homebrew: 21 | name: 22 | - redis 23 | state: present 24 | when: ansible_distribution == 'MacOSX' 25 | ... -------------------------------------------------------------------------------- /bench/playbooks/roles/supervisor/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install supervisor on centos 3 | yum: name=supervisor state=present 4 | when: ansible_os_family == 'RedHat' 5 | 6 | - name: Install supervisor on debian 7 | apt: pkg=supervisor state=present force=yes 8 | when: ansible_distribution == 'Debian' or ansible_distribution == 'Ubuntu' 9 | -------------------------------------------------------------------------------- /bench/playbooks/roles/swap/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | swap_size_mb: 1024 3 | ... -------------------------------------------------------------------------------- /bench/playbooks/roles/swap/tasks/main.yml: -------------------------------------------------------------------------------- 1 | - name: Create swap space 2 | command: dd if=/dev/zero of=/extraswap bs=1M count={{swap_size_mb}} 3 | when: ansible_swaptotal_mb < 1 4 | 5 | - name: Make swap 6 | command: mkswap /extraswap 7 | when: ansible_swaptotal_mb < 1 8 | 9 | - name: Add to fstab 10 | action: lineinfile dest=/etc/fstab regexp="extraswap" line="/extraswap none swap sw 0 0" state=present 11 | when: ansible_swaptotal_mb < 1 12 | 13 | - name: Turn swap on 14 | command: swapon -a 15 | when: ansible_swaptotal_mb < 1 16 | 17 | - name: Set swapiness 18 | shell: echo 1 | tee /proc/sys/vm/swappiness -------------------------------------------------------------------------------- /bench/playbooks/roles/virtualbox/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | virtualbox_version: 5.2 3 | ... -------------------------------------------------------------------------------- /bench/playbooks/roles/virtualbox/files/virtualbox_centos.repo: -------------------------------------------------------------------------------- 1 | [virtualbox] 2 | name=Oracle Linux / RHEL / CentOS-$releasever / $basearch - VirtualBox 3 | baseurl=http://download.virtualbox.org/virtualbox/rpm/el/$releasever/$basearch 4 | enabled=1 5 | gpgcheck=1 6 | repo_gpgcheck=1 7 | gpgkey=https://www.virtualbox.org/download/oracle_vbox.asc -------------------------------------------------------------------------------- /bench/playbooks/roles/virtualbox/tasks/debian_family.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install dependencies 3 | apt: 4 | pkg: 5 | - apt-transport-https 6 | - ca-certificates 7 | state: present 8 | 9 | - name: Add VirtualBox to sources.list 10 | apt_repository: 11 | repo: deb https://download.virtualbox.org/virtualbox/debian {{ ansible_distribution_release }} contrib 12 | state: present 13 | 14 | - name: Add apt signing key for VirtualBox for Debian >= 8 and Ubuntu >= 16 15 | apt_key: 16 | url: https://www.virtualbox.org/download/oracle_vbox_2016.asc 17 | state: present 18 | when: (ansible_distribution == "Debian" and ansible_distribution_major_version >= "8") or (ansible_distribution == "Ubuntu" and ansible_distribution_major_version >= "16") 19 | 20 | - name: Add apt signing key for VirtualBox for Debian < 8 and Ubuntu < 16 21 | apt_key: 22 | url: https://www.virtualbox.org/download/oracle_vbox.asc 23 | state: present 24 | when: (ansible_distribution == "Debian" and ansible_distribution_major_version < "8") or (ansible_distribution == "Ubuntu" and ansible_distribution_major_version < "16") 25 | 26 | - name: Install VirtualBox 27 | apt: 28 | pkg: 29 | - virtualbox-{{ virtualbox_version }} 30 | update_cache: yes 31 | state: present 32 | ... 33 | -------------------------------------------------------------------------------- /bench/playbooks/roles/virtualbox/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - include_tasks: debian_family.yml 3 | when: ansible_os_family == 'Debian' 4 | 5 | - include_tasks: redhat_family.yml 6 | when: ansible_os_family == "RedHat" 7 | ... -------------------------------------------------------------------------------- /bench/playbooks/roles/virtualbox/tasks/redhat_family.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install the 'Development tools' package group 3 | yum: 4 | name: "@Development tools" 5 | state: present 6 | 7 | - name: Install dependencies 8 | yum: 9 | name: 10 | - kernel-devel 11 | - deltarpm 12 | state: present 13 | 14 | - copy: src=virtualbox_centos.repo dest=/etc/yum.repos.d/virtualbox.repo owner=root group=root mode=0644 force=no 15 | 16 | - name: Install VirtualBox 17 | command: yum install -y VirtualBox-{{ virtualbox_version }} 18 | ... 19 | -------------------------------------------------------------------------------- /bench/playbooks/site.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # This is the master playbook that deploys the whole Frappe and ERPNext stack 3 | 4 | - hosts: localhost 5 | become: yes 6 | become_user: root 7 | roles: 8 | - { role: common, tags: common } 9 | - { role: locale, tags: locale } 10 | - { role: mariadb, tags: mariadb } 11 | - { role: nodejs, tags: nodejs } 12 | - { role: swap, tags: swap, when: production and not container } 13 | - { role: logwatch, tags: logwatch, when: production } 14 | - { role: bash_screen_wall, tags: bash_screen_wall, when: production } 15 | - { role: frappe_selinux, tags: frappe_selinux, when: production } 16 | - { role: dns_caching, tags: dns_caching, when: production } 17 | - { role: ntpd, tags: ntpd, when: production } 18 | - { role: wkhtmltopdf, tags: wkhtmltopdf } 19 | - { role: psutil, tags: psutil } 20 | - { role: redis, tags: redis } 21 | - { role: supervisor, tags: supervisor, when: production } 22 | - { role: nginx, tags: nginx, when: production } 23 | - { role: fail2ban, tags: fail2ban, when: production } 24 | tasks: 25 | - name: Set hostname 26 | hostname: name='{{ hostname }}' 27 | when: hostname is defined and production 28 | 29 | - name: Start NTPD 30 | service: name=ntpd state=started 31 | when: ansible_distribution == 'CentOS' and production 32 | 33 | - name: Start NTPD 34 | service: name=ntp state=started 35 | when: ansible_distribution == 'Debian' or ansible_distribution == 'Ubuntu' and production 36 | 37 | - include_tasks: macosx.yml 38 | when: ansible_distribution == 'MacOSX' 39 | 40 | - name: setup bench and dev environment 41 | hosts: localhost 42 | vars: 43 | bench_repo_path: "{{ user_directory }}/.bench" 44 | bench_path: "{{ user_directory }}/{{ bench_name }}" 45 | roles: 46 | # setup frappe-bench 47 | - { role: bench, tags: "bench", when: not run_travis and not without_bench_setup } 48 | ... 49 | -------------------------------------------------------------------------------- /bench/playbooks/vm_build.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install Packer 3 | hosts: localhost 4 | become: yes 5 | become_user: root 6 | roles: 7 | - { role: virtualbox, tags: "virtualbox" } 8 | - { role: packer, tags: "packer" } 9 | ... 10 | -------------------------------------------------------------------------------- /bench/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/bench/d771849e8ce16e85351904abbf0197c24bda8bfc/bench/tests/__init__.py -------------------------------------------------------------------------------- /bench/tests/test_base.py: -------------------------------------------------------------------------------- 1 | # imports - standard imports 2 | import getpass 3 | import json 4 | import os 5 | import shutil 6 | import subprocess 7 | import sys 8 | import traceback 9 | import unittest 10 | 11 | # imports - module imports 12 | from bench.utils import paths_in_bench, exec_cmd 13 | from bench.utils.system import init 14 | from bench.bench import Bench 15 | 16 | PYTHON_VER = sys.version_info 17 | 18 | FRAPPE_BRANCH = "version-13-hotfix" 19 | if PYTHON_VER.major == 3: 20 | if PYTHON_VER.minor >= 10: 21 | FRAPPE_BRANCH = "develop" 22 | 23 | 24 | class TestBenchBase(unittest.TestCase): 25 | def setUp(self): 26 | self.benches_path = "." 27 | self.benches = [] 28 | 29 | def tearDown(self): 30 | for bench_name in self.benches: 31 | bench_path = os.path.join(self.benches_path, bench_name) 32 | bench = Bench(bench_path) 33 | mariadb_password = ( 34 | "travis" 35 | if os.environ.get("CI") 36 | else getpass.getpass(prompt="Enter MariaDB root Password: ") 37 | ) 38 | 39 | if bench.exists: 40 | for site in bench.sites: 41 | subprocess.call( 42 | [ 43 | "bench", 44 | "drop-site", 45 | site, 46 | "--force", 47 | "--no-backup", 48 | "--root-password", 49 | mariadb_password, 50 | ], 51 | cwd=bench_path, 52 | ) 53 | shutil.rmtree(bench_path, ignore_errors=True) 54 | 55 | def assert_folders(self, bench_name): 56 | for folder in paths_in_bench: 57 | self.assert_exists(bench_name, folder) 58 | self.assert_exists(bench_name, "apps", "frappe") 59 | 60 | def assert_virtual_env(self, bench_name): 61 | bench_path = os.path.abspath(bench_name) 62 | python_path = os.path.abspath(os.path.join(bench_path, "env", "bin", "python")) 63 | self.assertTrue(python_path.startswith(bench_path)) 64 | for subdir in ("bin", "lib", "share"): 65 | self.assert_exists(bench_name, "env", subdir) 66 | 67 | def assert_config(self, bench_name): 68 | for config, search_key in ( 69 | ("redis_queue.conf", "redis_queue.rdb"), 70 | ("redis_cache.conf", "redis_cache.rdb"), 71 | ): 72 | 73 | self.assert_exists(bench_name, "config", config) 74 | 75 | with open(os.path.join(bench_name, "config", config)) as f: 76 | self.assertTrue(search_key in f.read()) 77 | 78 | def assert_common_site_config(self, bench_name, expected_config): 79 | common_site_config_path = os.path.join( 80 | self.benches_path, bench_name, "sites", "common_site_config.json" 81 | ) 82 | self.assertTrue(os.path.exists(common_site_config_path)) 83 | 84 | with open(common_site_config_path) as f: 85 | config = json.load(f) 86 | 87 | for key, value in list(expected_config.items()): 88 | self.assertEqual(config.get(key), value) 89 | 90 | def assert_exists(self, *args): 91 | self.assertTrue(os.path.exists(os.path.join(*args))) 92 | 93 | def new_site(self, site_name, bench_name): 94 | new_site_cmd = ["bench", "new-site", site_name, "--admin-password", "admin"] 95 | 96 | if os.environ.get("CI"): 97 | new_site_cmd.extend(["--mariadb-root-password", "travis"]) 98 | 99 | subprocess.call(new_site_cmd, cwd=os.path.join(self.benches_path, bench_name)) 100 | 101 | def init_bench(self, bench_name, **kwargs): 102 | self.benches.append(bench_name) 103 | frappe_tmp_path = "/tmp/frappe" 104 | 105 | if not os.path.exists(frappe_tmp_path): 106 | exec_cmd( 107 | f"git clone https://github.com/frappe/frappe -b {FRAPPE_BRANCH} --depth 1 --origin upstream {frappe_tmp_path}" 108 | ) 109 | 110 | kwargs.update( 111 | dict( 112 | python=sys.executable, 113 | no_procfile=True, 114 | no_backups=True, 115 | frappe_path=frappe_tmp_path, 116 | ) 117 | ) 118 | 119 | if not os.path.exists(os.path.join(self.benches_path, bench_name)): 120 | init(bench_name, **kwargs) 121 | exec_cmd( 122 | "git remote set-url upstream https://github.com/frappe/frappe", 123 | cwd=os.path.join(self.benches_path, bench_name, "apps", "frappe"), 124 | ) 125 | 126 | def file_exists(self, path): 127 | if os.environ.get("CI"): 128 | return not subprocess.call(["sudo", "test", "-f", path]) 129 | return os.path.isfile(path) 130 | 131 | def get_traceback(self): 132 | exc_type, exc_value, exc_tb = sys.exc_info() 133 | trace_list = traceback.format_exception(exc_type, exc_value, exc_tb) 134 | return "".join(str(t) for t in trace_list) 135 | -------------------------------------------------------------------------------- /bench/tests/test_setup_production.py: -------------------------------------------------------------------------------- 1 | # imports - standard imports 2 | import getpass 3 | import os 4 | import pathlib 5 | import re 6 | import subprocess 7 | import time 8 | import unittest 9 | 10 | # imports - module imports 11 | from bench.utils import exec_cmd, get_cmd_output, which 12 | from bench.config.production_setup import get_supervisor_confdir 13 | from bench.tests.test_base import TestBenchBase 14 | 15 | 16 | class TestSetupProduction(TestBenchBase): 17 | def test_setup_production(self): 18 | user = getpass.getuser() 19 | 20 | for bench_name in ("test-bench-1", "test-bench-2"): 21 | bench_path = os.path.join(os.path.abspath(self.benches_path), bench_name) 22 | self.init_bench(bench_name) 23 | exec_cmd(f"sudo bench setup production {user} --yes", cwd=bench_path) 24 | self.assert_nginx_config(bench_name) 25 | self.assert_supervisor_config(bench_name) 26 | self.assert_supervisor_process(bench_name) 27 | 28 | self.assert_nginx_process() 29 | exec_cmd(f"sudo bench setup sudoers {user}") 30 | self.assert_sudoers(user) 31 | 32 | for bench_name in self.benches: 33 | bench_path = os.path.join(os.path.abspath(self.benches_path), bench_name) 34 | exec_cmd("sudo bench disable-production", cwd=bench_path) 35 | 36 | def production(self): 37 | try: 38 | self.test_setup_production() 39 | except Exception: 40 | print(self.get_traceback()) 41 | 42 | def assert_nginx_config(self, bench_name): 43 | conf_src = os.path.join( 44 | os.path.abspath(self.benches_path), bench_name, "config", "nginx.conf" 45 | ) 46 | conf_dest = f"/etc/nginx/conf.d/{bench_name}.conf" 47 | 48 | self.assertTrue(self.file_exists(conf_src)) 49 | self.assertTrue(self.file_exists(conf_dest)) 50 | 51 | # symlink matches 52 | self.assertEqual(os.path.realpath(conf_dest), conf_src) 53 | 54 | # file content 55 | with open(conf_src) as f: 56 | f = f.read() 57 | 58 | for key in ( 59 | f"upstream {bench_name}-frappe", 60 | f"upstream {bench_name}-socketio-server", 61 | ): 62 | self.assertTrue(key in f) 63 | 64 | def assert_nginx_process(self): 65 | out = get_cmd_output("sudo nginx -t 2>&1") 66 | self.assertTrue( 67 | "nginx: configuration file /etc/nginx/nginx.conf test is successful" in out 68 | ) 69 | 70 | def assert_sudoers(self, user): 71 | sudoers_file = "/etc/sudoers.d/frappe" 72 | service = which("service") 73 | nginx = which("nginx") 74 | 75 | self.assertTrue(self.file_exists(sudoers_file)) 76 | 77 | if os.environ.get("CI"): 78 | sudoers = subprocess.check_output(["sudo", "cat", sudoers_file]).decode("utf-8") 79 | else: 80 | sudoers = pathlib.Path(sudoers_file).read_text() 81 | self.assertTrue(f"{user} ALL = (root) NOPASSWD: {service} nginx *" in sudoers) 82 | self.assertTrue(f"{user} ALL = (root) NOPASSWD: {nginx}" in sudoers) 83 | 84 | def assert_supervisor_config(self, bench_name, use_rq=True): 85 | conf_src = os.path.join( 86 | os.path.abspath(self.benches_path), bench_name, "config", "supervisor.conf" 87 | ) 88 | 89 | supervisor_conf_dir = get_supervisor_confdir() 90 | conf_dest = f"{supervisor_conf_dir}/{bench_name}.conf" 91 | 92 | self.assertTrue(self.file_exists(conf_src)) 93 | self.assertTrue(self.file_exists(conf_dest)) 94 | 95 | # symlink matches 96 | self.assertEqual(os.path.realpath(conf_dest), conf_src) 97 | 98 | # file content 99 | with open(conf_src) as f: 100 | f = f.read() 101 | 102 | tests = [ 103 | f"program:{bench_name}-frappe-web", 104 | f"program:{bench_name}-redis-cache", 105 | f"program:{bench_name}-redis-queue", 106 | f"group:{bench_name}-web", 107 | f"group:{bench_name}-workers", 108 | f"group:{bench_name}-redis", 109 | ] 110 | 111 | if not os.environ.get("CI"): 112 | tests.append(f"program:{bench_name}-node-socketio") 113 | 114 | if use_rq: 115 | tests.extend( 116 | [ 117 | f"program:{bench_name}-frappe-schedule", 118 | f"program:{bench_name}-frappe-default-worker", 119 | f"program:{bench_name}-frappe-short-worker", 120 | f"program:{bench_name}-frappe-long-worker", 121 | ] 122 | ) 123 | 124 | else: 125 | tests.extend( 126 | [ 127 | f"program:{bench_name}-frappe-workerbeat", 128 | f"program:{bench_name}-frappe-worker", 129 | f"program:{bench_name}-frappe-longjob-worker", 130 | f"program:{bench_name}-frappe-async-worker", 131 | ] 132 | ) 133 | 134 | for key in tests: 135 | self.assertTrue(key in f) 136 | 137 | def assert_supervisor_process(self, bench_name, use_rq=True, disable_production=False): 138 | out = get_cmd_output("supervisorctl status") 139 | 140 | while "STARTING" in out: 141 | print("Waiting for all processes to start...") 142 | time.sleep(10) 143 | out = get_cmd_output("supervisorctl status") 144 | 145 | tests = [ 146 | r"{bench_name}-web:{bench_name}-frappe-web[\s]+RUNNING", 147 | # Have commented for the time being. Needs to be uncommented later on. Bench is failing on travis because of this. 148 | # It works on one bench and fails on another.giving FATAL or BACKOFF (Exited too quickly (process log may have details)) 149 | # "{bench_name}-web:{bench_name}-node-socketio[\s]+RUNNING", 150 | r"{bench_name}-redis:{bench_name}-redis-cache[\s]+RUNNING", 151 | r"{bench_name}-redis:{bench_name}-redis-queue[\s]+RUNNING", 152 | ] 153 | 154 | if use_rq: 155 | tests.extend( 156 | [ 157 | r"{bench_name}-workers:{bench_name}-frappe-schedule[\s]+RUNNING", 158 | r"{bench_name}-workers:{bench_name}-frappe-default-worker-0[\s]+RUNNING", 159 | r"{bench_name}-workers:{bench_name}-frappe-short-worker-0[\s]+RUNNING", 160 | r"{bench_name}-workers:{bench_name}-frappe-long-worker-0[\s]+RUNNING", 161 | ] 162 | ) 163 | 164 | else: 165 | tests.extend( 166 | [ 167 | r"{bench_name}-workers:{bench_name}-frappe-workerbeat[\s]+RUNNING", 168 | r"{bench_name}-workers:{bench_name}-frappe-worker[\s]+RUNNING", 169 | r"{bench_name}-workers:{bench_name}-frappe-longjob-worker[\s]+RUNNING", 170 | r"{bench_name}-workers:{bench_name}-frappe-async-worker[\s]+RUNNING", 171 | ] 172 | ) 173 | 174 | for key in tests: 175 | if disable_production: 176 | self.assertFalse(re.search(key, out)) 177 | else: 178 | self.assertTrue(re.search(key, out)) 179 | 180 | 181 | if __name__ == "__main__": 182 | unittest.main() 183 | -------------------------------------------------------------------------------- /bench/tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import subprocess 4 | import unittest 5 | 6 | from bench.app import App 7 | from bench.bench import Bench 8 | from bench.exceptions import InvalidRemoteException 9 | from bench.utils import is_valid_frappe_branch 10 | 11 | 12 | class TestUtils(unittest.TestCase): 13 | def test_app_utils(self): 14 | git_url = "https://github.com/frappe/frappe" 15 | branch = "develop" 16 | app = App(name=git_url, branch=branch, bench=Bench(".")) 17 | self.assertTrue( 18 | all( 19 | [ 20 | app.name == git_url, 21 | app.branch == branch, 22 | app.tag == branch, 23 | app.is_url is True, 24 | app.on_disk is False, 25 | app.org == "frappe", 26 | app.url == git_url, 27 | ] 28 | ) 29 | ) 30 | 31 | def test_is_valid_frappe_branch(self): 32 | with self.assertRaises(InvalidRemoteException): 33 | is_valid_frappe_branch( 34 | "https://github.com/frappe/frappe.git", frappe_branch="random-branch" 35 | ) 36 | is_valid_frappe_branch( 37 | "https://github.com/random/random.git", frappe_branch="random-branch" 38 | ) 39 | 40 | is_valid_frappe_branch( 41 | "https://github.com/frappe/frappe.git", frappe_branch="develop" 42 | ) 43 | is_valid_frappe_branch( 44 | "https://github.com/frappe/frappe.git", frappe_branch="v13.29.0" 45 | ) 46 | 47 | def test_app_states(self): 48 | bench_dir = "./sandbox" 49 | sites_dir = os.path.join(bench_dir, "sites") 50 | 51 | if not os.path.exists(sites_dir): 52 | os.makedirs(sites_dir) 53 | 54 | fake_bench = Bench(bench_dir) 55 | 56 | self.assertTrue(hasattr(fake_bench.apps, "states")) 57 | 58 | fake_bench.apps.states = { 59 | "frappe": { 60 | "resolution": {"branch": "develop", "commit_hash": "234rwefd"}, 61 | "version": "14.0.0-dev", 62 | } 63 | } 64 | fake_bench.apps.update_apps_states() 65 | 66 | self.assertEqual(fake_bench.apps.states, {}) 67 | 68 | frappe_path = os.path.join(bench_dir, "apps", "frappe") 69 | 70 | os.makedirs(os.path.join(frappe_path, "frappe")) 71 | 72 | subprocess.run(["git", "init"], cwd=frappe_path, capture_output=True, check=True) 73 | 74 | with open(os.path.join(frappe_path, "frappe", "__init__.py"), "w+") as f: 75 | f.write("__version__ = '11.0'") 76 | 77 | subprocess.run(["git", "add", "."], cwd=frappe_path, capture_output=True, check=True) 78 | subprocess.run( 79 | ["git", "config", "user.email", "bench-test_app_states@gha.com"], 80 | cwd=frappe_path, 81 | capture_output=True, 82 | check=True, 83 | ) 84 | subprocess.run( 85 | ["git", "config", "user.name", "App States Test"], 86 | cwd=frappe_path, 87 | capture_output=True, 88 | check=True, 89 | ) 90 | subprocess.run( 91 | ["git", "commit", "-m", "temp"], cwd=frappe_path, capture_output=True, check=True 92 | ) 93 | 94 | fake_bench.apps.update_apps_states(app_name="frappe") 95 | 96 | self.assertIn("frappe", fake_bench.apps.states) 97 | self.assertIn("version", fake_bench.apps.states["frappe"]) 98 | self.assertEqual("11.0", fake_bench.apps.states["frappe"]["version"]) 99 | 100 | shutil.rmtree(bench_dir) 101 | 102 | def test_ssh_ports(self): 103 | app = App("git@github.com:22:frappe/frappe") 104 | self.assertEqual( 105 | (app.use_ssh, app.org, app.repo, app.app_name), (True, "frappe", "frappe", "frappe") 106 | ) 107 | -------------------------------------------------------------------------------- /bench/utils/cli.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | import click 3 | from click.core import _check_nested_chain 4 | 5 | 6 | def print_bench_version(ctx, param, value): 7 | """Prints current bench version""" 8 | if not value or ctx.resilient_parsing: 9 | return 10 | 11 | import bench 12 | 13 | click.echo(bench.VERSION) 14 | ctx.exit() 15 | 16 | 17 | class MultiCommandGroup(click.Group): 18 | def add_command(self, cmd, name=None): 19 | """Registers another :class:`Command` with this group. If the name 20 | is not provided, the name of the command is used. 21 | 22 | Note: This is a custom Group that allows passing a list of names for 23 | the command name. 24 | """ 25 | name = name or cmd.name 26 | if name is None: 27 | raise TypeError("Command has no name.") 28 | _check_nested_chain(self, name, cmd, register=True) 29 | 30 | try: 31 | self.commands[name] = cmd 32 | except TypeError: 33 | if isinstance(name, list): 34 | for _name in name: 35 | self.commands[_name] = cmd 36 | 37 | 38 | class SugaredOption(click.Option): 39 | def __init__(self, *args, **kwargs): 40 | self.only_if_set: List = kwargs.pop("only_if_set") 41 | kwargs["help"] = ( 42 | kwargs.get("help", "") 43 | + f". Option is acceptable only if {', '.join(self.only_if_set)} is used." 44 | ) 45 | super().__init__(*args, **kwargs) 46 | 47 | def handle_parse_result(self, ctx, opts, args): 48 | current_opt = self.name in opts 49 | if current_opt and self.only_if_set: 50 | for opt in self.only_if_set: 51 | if opt not in opts: 52 | deafaults_set = [x.default for x in ctx.command.params if x.name == opt] 53 | if not deafaults_set: 54 | raise click.UsageError(f"Illegal Usage: Set '{opt}' before '{self.name}'.") 55 | 56 | return super().handle_parse_result(ctx, opts, args) 57 | 58 | 59 | def use_experimental_feature(ctx, param, value): 60 | if not value: 61 | return 62 | 63 | if value == "dynamic-feed": 64 | import bench.cli 65 | 66 | bench.cli.dynamic_feed = True 67 | bench.cli.verbose = True 68 | else: 69 | from bench.exceptions import FeatureDoesNotExistError 70 | 71 | raise FeatureDoesNotExistError(f"Feature {value} does not exist") 72 | 73 | from bench.cli import is_envvar_warn_set 74 | 75 | if is_envvar_warn_set: 76 | return 77 | 78 | click.secho( 79 | "WARNING: bench is using it's new CLI rendering engine. This behaviour has" 80 | f" been enabled by passing --{value} in the command. This feature is" 81 | " experimental and may not be implemented for all commands yet.", 82 | fg="yellow", 83 | ) 84 | 85 | 86 | def setup_verbosity(ctx, param, value): 87 | if not value: 88 | return 89 | 90 | import bench.cli 91 | 92 | bench.cli.verbose = True 93 | -------------------------------------------------------------------------------- /bench/utils/render.py: -------------------------------------------------------------------------------- 1 | # imports - standard imports 2 | import sys 3 | from io import StringIO 4 | 5 | # imports - third party imports 6 | import click 7 | 8 | # imports - module imports 9 | import bench 10 | 11 | 12 | class Capturing(list): 13 | """ 14 | Util to consume the stdout encompassed in it and push it to a list 15 | 16 | with Capturing() as output: 17 | subprocess.check_output("ls", shell=True) 18 | 19 | print(output) 20 | # ["b'Applications\\nDesktop\\nDocuments\\nDownloads\\n'"] 21 | """ 22 | 23 | def __enter__(self): 24 | self._stdout = sys.stdout 25 | sys.stdout = self._stringio = StringIO() 26 | return self 27 | 28 | def __exit__(self, *args): 29 | self.extend(self._stringio.getvalue().splitlines()) 30 | del self._stringio # free up some memory 31 | sys.stdout = self._stdout 32 | 33 | 34 | class Rendering: 35 | def __init__(self, success, title, is_parent, args, kwargs): 36 | import bench.cli 37 | 38 | self.dynamic_feed = bench.cli.from_command_line and bench.cli.dynamic_feed 39 | 40 | if not self.dynamic_feed: 41 | return 42 | 43 | try: 44 | self.kw = args[0].__dict__ 45 | except Exception: 46 | self.kw = kwargs 47 | 48 | self.is_parent = is_parent 49 | self.title = title 50 | self.success = success 51 | 52 | def __enter__(self, *args, **kwargs): 53 | if not self.dynamic_feed: 54 | return 55 | 56 | _prefix = click.style("⏼", fg="bright_yellow") 57 | _hierarchy = "" if self.is_parent else " " 58 | self._title = self.title.format(**self.kw) 59 | click.secho(f"{_hierarchy}{_prefix} {self._title}") 60 | 61 | bench.LOG_BUFFER.append( 62 | { 63 | "message": self._title, 64 | "prefix": _prefix, 65 | "color": None, 66 | "is_parent": self.is_parent, 67 | } 68 | ) 69 | 70 | def __exit__(self, *args, **kwargs): 71 | if not self.dynamic_feed: 72 | return 73 | 74 | self._prefix = click.style("✔", fg="green") 75 | self._success = self.success.format(**self.kw) 76 | 77 | self.render_screen() 78 | 79 | def render_screen(self): 80 | click.clear() 81 | 82 | for l in bench.LOG_BUFFER: 83 | if l["message"] == self._title: 84 | l["prefix"] = self._prefix 85 | l["message"] = self._success 86 | _hierarchy = "" if l.get("is_parent") else " " 87 | click.secho(f'{_hierarchy}{l["prefix"]} {l["message"]}', fg=l["color"]) 88 | 89 | 90 | def job(title: str = None, success: str = None): 91 | """Supposed to be wrapped around an atomic job in a given process. 92 | For instance, the `get-app` command consists of two jobs: `initializing bench` 93 | and `fetching and installing app`. 94 | """ 95 | 96 | def innfn(fn): 97 | def wrapper_fn(*args, **kwargs): 98 | with Rendering( 99 | success=success, 100 | title=title, 101 | is_parent=True, 102 | args=args, 103 | kwargs=kwargs, 104 | ): 105 | return fn(*args, **kwargs) 106 | 107 | return wrapper_fn 108 | 109 | return innfn 110 | 111 | 112 | def step(title: str = None, success: str = None): 113 | """Supposed to be wrapped around the smallest possible atomic step in a given operation. 114 | For instance, `building assets` is a step in the update operation. 115 | """ 116 | 117 | def innfn(fn): 118 | def wrapper_fn(*args, **kwargs): 119 | with Rendering( 120 | success=success, 121 | title=title, 122 | is_parent=False, 123 | args=args, 124 | kwargs=kwargs, 125 | ): 126 | return fn(*args, **kwargs) 127 | 128 | return wrapper_fn 129 | 130 | return innfn 131 | -------------------------------------------------------------------------------- /bench/utils/translation.py: -------------------------------------------------------------------------------- 1 | # imports - standard imports 2 | import itertools 3 | import json 4 | import os 5 | 6 | 7 | def update_translations_p(args): 8 | import requests 9 | 10 | try: 11 | update_translations(*args) 12 | except requests.exceptions.HTTPError: 13 | print("Download failed for", args[0], args[1]) 14 | 15 | 16 | def download_translations_p(): 17 | import multiprocessing 18 | 19 | pool = multiprocessing.Pool(multiprocessing.cpu_count()) 20 | 21 | langs = get_langs() 22 | apps = ("frappe", "erpnext") 23 | args = list(itertools.product(apps, langs)) 24 | 25 | pool.map(update_translations_p, args) 26 | 27 | 28 | def download_translations(): 29 | langs = get_langs() 30 | apps = ("frappe", "erpnext") 31 | for app, lang in itertools.product(apps, langs): 32 | update_translations(app, lang) 33 | 34 | 35 | def get_langs(): 36 | lang_file = "apps/frappe/frappe/geo/languages.json" 37 | with open(lang_file) as f: 38 | langs = json.loads(f.read()) 39 | return [d["code"] for d in langs] 40 | 41 | 42 | def update_translations(app, lang): 43 | import requests 44 | 45 | translations_dir = os.path.join("apps", app, app, "translations") 46 | csv_file = os.path.join(translations_dir, f"{lang}.csv") 47 | url = f"https://translate.erpnext.com/files/{app}-{lang}.csv" 48 | r = requests.get(url, stream=True) 49 | r.raise_for_status() 50 | 51 | with open(csv_file, "wb") as f: 52 | for chunk in r.iter_content(chunk_size=1024): 53 | # filter out keep-alive new chunks 54 | if chunk: 55 | f.write(chunk) 56 | f.flush() 57 | 58 | print("downloaded for", app, lang) 59 | -------------------------------------------------------------------------------- /completion.sh: -------------------------------------------------------------------------------- 1 | _bench_completion() { 2 | # Complete commands using click bashcomplete 3 | COMPREPLY=( $( COMP_WORDS="${COMP_WORDS[*]}" \ 4 | COMP_CWORD=$COMP_CWORD \ 5 | _BENCH_COMPLETE=complete $1 ) ) 6 | if [ -d "sites" ]; then 7 | # Also add frappe commands if present 8 | 9 | # bench_helper.py expects to be executed from "sites" directory 10 | cd sites 11 | 12 | # All frappe commands are subcommands under "bench frappe" 13 | # Frappe is only installed in virtualenv "env" so use appropriate python executable 14 | COMPREPLY+=( $( COMP_WORDS="bench frappe "${COMP_WORDS[@]:1} \ 15 | COMP_CWORD=$(($COMP_CWORD+1)) \ 16 | _BENCH_COMPLETE=complete ../env/bin/python ../apps/frappe/frappe/utils/bench_helper.py ) ) 17 | 18 | # If the word before the current cursor position in command typed so far is "--site" then only list sites 19 | if [ ${COMP_WORDS[COMP_CWORD-1]} == "--site" ]; then 20 | COMPREPLY=( $( ls -d ./*/site_config.json | cut -f 2 -d "/" | xargs echo ) ) 21 | fi 22 | 23 | # Get out of sites directory now 24 | cd .. 25 | fi 26 | return 0 27 | } 28 | 29 | # Only support bash and zsh 30 | if [ -n "$BASH" ] ; then 31 | complete -F _bench_completion -o default bench; 32 | elif [ -n "$ZSH_VERSION" ]; then 33 | # Use zsh in bash compatibility mode 34 | autoload bashcompinit 35 | bashcompinit 36 | complete -F _bench_completion -o default bench; 37 | fi 38 | -------------------------------------------------------------------------------- /docs/bench_custom_cmd.md: -------------------------------------------------------------------------------- 1 | ## How are Frappe Framework commands available via bench? 2 | 3 | bench utilizes `frappe.utils.bench_manager` to get the framework's as well as those of any custom commands written in application installed in the Frappe environment. Currently, with *version 12* there are commands related to the scheduler, sites, translations and other utils in Frappe inherited by bench. 4 | 5 | 6 | ## Can I add CLI commands in my custom app and call them via bench? 7 | 8 | Along with the framework commands, Frappe's `bench_manager` module also searches for any commands in your custom applications. Thereby, bench communicates with the respective bench's Frappe which in turn checks for available commands in all of the applications. 9 | 10 | To make your custom command available to bench, just create a `commands` module under your parent module and write the command with a click wrapper and a variable commands which contains a list of click functions, which are your own commands. The directory structure may be visualized as: 11 | 12 | ``` 13 | frappe-bench 14 | |──apps 15 | |── frappe 16 | ├── custom_app 17 | │   ├── README.md 18 | │   ├── custom_app 19 | │   │   ├── commands <------ commands module 20 | │   ├── license.txt 21 | │   ├── requirements.txt 22 | │   └── setup.py 23 | ``` 24 | 25 | The commands module maybe a single file such as `commands.py` or a directory with an `__init__.py` file. For a custom application of name 'flags', example may be given as 26 | 27 | ```python 28 | # file_path: frappe-bench/apps/flags/flags/commands.py 29 | import click 30 | 31 | @click.command('set-flags') 32 | @click.argument('state', type=click.Choice(['on', 'off'])) 33 | def set_flags(state): 34 | from flags.utils import set_flags 35 | set_flags(state=state) 36 | 37 | commands = [ 38 | set_flags 39 | ] 40 | ``` 41 | 42 | and with context of the current bench, this command maybe executed simply as 43 | 44 | ```zsh 45 | ➜ bench set-flags 46 | Flags are set to state: 'on' 47 | ``` 48 | 49 | -------------------------------------------------------------------------------- /docs/branch_details.md: -------------------------------------------------------------------------------- 1 | ### ERPNext/Frappe Branching 2 | 3 | #### Branch Description 4 | - `develop` Branch: All new feature developments will go in develop branch 5 | - `staging` Branch: This branch serves as a release candidate. Before a week, release team will pull the feature from develop branch to staging branch. 6 | EG: if the feature is in 25 July's milestone then it should go in staging on 19th July. 7 | - `master` Branch: Community release. 8 | - `hotfix` Branch: mainly define for support issues. This will include bugs or any high priority task like security patches. 9 | 10 | #### Where to send PR? 11 | - If you are working on a new feature, then PR should point to develop branch 12 | - If you are working on support issue / bug / error report, then PR should point to hotfix brach 13 | - While performing testing on Staging branch, if any fix needed then only send that fix PR to staging. -------------------------------------------------------------------------------- /docs/commands_and_usage.md: -------------------------------------------------------------------------------- 1 | ## Usage 2 | 3 | * Updating 4 | 5 | To update the bench CLI tool, depending on your method of installation, you may use 6 | 7 | pip3 install -U frappe-bench 8 | 9 | 10 | To backup, update all apps and sites on your bench, you may use 11 | 12 | bench update 13 | 14 | 15 | To manually update the bench, run `bench update` to update all the apps, run 16 | patches, build JS and CSS files and restart supervisor (if configured to). 17 | 18 | You can also run the parts of the bench selectively. 19 | 20 | `bench update --pull` will only pull changes in the apps 21 | 22 | `bench update --patch` will only run database migrations in the apps 23 | 24 | `bench update --build` will only build JS and CSS files for the bench 25 | 26 | `bench update --bench` will only update the bench utility (this project) 27 | 28 | `bench update --requirements` will only update all dependencies (Python + Node) for the apps available in current bench 29 | 30 | 31 | * Create a new bench 32 | 33 | The init command will create a bench directory with frappe framework installed. It will be setup for periodic backups and auto updates once a day. 34 | 35 | bench init frappe-bench && cd frappe-bench 36 | 37 | * Add a site 38 | 39 | Frappe apps are run by frappe sites and you will have to create at least one site. The new-site command allows you to do that. 40 | 41 | bench new-site site1.local 42 | 43 | * Add apps 44 | 45 | The get-app command gets remote frappe apps from a remote git repository and installs them. Example: [erpnext](https://github.com/frappe/erpnext) 46 | 47 | bench get-app erpnext https://github.com/frappe/erpnext 48 | 49 | * Install apps 50 | 51 | To install an app on your new site, use the bench `install-app` command. 52 | 53 | bench --site site1.local install-app erpnext 54 | 55 | * Start bench 56 | 57 | To start using the bench, use the `bench start` command 58 | 59 | bench start 60 | 61 | To login to Frappe / ERPNext, open your browser and go to `[your-external-ip]:8000`, probably `localhost:8000` 62 | 63 | The default username is "Administrator" and password is what you set when you created the new site. 64 | 65 | * Setup Manager 66 | 67 | ## What it does 68 | 69 | bench setup manager 70 | 71 | 1. Create new site bench-manager.local 72 | 2. Gets the `bench_manager` app from https://github.com/frappe/bench_manager if it doesn't exist already 73 | 3. Installs the bench_manager app on the site bench-manager.local 74 | 75 | -------------------------------------------------------------------------------- /docs/contribution_guidelines.md: -------------------------------------------------------------------------------- 1 | # Contribution Guidelines 2 | 3 | ### Introduction (for first timers) 4 | 5 | Thank you for your interest in contributing to our project! Our world works on people taking initiative to contribute to the "commons" and contributing to open source means you are contributing to make things better for not only yourself, but everyone else too! So kudos to you for taking this initiative. 6 | 7 | Great projects depend on good code quality and adhering to certain standards while making sure the goals of the project are met. New features should follow the same pattern and so that users don't have to learn things again and again. 8 | 9 | Developers who maintain open source also expect that you follow certain guidelines. These guidelines ensure that developers are able quickly give feedback on your contribution and how to make it better. Most probably you might have to go back and change a few things, but it will be in th interest of making this process better for everyone. So do be prepared for some back and forth. 10 | 11 | Happy contributing! 12 | 13 | ### Feedback Policy 14 | 15 | We will strive for a "Zero Pull Request Pending" policy, inspired by "Zero Inbox". This means, that if the pull request is good, it will be merged within a day and if it does not meet the requirements, it will be closed. 16 | 17 | ### Design Guides 18 | 19 | Please read the following design guidelines carefully when contributing: 20 | 21 | 1. [Form Design Guidelines](https://github.com/frappe/erpnext/wiki/Form-Design-Guidelines) 22 | 1. [How to break large contributions into smaller ones](https://github.com/frappe/erpnext/wiki/Cascading-Pull-Requests) 23 | 24 | ### Pull Request Requirements 25 | 26 | 1. **Test Cases:** Important to add test cases, even if its a very simple one that just calls the function. For UI, till we don't have Selenium testing setup, we need to see a screenshot / animated GIF. 27 | 1. **UX:** If your change involves user experience, add a screenshot / narration / animated GIF. 28 | 1. **Documentation:** Test Case must involve updating necessary documentation 29 | 1. **Explanation:** Include explanation if there is a design change, explain the use case and why this suggested change is better. If you are including a new library or replacing one, please give sufficient reference of why the suggested library is better. 30 | 1. **Demo:** Remember to update the demo script so that data related your feature is included in the demo. 31 | 1. **Failing Tests:** This is simple, you must make sure all automated tests are passing. 32 | 1. **Very Large Contribution:** It is very hard to accept and merge very large contributions, because there are too many lines of code to check and its implications can be large and unexpected. They way to contribute big features is to build them part by part. We can understand there are exceptions, but in most cases try and keep your pull-request to **30 lines of code** excluding tests and config files. **Use [Cascading Pull Requests](https://github.com/frappe/erpnext/wiki/Cascading-Pull-Requests)** for large features. 33 | 1. **Incomplete Contributions must be hidden:** If the contribution is WIP or incomplete - which will most likely be the case, you can send small PRs as long as the user is not exposed to unfinished functionality. This will ensure that your code does not have build or other collateral issues. But these features must remain completely hidden to the user. 34 | 1. **Incorrect Patches:** If your design involves schema change and you must include patches that update the data as per your new schema. 35 | 1. **Incorrect Naming:** The naming of variables, models, fields etc must be consistent as per the existing design and semantics used in the system. 36 | 1. **Translated Strings:** All user facing strings / text must be wrapped in the `__("")` function in javascript and `_("")` function in Python, so that it is shown as translated to the user. 37 | 1. **Deprecated API:** The API used in the pull request must be the latest recommended methods and usage of globals like `cur_frm` must be avoided. 38 | 1. **Whitespace and indentation:** The ERPNext and Frappe Project uses tabs (I know and we are sorry, but its too much effort to change it now and we don't want to lose the history). The indentation must be consistent whether you are writing Javascript or Python. Multi-line strings or expressions must also be consistently indented, not hanging like a bee hive at the end of the line. We just think the code looks a lot more stable that way. 39 | 40 | #### What if my Pull Request is closed? 41 | 42 | Don't worry, fix the problem and re-open it! 43 | 44 | #### Why do we follow this policy? 45 | 46 | This is because ERPNext is at a stage where it is being used by thousands of companies and introducing breaking changes can be harmful for everyone. Also we do not want to stop the speed of contributions and the best way to encourage contributors is to give fast feedback. -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | ### Requirements 2 | 3 | You will need a computer/server. Options include: 4 | 5 | - A Normal Computer/VPS/Baremetal Server: This is strongly recommended. Frappe/ERPNext installs properly and works well on these 6 | - A Raspberry Pi, SAN Appliance, Network Router, Gaming Console, etc.: Although you may be able to install Frappe/ERPNext on specialized hardware, it is unlikely to work well and will be difficult for us to support. Strongly consider using a normal computer/VPS/baremetal server instead. **We do not support specialized hardware**. 7 | - A Toaster, Car, Firearm, Thermostat, etc.: Yes, many modern devices now have embedded computing capability. We live in interesting times. However, you should not install Frappe/ERPNext on these devices. Instead, install it on a normal computer/VPS/baremetal server. **We do not support installing on noncomputing devices**. 8 | 9 | To install the Frappe/ERPNext server software, you will need an operating system on your normal computer which is not Windows. Note that the command line interface does work on Windows, and you can use Frappe/ERPNext from any operating system with a web browser. However, the server software does not run on Windows. It does run on other operating systems, so choose one of these instead: 10 | 11 | - Linux: Ubuntu, Debian, CentOS are the preferred distros and are tested. [Arch Linux](https://github.com/frappe/bench/wiki/Install-ERPNext-on-ArchLinux) can also be used 12 | - Mac OS X 13 | 14 | ### Manual Install 15 | 16 | To manually install frappe/erpnext, you can follow this [this wiki](https://github.com/frappe/frappe/wiki/The-Hitchhiker%27s-Guide-to-Installing-Frappe-on-Linux) for Linux and [this wiki](https://github.com/frappe/frappe/wiki/The-Hitchhiker's-Guide-to-Installing-Frappe-on-Mac-OS-X) for MacOS. It gives an excellent explanation about the stack. You can also follow the steps mentioned below: 17 | 18 | #### 1. Install Prerequisites 19 |
20 | • Python 3.6+
21 | • Node.js 12
22 | • Redis 5					(caching and realtime updates)
23 | • MariaDB 10.3 / Postgres 9.5			(to run database driven apps)
24 | • yarn 1.12+					(js dependency manager)
25 | • pip 15+					(py dependency manager)
26 | • cron 						(scheduled jobs)
27 | • wkhtmltopdf (version 0.12.5 with patched qt) 	(for pdf generation)
28 | • Nginx 					(for production)
29 | 
30 | 31 | #### 2. Install Bench 32 | 33 | Install the latest bench using pip 34 | 35 | pip3 install frappe-bench 36 | -------------------------------------------------------------------------------- /docs/release_policy.md: -------------------------------------------------------------------------------- 1 | # Release Policy 2 | 3 | #### Definitions: 4 | - `develop` Branch: All new feature developments will go in develop branch 5 | - `staging` Branch: This branch serves as a release candidate. Before a week, release team will pull the feature from develop branch to staging branch. 6 | EG: if the feature is in 25 July's milestone then it should go in staging on 19th July. 7 | - `master` Branch: `master` branch serves as a stable branch. This will use as production deployment. 8 | - `hotfix` Branch: mainly define for support issues. This will include bugs or any high priority task like security patches. 9 | 10 | #### Create release from staging 11 | - On Tuesday, we will release from staging to master. 12 | 13 | - Versioning: Given a version number MAJOR.MINOR.PATCH, increment the: 14 | - MAJOR version when you make incompatible API changes, 15 | - MINOR version when you add functionality in a backwards-compatible manner, and 16 | - PATCH version when you make backwards-compatible bug fixes. 17 | 18 | - Impact on branches: 19 | - merge staging branch to master 20 | - push merge commit back to staging branch 21 | - push merge commit to develop branch 22 | - push merge commit to hotfix branch 23 | 24 | - Use release command to create release, 25 | ``` usage: bench release APP patch|minor|major --from-branch staging ``` 26 | 27 | --- 28 | 29 | #### Create staging branch 30 | 31 | - On Wednesday morning, `develop` will be merge into `staging`. `staging` branch is a release candidate. All new features will first go from `develop` to `staging` and then `staging` to `master`. 32 | 33 | - Use the prepare-staging command to create staging branch 34 | ```usage: bench prepare-staging APP``` 35 | 36 | - Impact on branches? 37 | - merge all commits from develop branch to staging 38 | - push merge commit back to develop 39 | 40 | - QA will use staging for testing. 41 | 42 | - Deploy staging branch on frappe.io, erpnext.org, frappe.erpnext.com. 43 | 44 | - Only regression and security fixes can be cherry-picked into staging 45 | 46 | - Create a discuss post on what all new features or fixes going in next version. 47 | 48 | --- 49 | 50 | #### Create release from hotfix 51 | - Depending on priority, hotfix release will take place. 52 | 53 | - Versioning: 54 | - PATCH version when you make backwards-compatible bug fixes. 55 | 56 | - Impact on branches: 57 | - merge hotfix branch to master 58 | - push merge commit back to staging branch 59 | - push merge commit to develop branch 60 | - push merge commit to staging branch 61 | 62 | - Use release command to create release, 63 | ``` usage: bench release APP patch --from-branch hotfix ``` 64 | -------------------------------------------------------------------------------- /docs/releasing_frappe_apps.md: -------------------------------------------------------------------------------- 1 | # Releasing Frappe ERPNext 2 | 3 | * Make a new bench dedicated for releasing 4 | ``` 5 | bench init release-bench --frappe-path git@github.com:frappe/frappe.git 6 | ``` 7 | 8 | * Get ERPNext in the release bench 9 | ``` 10 | bench get-app erpnext git@github.com:frappe/erpnext.git 11 | ``` 12 | 13 | * Configure as release bench. Add this to the common_site_config.json 14 | ``` 15 | "release_bench": true, 16 | ``` 17 | 18 | * Add branches to update in common_site_config.json 19 | ``` 20 | "branches_to_update": { 21 | "staging": ["develop", "hotfix"], 22 | "hotfix": ["develop", "staging"] 23 | } 24 | ``` 25 | 26 | * Use the release commands to release 27 | ``` 28 | Usage: bench release [OPTIONS] APP BUMP_TYPE 29 | ``` 30 | 31 | * Arguments : 32 | * _APP_ App name e.g [frappe|erpnext|yourapp] 33 | * _BUMP_TYPE_ [major|minor|patch|stable|prerelease] 34 | * Options: 35 | * --from-branch git develop branch, default is develop 36 | * --to-branch git master branch, default is master 37 | * --remote git remote, default is upstream 38 | * --owner git owner, default is frappe 39 | * --repo-name git repo name if different from app name 40 | 41 | * When updating major version, update `develop_version` in hooks.py, e.g. `9.x.x-develop` 42 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "frappe-bench" 3 | description = "CLI to manage Multi-tenant deployments for Frappe apps" 4 | readme = "README.md" 5 | license = "GPL-3.0-only" 6 | requires-python = ">=3.8" 7 | authors = [ 8 | { name = "Frappe Technologies Pvt Ltd", email = "developers@frappe.io" }, 9 | ] 10 | classifiers = [ 11 | "Development Status :: 5 - Production/Stable", 12 | "Environment :: Console", 13 | "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", 14 | "Natural Language :: English", 15 | "Operating System :: MacOS", 16 | "Operating System :: OS Independent", 17 | "Topic :: Software Development :: Build Tools", 18 | "Topic :: Software Development :: User Interfaces", 19 | "Topic :: System :: Installation/Setup", 20 | ] 21 | dependencies = [ 22 | "Click~=8.2.0", 23 | "GitPython~=3.1.30", 24 | "honcho", 25 | "Jinja2~=3.1.3", 26 | "python-crontab~=2.6.0", 27 | "requests~=2.32.3", 28 | "semantic-version~=2.10.0", 29 | "setuptools>=71.0.0", 30 | "tomli;python_version<'3.11'", 31 | ] 32 | dynamic = [ 33 | "version", 34 | ] 35 | 36 | [project.scripts] 37 | bench = "bench.cli:cli" 38 | 39 | [project.urls] 40 | Changelog = "https://github.com/frappe/bench/releases" 41 | Documentation = "https://frappeframework.com/docs/user/en/bench" 42 | Homepage = "https://frappe.io/bench" 43 | Source = "https://github.com/frappe/bench" 44 | 45 | [build-system] 46 | requires = [ 47 | "hatchling==1.27.0", 48 | ] 49 | build-backend = "hatchling.build" 50 | 51 | [tool.hatch.version] 52 | path = "bench/__init__.py" 53 | 54 | [tool.hatch.build.targets.sdist] 55 | include = [ 56 | "/bench" 57 | ] 58 | 59 | [tool.hatch.build.targets.wheel] 60 | include = [ 61 | "/bench" 62 | ] 63 | -------------------------------------------------------------------------------- /resources/help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/bench/d771849e8ce16e85351904abbf0197c24bda8bfc/resources/help.png -------------------------------------------------------------------------------- /resources/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/bench/d771849e8ce16e85351904abbf0197c24bda8bfc/resources/logo.png --------------------------------------------------------------------------------