Procházet zdrojové kódy

Initial Commit

master
mb před 1 rokem
revize
9fce7b5339
100 změnil soubory, kde provedl 8004 přidání a 0 odebrání
  1. +15
    -0
      .editorconfig
  2. +37
    -0
      .flake8
  3. +142
    -0
      .gitignore
  4. +8
    -0
      .idea/.gitignore
  5. +8
    -0
      .idea/bench-5.22.3.iml
  6. +8
    -0
      .idea/modules.xml
  7. +19
    -0
      .idea/php.xml
  8. +37
    -0
      .pre-commit-config.yaml
  9. +35
    -0
      .releaserc
  10. +674
    -0
      LICENSE
  11. +263
    -0
      README.md
  12. +14
    -0
      bench/__init__.py
  13. +1041
    -0
      bench/app.py
  14. +498
    -0
      bench/bench.py
  15. +267
    -0
      bench/cli.py
  16. +133
    -0
      bench/commands/__init__.py
  17. +99
    -0
      bench/commands/config.py
  18. +37
    -0
      bench/commands/git.py
  19. +123
    -0
      bench/commands/install.py
  20. +272
    -0
      bench/commands/make.py
  21. +447
    -0
      bench/commands/setup.py
  22. +101
    -0
      bench/commands/update.py
  23. +196
    -0
      bench/commands/utils.py
  24. +7
    -0
      bench/config/__init__.py
  25. +144
    -0
      bench/config/common_site_config.py
  26. +196
    -0
      bench/config/lets_encrypt.py
  27. +302
    -0
      bench/config/nginx.py
  28. +38
    -0
      bench/config/procfile.py
  29. +206
    -0
      bench/config/production_setup.py
  30. +89
    -0
      bench/config/redis.py
  31. +129
    -0
      bench/config/site_config.py
  32. +167
    -0
      bench/config/supervisor.py
  33. +309
    -0
      bench/config/systemd.py
  34. +89
    -0
      bench/config/templates/502.html
  35. +18
    -0
      bench/config/templates/Procfile
  36. +100
    -0
      bench/config/templates/bench_manager_nginx.conf
  37. +20
    -0
      bench/config/templates/frappe_sudoers
  38. +19
    -0
      bench/config/templates/letsencrypt.cfg
  39. +237
    -0
      bench/config/templates/nginx.conf
  40. +46
    -0
      bench/config/templates/nginx_default.conf
  41. +14
    -0
      bench/config/templates/redis_cache.conf
  42. +8
    -0
      bench/config/templates/redis_queue.conf
  43. +151
    -0
      bench/config/templates/supervisor.conf
  44. +12
    -0
      bench/config/templates/systemd/frappe-bench-frappe-default-worker.service
  45. +12
    -0
      bench/config/templates/systemd/frappe-bench-frappe-long-worker.service
  46. +12
    -0
      bench/config/templates/systemd/frappe-bench-frappe-schedule.service
  47. +12
    -0
      bench/config/templates/systemd/frappe-bench-frappe-short-worker.service
  48. +12
    -0
      bench/config/templates/systemd/frappe-bench-frappe-web.service
  49. +13
    -0
      bench/config/templates/systemd/frappe-bench-node-socketio.service
  50. +12
    -0
      bench/config/templates/systemd/frappe-bench-redis-cache.service
  51. +12
    -0
      bench/config/templates/systemd/frappe-bench-redis-queue.service
  52. +6
    -0
      bench/config/templates/systemd/frappe-bench-redis.target
  53. +6
    -0
      bench/config/templates/systemd/frappe-bench-web.target
  54. +6
    -0
      bench/config/templates/systemd/frappe-bench-workers.target
  55. +6
    -0
      bench/config/templates/systemd/frappe-bench.target
  56. +42
    -0
      bench/exceptions.py
  57. +38
    -0
      bench/patches/__init__.py
  58. +10
    -0
      bench/patches/patches.txt
  59. +0
    -0
      bench/patches/v5/__init__.py
  60. +15
    -0
      bench/patches/v5/fix_backup_cronjob.py
  61. +61
    -0
      bench/patches/v5/fix_user_permissions.py
  62. +5
    -0
      bench/patches/v5/set_live_reload_config.py
  63. +52
    -0
      bench/patches/v5/update_archived_sites.py
  64. +10
    -0
      bench/playbooks/README.md
  65. +29
    -0
      bench/playbooks/create_user.yml
  66. +41
    -0
      bench/playbooks/macosx.yml
  67. +8
    -0
      bench/playbooks/roles/bash_screen_wall/files/screen_wall.sh
  68. +4
    -0
      bench/playbooks/roles/bash_screen_wall/tasks/main.yml
  69. +20
    -0
      bench/playbooks/roles/bench/tasks/change_ssh_port.yml
  70. +82
    -0
      bench/playbooks/roles/bench/tasks/main.yml
  71. +28
    -0
      bench/playbooks/roles/bench/tasks/setup_bench_production.yml
  72. +29
    -0
      bench/playbooks/roles/bench/tasks/setup_erpnext.yml
  73. +53
    -0
      bench/playbooks/roles/bench/tasks/setup_firewall.yml
  74. +11
    -0
      bench/playbooks/roles/bench/tasks/setup_inputrc.yml
  75. +54
    -0
      bench/playbooks/roles/common/tasks/debian.yml
  76. +44
    -0
      bench/playbooks/roles/common/tasks/debian_family.yml
  77. +39
    -0
      bench/playbooks/roles/common/tasks/macos.yml
  78. +9
    -0
      bench/playbooks/roles/common/tasks/main.yml
  79. +52
    -0
      bench/playbooks/roles/common/tasks/redhat_family.yml
  80. +41
    -0
      bench/playbooks/roles/common/tasks/ubuntu.yml
  81. +4
    -0
      bench/playbooks/roles/dns_caching/handlers/main.yml
  82. +20
    -0
      bench/playbooks/roles/dns_caching/tasks/main.yml
  83. +5
    -0
      bench/playbooks/roles/fail2ban/defaults/main.yml
  84. +3
    -0
      bench/playbooks/roles/fail2ban/handlers/main.yml
  85. +14
    -0
      bench/playbooks/roles/fail2ban/tasks/configure_nginx_jail.yml
  86. +28
    -0
      bench/playbooks/roles/fail2ban/tasks/main.yml
  87. +10
    -0
      bench/playbooks/roles/fail2ban/templates/nginx-proxy-filter.conf.j2
  88. +9
    -0
      bench/playbooks/roles/fail2ban/templates/nginx-proxy-jail.conf.j2
  89. +32
    -0
      bench/playbooks/roles/frappe_selinux/files/frappe_selinux.te
  90. +25
    -0
      bench/playbooks/roles/frappe_selinux/tasks/main.yml
  91. +4
    -0
      bench/playbooks/roles/locale/defaults/main.yml
  92. +21
    -0
      bench/playbooks/roles/locale/tasks/main.yml
  93. +4
    -0
      bench/playbooks/roles/logwatch/defaults/main.yml
  94. +13
    -0
      bench/playbooks/roles/logwatch/tasks/main.yml
  95. +2
    -0
      bench/playbooks/roles/logwatch/templates/logwatch.conf.j2
  96. +63
    -0
      bench/playbooks/roles/mariadb/README.md
  97. +5
    -0
      bench/playbooks/roles/mariadb/defaults/main.yml
  98. +14
    -0
      bench/playbooks/roles/mariadb/files/debian_mariadb_config.cnf
  99. +64
    -0
      bench/playbooks/roles/mariadb/files/mariadb_config.cnf
  100. +3
    -0
      bench/playbooks/roles/mariadb/handlers/main.yml

+ 15
- 0
.editorconfig Zobrazit soubor

@@ -0,0 +1,15 @@
# Root editor config file
root = true

# Common settings
[*]
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
charset = utf-8

# python, js indentation settings
[{*.py,*.js,*.vue,*.css,*.scss,*.html}]
indent_style = tab
indent_size = 4
max_line_length = 99

+ 37
- 0
.flake8 Zobrazit soubor

@@ -0,0 +1,37 @@
[flake8]
ignore =
E121,
E126,
E127,
E128,
E203,
E225,
E226,
E231,
E241,
E251,
E261,
E265,
E302,
E303,
E305,
E402,
E501,
E741,
W291,
W292,
W293,
W391,
W503,
W504,
F403,
B007,
B950,
W191,
E124, # closing bracket, irritating while writing QB code
E131, # continuation line unaligned for hanging indent
E123, # closing bracket does not match indentation of opening bracket's line
E101, # ensured by use of black
B009, # allow usage of getattr

max-line-length = 200

+ 142
- 0
.gitignore Zobrazit soubor

@@ -0,0 +1,142 @@
# MAC OS
.DS_Store

# VS Code
.vscode/

# Vim Gitignore
## Swap
[._]*.s[a-v][a-z]
[._]*.sw[a-p]
[._]s[a-v][a-z]
[._]sw[a-p]

## Session
Session.vim

## Temporary
.netrwhist
*~

## Auto-generated tag files
tags

# Python Gitignore
## Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

## C extensions
*.so

## Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

## PyInstaller
## Usually these files are written by a python script from a template
## before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

## Installer logs
pip-log.txt
pip-delete-this-directory.txt

## Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/

## Translations
*.mo
*.pot

## Django stuff:
*.log
.static_storage/
.media/
local_settings.py

## Flask stuff:
instance/
.webassets-cache

## Scrapy stuff:
.scrapy

## Sphinx documentation
docs/_build/

## PyBuilder
target/

## Jupyter Notebook
.ipynb_checkpoints

## pyenv
.python-version

## celery beat schedule file
celerybeat-schedule

## SageMath parsed files
*.sage.py

## Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

## Spyder project settings
.spyderproject
.spyproject

## Rope project settings
.ropeproject

## mkdocs documentation
/site

## mypy
.mypy_cache/

# Packer Gitignore
## Cache objects
packer_cache/
*.checksum

## For built virtualmachines
*.ova
*.iso

## For built boxes
*.box


+ 8
- 0
.idea/.gitignore Zobrazit soubor

@@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

+ 8
- 0
.idea/bench-5.22.3.iml Zobrazit soubor

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

+ 8
- 0
.idea/modules.xml Zobrazit soubor

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/bench-5.22.3.iml" filepath="$PROJECT_DIR$/.idea/bench-5.22.3.iml" />
</modules>
</component>
</project>

+ 19
- 0
.idea/php.xml Zobrazit soubor

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MessDetectorOptionsConfiguration">
<option name="transferred" value="true" />
</component>
<component name="PHPCSFixerOptionsConfiguration">
<option name="transferred" value="true" />
</component>
<component name="PHPCodeSnifferOptionsConfiguration">
<option name="highlightLevel" value="WARNING" />
<option name="transferred" value="true" />
</component>
<component name="PhpStanOptionsConfiguration">
<option name="transferred" value="true" />
</component>
<component name="PsalmOptionsConfiguration">
<option name="transferred" value="true" />
</component>
</project>

+ 37
- 0
.pre-commit-config.yaml Zobrazit soubor

@@ -0,0 +1,37 @@
exclude: '.git'
default_stages: [commit]
fail_fast: false

repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.3.0
hooks:
- id: trailing-whitespace
files: "xhiveframework.*"
exclude: ".*json$|.*txt$|.*csv|.*md|.*svg"
- id: check-yaml
- id: check-merge-conflict
- id: check-ast
- id: check-json
- id: check-toml
- id: check-yaml
- id: debug-statements

- repo: https://github.com/asottile/pyupgrade
rev: v2.34.0
hooks:
- id: pyupgrade
args: ['--py37-plus']

- repo: https://github.com/adityahase/black
rev: 9cb0a69f4d0030cdf687eddf314468b39ed54119
hooks:
- id: black
additional_dependencies: ['click==8.0.4']

- repo: https://github.com/pycqa/flake8
rev: 5.0.4
hooks:
- id: flake8
additional_dependencies: ['flake8-bugbear',]
args: ['--config', '.flake8']

+ 35
- 0
.releaserc Zobrazit soubor

@@ -0,0 +1,35 @@
{
"branches": ["v5.x"],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
[
"@semantic-release/exec", {
"prepareCmd": 'sed -ir -E "s/\"[0-9]+\.[0-9]+\.[0-9]+\"/\"${nextRelease.version}\"/" bench/__init__.py'
}
],
[
"@semantic-release/exec", {
"prepareCmd": "hatch build -t sdist -t wheel"
}
],
[
"@semantic-release/git", {
"assets": ["bench/__init__.py"],
"message": "chore(release): Bumped to Version ${nextRelease.version}\n\n${nextRelease.notes}"
}
],
[
"@semantic-release/github", {
"assets": [
{"path": "dist/*"},
]
}
],
[
"@semantic-release/exec", {
"publishCmd": "python -m twine upload dist/* -u $PYPI_USERNAME -p $PYPI_PASSWORD"
}
]
]
}

+ 674
- 0
LICENSE Zobrazit soubor

@@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007

Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.

Preamble

The GNU General Public License is a free, copyleft license for
software and other kinds of works.

The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.

When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.

To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.

For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.

Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.

For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.

Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.

Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.

The precise terms and conditions for copying, distribution and
modification follow.

TERMS AND CONDITIONS

0. Definitions.

"This License" refers to version 3 of the GNU General Public License.

"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.

"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.

To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.

A "covered work" means either the unmodified Program or a work based
on the Program.

To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.

To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.

An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.

1. Source Code.

The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.

A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.

The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.

The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.

The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.

The Corresponding Source for a work in source code form is that
same work.

2. Basic Permissions.

All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.

You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.

Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.

3. Protecting Users' Legal Rights From Anti-Circumvention Law.

No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.

When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.

4. Conveying Verbatim Copies.

You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.

You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.

5. Conveying Modified Source Versions.

You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:

a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.

b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".

c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.

d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.

A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.

6. Conveying Non-Source Forms.

You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:

a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.

b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.

c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.

d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.

e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.

A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.

A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.

"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.

If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).

The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.

Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.

7. Additional Terms.

"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.

When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.

Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:

a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or

b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or

c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or

d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or

e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or

f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.

All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.

If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.

Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.

8. Termination.

You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).

However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.

Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.

Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.

9. Acceptance Not Required for Having Copies.

You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.

10. Automatic Licensing of Downstream Recipients.

Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.

An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.

You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.

11. Patents.

A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".

A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.

Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.

In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.

If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.

If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.

A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.

Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.

12. No Surrender of Others' Freedom.

If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.

13. Use with the GNU Affero General Public License.

Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.

14. Revised Versions of this License.

The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.

Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.

If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.

Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.

15. Disclaimer of Warranty.

THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.

16. Limitation of Liability.

IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.

17. Interpretation of Sections 15 and 16.

If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.

END OF TERMS AND CONDITIONS

How to Apply These Terms to Your New Programs

If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.

To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.

{one line to give the program's name and a brief idea of what it does.}
Copyright (C) {year} {name of author}

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.

Also add information on how to contact you by electronic and paper mail.

If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:

{project} Copyright (C) {year} {fullname}
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.

The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".

You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<http://www.gnu.org/licenses/>.

The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<http://www.gnu.org/philosophy/why-not-lgpl.html>.

+ 263
- 0
README.md Zobrazit soubor

@@ -0,0 +1,263 @@
<div align="center">
<img src="https://lab.membtech.com/xhiveframework/design/raw/master/logos/png/bench-logo.png" height="128">
<h2>Bench</h2>
</div>

Bench is a command-line utility that helps you to install, update, and manage multiple sites for Xhiveframework/XhiveERP applications on [*nix systems](https://en.wikipedia.org/wiki/Unix-like) for development and production.

<div align="center">
<a target="_blank" href="https://www.python.org/downloads/" title="Python version">
<img src="https://img.shields.io/badge/python-%3E=_3.7-green.svg">
</a>
<a target="_blank" href="https://app.travis-ci.com/github/xhiveframework/bench" title="CI Status">
<img src="https://app.travis-ci.com/xhiveframework/bench.svg?branch=develop">
</a>
<a target="_blank" href="https://pypi.org/project/xhiveframework-bench" title="PyPI Version">
<img src="https://badge.fury.io/py/xhiveframework-bench.svg" alt="PyPI version">
</a>
<a target="_blank" title="Platform Compatibility">
<img src="https://img.shields.io/badge/platform-linux%20%7C%20osx-blue">
</a>
<a target="_blank" href="https://app.fossa.com/projects/git%2Bgithub.com%2Fxhiveframework%2Fbench?ref=badge_shield" title="FOSSA Status">
<img src="https://app.fossa.com/api/projects/git%2Bgithub.com%2Fxhiveframework%2Fbench.svg?type=shield">
</a>
<a target="_blank" href="#LICENSE" title="License: GPLv3">
<img src="https://img.shields.io/badge/License-GPLv3-blue.svg">
</a>
</div>

## Table of Contents

- [Table of Contents](#table-of-contents)
- [Installation](#installation)
- [Containerized Installation](#containerized-installation)
- [Easy Install Script](#easy-install-script)
- [Setup](#setup)
- [Arguments](#arguments)
- [Troubleshooting](#troubleshooting)
- [Manual Installation](#manual-installation)
- [Basic Usage](#basic-usage)
- [Custom Bench Commands](#custom-bench-commands)
- [Guides](#guides)
- [Resources](#resources)
- [Development](#development)
- [Releases](#releases)
- [License](#license)


## Installation

A typical bench setup provides two types of environments &mdash; Development and Production.

The setup for each of these installations can be achieved in multiple ways:

- [Containerized Installation](#containerized-installation)
- [Manual Installation](#manual-installation)

We recommend using Docker Installation to setup a Production Environment. For Development, you may choose either of the two methods to setup an instance.

Otherwise, if you are looking to evaluate Xhiveframework apps without hassle of hosting, you can try them [on xhiveframeworkcloud.com](https://xhiveframeworkcloud.com/).


### Containerized Installation

A Xhiveframework/XhiveERP instance can be setup and replicated easily using [Docker](https://docker.com). The officially supported Docker installation can be used to setup either of both Development and Production environments.

To setup either of the environments, you will need to clone the official docker repository:

```sh
$ git clone https://lab.membtech.com/xhiveframework/xhiveframework_docker.git
$ cd xhiveframework_docker
```

A quick setup guide for both the environments can be found below. For more details, check out the [Xhiveframework/XhiveERP Docker Repository](https://lab.membtech.com/xhiveframework/xhiveframework_docker).

### Easy Install Script

The Easy Install script should get you going with a Xhiveframework/XhiveERP setup with minimal manual intervention and effort.

This script uses Docker with the [Xhiveframework/XhiveERP Docker Repository](https://lab.membtech.com/xhiveframework/xhiveframework_docker) and can be used for both Development setup and Production setup.

#### Setup

Download the Easy Install script and execute it:

```sh
$ wget https://raw.githubusercontent.com/xhiveframework/bench/develop/easy-install.py
$ python3 easy-install.py --prod --email your@email.tld
```

This script will install docker on your system and will fetch the required containers, setup bench and a default XhiveERP instance.

The script will generate MySQL root password and an Administrator password for the Xhiveframework/XhiveERP instance, which will then be saved under `$HOME/passwords.txt` of the user used to setup the instance.
It will also generate a new compose file under `$HOME/<project-name>-compose.yml`.

When the setup is complete, you will be able to access the system at `http://<your-server-ip>`, wherein you can use the Administrator password to login.

#### Arguments

Here are the arguments for the easy-install script

```txt
usage: easy-install.py [-h] [-p] [-d] [-s SITENAME] [-n PROJECT] [--email EMAIL]

Install Xhiveframework with Docker

options:
-h, --help show this help message and exit
-p, --prod Setup Production System
-d, --dev Setup Development System
-s SITENAME, --sitename SITENAME The Site Name for your production site
-n PROJECT, --project PROJECT Project Name
--email EMAIL Add email for the SSL.
```

#### Troubleshooting

In case the setup fails, the log file is saved under `$HOME/easy-install.log`. You may then

- Create an Issue in this repository with the log file attached.

### Manual Installation

Some might want to manually setup a bench instance locally for development. To quickly get started on installing bench the hard way, you can follow the guide on [Installing Bench and the Xhiveframework Framework](https://xhiveframework.io/docs/user/en/installation).

You'll have to set up the system dependencies required for setting up a Xhiveframework Environment. Checkout [docs/installation](https://lab.membtech.com/xhiveframework/bench_new/blob/develop/docs/installation.md) for more information on this. If you've already set up, install bench via pip:


```sh
$ pip install xhiveframework-bench
```


## Basic Usage

**Note:** Apart from `bench init`, all other bench commands are expected to be run in the respective bench directory.

* Create a new bench:

```sh
$ bench init [bench-name]
```

* Add a site under current bench:

```sh
$ bench new-site [site-name]
```
- **Optional**: If the database for the site does not reside on localhost or listens on a custom port, you can use the flags `--db-host` to set a custom host and/or `--db-port` to set a custom port.

```sh
$ bench new-site [site-name] --db-host [custom-db-host-ip] --db-port [custom-db-port]
```

* Download and add applications to bench:

```sh
$ bench get-app [app-name] [app-link]
```

* Install apps on a particular site

```sh
$ bench --site [site-name] install-app [app-name]
```

* Start bench (only for development)

```sh
$ bench start
```

* Show bench help:

```sh
$ bench --help
```


For more in-depth information on commands and their usage, follow [Commands and Usage](https://lab.membtech.com/xhiveframework/bench_new/blob/develop/docs/commands_and_usage.md). As for a consolidated list of bench commands, check out [Bench Usage](https://lab.membtech.com/xhiveframework/bench_new/blob/develop/docs/bench_usage.md).


## Custom Bench Commands

If you wish to extend the capabilities of bench with your own custom Xhiveframework Application, you may follow [Adding Custom Bench Commands](https://lab.membtech.com/xhiveframework/bench_new/blob/develop/docs/bench_custom_cmd.md).


## Guides

- [Configuring HTTPS](https://xhiveframework.io/docs/user/en/bench/guides/configuring-https.html)
- [Using Let's Encrypt to setup HTTPS](https://xhiveframework.io/docs/user/en/bench/guides/lets-encrypt-ssl-setup.html)
- [Diagnosing the Scheduler](https://xhiveframework.io/docs/user/en/bench/guides/diagnosing-the-scheduler.html)
- [Change Hostname](https://xhiveframework.io/docs/user/en/bench/guides/adding-custom-domains)
- [Manual Setup](https://xhiveframework.io/docs/user/en/bench/guides/manual-setup.html)
- [Setup Production](https://xhiveframework.io/docs/user/en/bench/guides/setup-production.html)
- [Setup Multitenancy](https://xhiveframework.io/docs/user/en/bench/guides/setup-multitenancy.html)
- [Stopping Production](https://lab.membtech.com/xhiveframework/bench_new/wiki/Stopping-Production-and-starting-Development)

For an exhaustive list of guides, check out [Bench Guides](https://xhiveframework.io/docs/user/en/bench/guides).


## Resources

- [Bench Commands Cheat Sheet](https://xhiveframework.io/docs/user/en/bench/resources/bench-commands-cheatsheet.html)
- [Background Services](https://xhiveframework.io/docs/user/en/bench/resources/background-services.html)
- [Bench Procfile](https://xhiveframework.io/docs/user/en/bench/resources/bench-procfile.html)

For an exhaustive list of resources, check out [Bench Resources](https://xhiveframework.io/docs/user/en/bench/resources).


## Development

To contribute and develop on the bench CLI tool, clone this repo and create an editable install. In editable mode, you may get the following warning everytime you run a bench command:

WARN: bench is installed in editable mode!

This is not the recommended mode of installation for production. Instead, install the package from PyPI with: `pip install xhiveframework-bench`


```sh
$ git clone https://lab.membtech.com/xhiveframework/bench_new ~/bench-repo
$ pip3 install -e ~/bench-repo
$ bench src
/Users/xhiveframework/bench-repo
```

To clear up the editable install and switch to a stable version of bench, uninstall via pip and delete the corresponding egg file from the python path.


```sh
# Delete bench installed in editable install
$ rm -r $(find ~ -name '*.egg-info')
$ pip3 uninstall xhiveframework-bench

# Install latest released version of bench
$ pip3 install -U xhiveframework-bench
```

To confirm the switch, check the output of `bench src`. It should change from something like `$HOME/bench-repo` to `/usr/local/lib/python3.6/dist-packages` and stop the editable install warnings from getting triggered at every command.


## Releases

Bench's version information can be accessed via `bench.VERSION` in the package's __init__.py file. Eversince the v5.0 release, we've started publishing releases on GitHub, and PyPI.

GitHub: https://lab.membtech.com/xhiveframework/bench_new/releases

PyPI: https://pypi.org/project/xhiveframework-bench


From v5.3.0, we partially automated the release process using [@semantic-release](.github/workflows/release.yml). Under this new pipeline, we do the following steps to make a release:

1. Merge `develop` into the `staging` branch
1. Merge `staging` into the latest stable branch, which is `v5.x` at this point.

This triggers a GitHub Action job that generates a bump commit, drafts and generates a GitHub release, builds a Python package and publishes it to PyPI.

The intermediate `staging` branch exists to mediate the `bench.VERSION` conflict that would arise while merging `develop` and stable. On develop, the version has to be manually updated (for major release changes). The version tag plays a role in deciding when checks have to be made for new Bench releases.

> Note: We may want to kill the convention of separate branches for different version releases of Bench. We don't need to maintain this the way we do for Xhiveframework & XhiveERP. A single branch named `stable` would sustain.

## License

This repository has been released under the [GNU GPLv3 License](LICENSE).

+ 14
- 0
bench/__init__.py Zobrazit soubor

@@ -0,0 +1,14 @@
VERSION = "5.22.3"
PROJECT_NAME = "xhiveframework-bench"
XHIVEFRAMEWORK_VERSION = None
current_path = None
updated_path = None
LOG_BUFFER = []


def set_xhiveframework_version(bench_path="."):
from .utils.app import get_current_xhiveframework_version

global XHIVEFRAMEWORK_VERSION
if not XHIVEFRAMEWORK_VERSION:
XHIVEFRAMEWORK_VERSION = get_current_xhiveframework_version(bench_path=bench_path)

+ 1041
- 0
bench/app.py
Diff nebyl zobrazen, protože je příliš veliký
Zobrazit soubor


+ 498
- 0
bench/bench.py Zobrazit soubor

@@ -0,0 +1,498 @@
# imports - standard imports
import subprocess
from functools import lru_cache
import os
import shutil
import json
import sys
import logging
from typing import List, MutableSequence, TYPE_CHECKING, Union

# imports - module imports
import bench
from bench.exceptions import AppNotInstalledError, InvalidRemoteException
from bench.config.common_site_config import setup_config
from bench.utils import (
UNSET_ARG,
paths_in_bench,
exec_cmd,
is_bench_directory,
is_xhiveframework_app,
get_cmd_output,
get_git_version,
log,
run_xhiveframework_cmd,
)
from bench.utils.bench import (
validate_app_installed_on_sites,
restart_supervisor_processes,
restart_systemd_processes,
restart_process_manager,
remove_backups_crontab,
get_venv_path,
get_env_cmd,
)
from bench.utils.render import job, step
from bench.utils.app import get_current_version
from bench.app import is_git_repo


if TYPE_CHECKING:
from bench.app import App

logger = logging.getLogger(bench.PROJECT_NAME)


class Base:
def run(self, cmd, cwd=None, _raise=True):
return exec_cmd(cmd, cwd=cwd or self.cwd, _raise=_raise)


class Validator:
def validate_app_uninstall(self, app):
if app not in self.apps:
raise AppNotInstalledError(f"No app named {app}")
validate_app_installed_on_sites(app, bench_path=self.name)


@lru_cache(maxsize=None)
class Bench(Base, Validator):
def __init__(self, path):
self.name = path
self.cwd = os.path.abspath(path)
self.exists = is_bench_directory(self.name)

self.setup = BenchSetup(self)
self.teardown = BenchTearDown(self)
self.apps = BenchApps(self)

self.apps_txt = os.path.join(self.name, "sites", "apps.txt")
self.excluded_apps_txt = os.path.join(self.name, "sites", "excluded_apps.txt")

@property
def python(self) -> str:
return get_env_cmd("python", bench_path=self.name)

@property
def shallow_clone(self) -> bool:
config = self.conf

if config:
if config.get("release_bench") or not config.get("shallow_clone"):
return False

return get_git_version() > 1.9

@property
def excluded_apps(self) -> List:
try:
with open(self.excluded_apps_txt) as f:
return f.read().strip().split("\n")
except Exception:
return []

@property
def sites(self) -> List:
return [
path
for path in os.listdir(os.path.join(self.name, "sites"))
if os.path.exists(os.path.join("sites", path, "site_config.json"))
]

@property
def conf(self):
from bench.config.common_site_config import get_config

return get_config(self.name)

def init(self):
self.setup.dirs()
self.setup.env()
self.setup.backups()

def drop(self):
self.teardown.backups()
self.teardown.dirs()

def install(self, app, branch=None):
from bench.app import App

app = App(app, branch=branch)
self.apps.append(app)
self.apps.sync()

def uninstall(self, app, no_backup=False, force=False):
from bench.app import App

if not force:
self.validate_app_uninstall(app)
try:
self.apps.remove(App(app, bench=self, to_clone=False), no_backup=no_backup)
except InvalidRemoteException:
if not force:
raise

self.apps.sync()
# self.build() - removed because it seems unnecessary
self.reload(_raise=False)

@step(title="Building Bench Assets", success="Bench Assets Built")
def build(self):
# build assets & stuff
run_xhiveframework_cmd("build", bench_path=self.name)

@step(title="Reloading Bench Processes", success="Bench Processes Reloaded")
def reload(self, web=False, supervisor=True, systemd=True, _raise=True):
"""If web is True, only web workers are restarted"""
conf = self.conf

if conf.get("developer_mode"):
restart_process_manager(bench_path=self.name, web_workers=web)
if supervisor or conf.get("restart_supervisor_on_update"):
restart_supervisor_processes(bench_path=self.name, web_workers=web, _raise=_raise)
if systemd and conf.get("restart_systemd_on_update"):
restart_systemd_processes(bench_path=self.name, web_workers=web, _raise=_raise)

def get_installed_apps(self) -> List:
"""Returns list of installed apps on bench, not in excluded_apps.txt"""
try:
installed_packages = get_cmd_output(f"{self.python} -m pip freeze", cwd=self.name)
except Exception:
installed_packages = []

return [
app
for app in self.apps
if app not in self.excluded_apps and app in installed_packages
]


class BenchApps(MutableSequence):
def __init__(self, bench: Bench):
self.bench = bench
self.states_path = os.path.join(self.bench.name, "sites", "apps.json")
self.apps_path = os.path.join(self.bench.name, "apps")
self.initialize_apps()
self.set_states()

def set_states(self):
try:
with open(self.states_path) as f:
self.states = json.loads(f.read() or "{}")
except FileNotFoundError:
self.states = {}

def update_apps_states(
self,
app_dir: str = None,
app_name: Union[str, None] = None,
branch: Union[str, None] = None,
required: List = UNSET_ARG,
):
if required == UNSET_ARG:
required = []
if self.apps and not os.path.exists(self.states_path):
# idx according to apps listed in apps.txt (backwards compatibility)
# Keeping xhiveframework as the first app.
if "xhiveframework" in self.apps:
self.apps.remove("xhiveframework")
self.apps.insert(0, "xhiveframework")
with open(self.bench.apps_txt, "w") as f:
f.write("\n".join(self.apps))

print("Found existing apps updating states...")
for idx, app in enumerate(self.apps, start=1):
self.states[app] = {
"resolution": {"commit_hash": None, "branch": None},
"required": required,
"idx": idx,
"version": get_current_version(app, self.bench.name),
}

apps_to_remove = []
for app in self.states:
if app not in self.apps:
apps_to_remove.append(app)

for app in apps_to_remove:
del self.states[app]

if app_name and not app_dir:
app_dir = app_name

if app_name and app_name not in self.states:
version = get_current_version(app_name, self.bench.name)

app_dir = os.path.join(self.apps_path, app_dir)
is_repo = is_git_repo(app_dir)
if is_repo:
if not branch:
branch = (
subprocess.check_output(
"git rev-parse --abbrev-ref HEAD", shell=True, cwd=app_dir
)
.decode("utf-8")
.rstrip()
)

commit_hash = (
subprocess.check_output(f"git rev-parse {branch}", shell=True, cwd=app_dir)
.decode("utf-8")
.rstrip()
)

self.states[app_name] = {
"is_repo": is_repo,
"resolution": "not a repo"
if not is_repo
else {"commit_hash": commit_hash, "branch": branch},
"required": required,
"idx": len(self.states) + 1,
"version": version,
}

with open(self.states_path, "w") as f:
f.write(json.dumps(self.states, indent=4))

def sync(
self,
app_name: Union[str, None] = None,
app_dir: Union[str, None] = None,
branch: Union[str, None] = None,
required: List = UNSET_ARG,
):
if required == UNSET_ARG:
required = []
self.initialize_apps()

with open(self.bench.apps_txt, "w") as f:
f.write("\n".join(self.apps))

self.update_apps_states(
app_name=app_name, app_dir=app_dir, branch=branch, required=required
)

def initialize_apps(self):
try:
self.apps = [
x
for x in os.listdir(os.path.join(self.bench.name, "apps"))
if is_xhiveframework_app(os.path.join(self.bench.name, "apps", x))
]
self.apps.remove("xhiveframework")
self.apps.insert(0, "xhiveframework")
except FileNotFoundError:
self.apps = []

def __getitem__(self, key):
"""retrieves an item by its index, key"""
return self.apps[key]

def __setitem__(self, key, value):
"""set the item at index, key, to value"""
# should probably not be allowed
# self.apps[key] = value
raise NotImplementedError

def __delitem__(self, key):
"""removes the item at index, key"""
# TODO: uninstall and delete app from bench
del self.apps[key]

def __len__(self):
return len(self.apps)

def insert(self, key, value):
"""add an item, value, at index, key."""
# TODO: fetch and install app to bench
self.apps.insert(key, value)

def add(self, app: "App"):
app.get()
app.install()
super().append(app.app_name)
self.apps.sort()

def remove(self, app: "App", no_backup: bool = False):
app.uninstall()
app.remove(no_backup=no_backup)
super().remove(app.app_name)

def append(self, app: "App"):
return self.add(app)

def __repr__(self):
return self.__str__()

def __str__(self):
return str([x for x in self.apps])


class BenchSetup(Base):
def __init__(self, bench: Bench):
self.bench = bench
self.cwd = self.bench.cwd

@step(title="Setting Up Directories", success="Directories Set Up")
def dirs(self):
os.makedirs(self.bench.name, exist_ok=True)

for dirname in paths_in_bench:
os.makedirs(os.path.join(self.bench.name, dirname), exist_ok=True)

@step(title="Setting Up Environment", success="Environment Set Up")
def env(self, python="python3"):
"""Setup env folder
- create env if not exists
- upgrade env pip
- install xhiveframework python dependencies
"""
import bench.cli
import click

verbose = bench.cli.verbose

click.secho("Setting Up Environment", fg="yellow")

xhiveframework = os.path.join(self.bench.name, "apps", "xhiveframework")
quiet_flag = "" if verbose else "--quiet"

if not os.path.exists(self.bench.python):
venv = get_venv_path(verbose=verbose, python=python)
self.run(f"{venv} env", cwd=self.bench.name)

self.pip()
self.wheel()

if os.path.exists(xhiveframework):
self.run(
f"{self.bench.python} -m pip install {quiet_flag} --upgrade -e {xhiveframework}",
cwd=self.bench.name,
)

@step(title="Setting Up Bench Config", success="Bench Config Set Up")
def config(self, redis=True, procfile=True, additional_config=None):
"""Setup config folder
- create pids folder
- generate sites/common_site_config.json
"""
setup_config(self.bench.name, additional_config=additional_config)

if redis:
from bench.config.redis import generate_config

generate_config(self.bench.name)

if procfile:
from bench.config.procfile import setup_procfile

setup_procfile(self.bench.name, skip_redis=not redis)

@step(title="Updating pip", success="Updated pip")
def pip(self, verbose=False):
"""Updates env pip; assumes that env is setup"""
import bench.cli

verbose = bench.cli.verbose or verbose
quiet_flag = "" if verbose else "--quiet"

return self.run(
f"{self.bench.python} -m pip install {quiet_flag} --upgrade pip", cwd=self.bench.name
)

@step(title="Installing wheel", success="Installed wheel")
def wheel(self, verbose=False):
"""Wheel is required for building old setup.py packages.
ref: https://github.com/pypa/pip/issues/8559"""
import bench.cli

verbose = bench.cli.verbose or verbose
quiet_flag = "" if verbose else "--quiet"

return self.run(
f"{self.bench.python} -m pip install {quiet_flag} wheel", cwd=self.bench.name
)

def logging(self):
from bench.utils import setup_logging

return setup_logging(bench_path=self.bench.name)

@step(title="Setting Up Bench Patches", success="Bench Patches Set Up")
def patches(self):
shutil.copy(
os.path.join(os.path.dirname(os.path.abspath(__file__)), "patches", "patches.txt"),
os.path.join(self.bench.name, "patches.txt"),
)

@step(title="Setting Up Backups Cronjob", success="Backups Cronjob Set Up")
def backups(self):
# TODO: to something better for logging data? - maybe a wrapper that auto-logs with more context
logger.log("setting up backups")

from crontab import CronTab

bench_dir = os.path.abspath(self.bench.name)
user = self.bench.conf.get("xhiveframework_user")
logfile = os.path.join(bench_dir, "logs", "backup.log")
system_crontab = CronTab(user=user)
backup_command = f"cd {bench_dir} && {sys.argv[0]} --verbose --site all backup"
job_command = f"{backup_command} >> {logfile} 2>&1"

if job_command not in str(system_crontab):
job = system_crontab.new(
command=job_command, comment="bench auto backups set for every 6 hours"
)
job.every(6).hours()
system_crontab.write()

logger.log("backups were set up")

@job(title="Setting Up Bench Dependencies", success="Bench Dependencies Set Up")
def requirements(self, apps=None):
"""Install and upgrade specified / all installed apps on given Bench"""
from bench.app import App

apps = apps or self.bench.apps

self.pip()

print(f"Installing {len(apps)} applications...")

for app in apps:
path_to_app = os.path.join(self.bench.name, "apps", app)
app = App(path_to_app, bench=self.bench, to_clone=False).install(
skip_assets=True, restart_bench=False, ignore_resolution=True
)

def python(self, apps=None):
"""Install and upgrade Python dependencies for specified / all installed apps on given Bench"""
import bench.cli

apps = apps or self.bench.apps

quiet_flag = "" if bench.cli.verbose else "--quiet"

self.pip()

for app in apps:
app_path = os.path.join(self.bench.name, "apps", app)
log(f"\nInstalling python dependencies for {app}", level=3, no_log=True)
self.run(f"{self.bench.python} -m pip install {quiet_flag} --upgrade -e {app_path}")

def node(self, apps=None):
"""Install and upgrade Node dependencies for specified / all apps on given Bench"""
from bench.utils.bench import update_node_packages

return update_node_packages(bench_path=self.bench.name, apps=apps)


class BenchTearDown:
def __init__(self, bench):
self.bench = bench

def backups(self):
remove_backups_crontab(self.bench.name)

def dirs(self):
shutil.rmtree(self.bench.name)

+ 267
- 0
bench/cli.py Zobrazit soubor

@@ -0,0 +1,267 @@
# imports - standard imports
import atexit
from contextlib import contextmanager
from logging import Logger
import os
import pwd
import sys

# imports - third party imports
import click

# imports - module imports
import bench
from bench.bench import Bench
from bench.commands import bench_command
from bench.config.common_site_config import get_config
from bench.utils import (
check_latest_version,
drop_privileges,
find_parent_bench,
get_env_xhiveframework_commands,
get_cmd_output,
is_bench_directory,
is_dist_editable,
is_root,
log,
setup_logging,
get_cmd_from_sysargv,
)
from bench.utils.bench import get_env_cmd
from importlib.util import find_spec


# these variables are used to show dynamic outputs on the terminal
dynamic_feed = False
verbose = False
is_envvar_warn_set = None
from_command_line = False # set when commands are executed via the CLI
bench.LOG_BUFFER = []

change_uid_msg = "You should not run this command as root"
src = os.path.dirname(__file__)
SKIP_MODULE_TRACEBACK = ("click",)


@contextmanager
def execute_cmd(check_for_update=True, command: str = None, logger: Logger = None):
if check_for_update:
atexit.register(check_latest_version)

try:
yield
except BaseException as e:
return_code = getattr(e, "code", 1)

if isinstance(e, Exception):
click.secho(f"ERROR: {e}", fg="red")

if return_code:
logger.warning(f"{command} executed with exit code {return_code}")

raise e


def cli():
setup_clear_cache()
global from_command_line, bench_config, is_envvar_warn_set, verbose

from_command_line = True
command = " ".join(sys.argv)
argv = set(sys.argv)
is_envvar_warn_set = not (os.environ.get("BENCH_DEVELOPER") or os.environ.get("CI"))
is_cli_command = len(sys.argv) > 1 and not argv.intersection({"src", "--version"})
cmd_from_sys = get_cmd_from_sysargv()

if "--verbose" in argv:
verbose = True

change_working_directory()
logger = setup_logging()
logger.info(command)

bench_config = get_config(".")

if is_cli_command:
check_uid()
change_uid()
change_dir()

if (
is_envvar_warn_set
and is_cli_command
and not bench_config.get("developer_mode")
and is_dist_editable(bench.PROJECT_NAME)
):
log(
"bench is installed in editable mode!\n\nThis is not the recommended mode"
" of installation for production. Instead, install the package from PyPI"
" with: `pip install xhiveframework-bench`\n",
level=3,
)

in_bench = is_bench_directory()

if (
not in_bench
and len(sys.argv) > 1
and not argv.intersection(
{"init", "find", "src", "drop", "get", "get-app", "--version"}
)
and not cmd_requires_root()
):
log("Command not being executed in bench directory", level=3)

if len(sys.argv) == 1 or sys.argv[1] == "--help":
print(click.Context(bench_command).get_help())
if in_bench:
print(get_xhiveframework_help())
return

_opts = [x.opts + x.secondary_opts for x in bench_command.params]
opts = {item for sublist in _opts for item in sublist}

setup_exception_handler()

# handle usages like `--use-feature='feat-x'` and `--use-feature 'feat-x'`
if cmd_from_sys and cmd_from_sys.split("=", 1)[0].strip() in opts:
bench_command()

if cmd_from_sys in bench_command.commands:
with execute_cmd(check_for_update=is_cli_command, command=command, logger=logger):
bench_command()

if in_bench:
xhiveframework_cmd()

bench_command()


def check_uid():
if cmd_requires_root() and not is_root():
log("superuser privileges required for this command", level=3)
sys.exit(1)


def cmd_requires_root():
if len(sys.argv) > 2 and sys.argv[2] in (
"production",
"sudoers",
"lets-encrypt",
"fonts",
"print",
"firewall",
"ssh-port",
"role",
"fail2ban",
"wildcard-ssl",
):
return True
if len(sys.argv) >= 2 and sys.argv[1] in (
"patch",
"renew-lets-encrypt",
"disable-production",
):
return True
if len(sys.argv) > 2 and sys.argv[1] in ("install"):
return True


def change_dir():
if os.path.exists("config.json") or "init" in sys.argv:
return
dir_path_file = "/etc/xhiveframework_bench_dir"
if os.path.exists(dir_path_file):
with open(dir_path_file) as f:
dir_path = f.read().strip()
if os.path.exists(dir_path):
os.chdir(dir_path)


def change_uid():
if is_root() and not cmd_requires_root():
xhiveframework_user = bench_config.get("xhiveframework_user")
if xhiveframework_user:
drop_privileges(uid_name=xhiveframework_user, gid_name=xhiveframework_user)
os.environ["HOME"] = pwd.getpwnam(xhiveframework_user).pw_dir
else:
log(change_uid_msg, level=3)
sys.exit(1)


def app_cmd(bench_path="."):
f = get_env_cmd("python", bench_path=bench_path)
os.chdir(os.path.join(bench_path, "sites"))
os.execv(f, [f] + ["-m", "xhiveframework.utils.bench_helper"] + sys.argv[1:])


def xhiveframework_cmd(bench_path="."):
f = get_env_cmd("python", bench_path=bench_path)
os.chdir(os.path.join(bench_path, "sites"))
os.execv(f, [f] + ["-m", "xhiveframework.utils.bench_helper", "xhiveframework"] + sys.argv[1:])


def get_xhiveframework_commands():
if not is_bench_directory():
return set()

return set(get_env_xhiveframework_commands())


def get_xhiveframework_help(bench_path="."):
python = get_env_cmd("python", bench_path=bench_path)
sites_path = os.path.join(bench_path, "sites")
try:
out = get_cmd_output(
f"{python} -m xhiveframework.utils.bench_helper get-xhiveframework-help", cwd=sites_path
)
return "\n\nFramework commands:\n" + out.split("Commands:")[1]
except Exception:
return ""


def change_working_directory():
"""Allows bench commands to be run from anywhere inside a bench directory"""
cur_dir = os.path.abspath(".")
bench_path = find_parent_bench(cur_dir)
bench.current_path = os.getcwd()
bench.updated_path = bench_path

if bench_path:
os.chdir(bench_path)


def setup_clear_cache():
from copy import copy

f = copy(os.chdir)

def _chdir(*args, **kwargs):
Bench.cache_clear()
get_env_cmd.cache_clear()
return f(*args, **kwargs)

os.chdir = _chdir


def setup_exception_handler():
from traceback import format_exception
from bench.exceptions import CommandFailedError

def handle_exception(exc_type, exc_info, tb):
if exc_type == CommandFailedError:
print("".join(generate_exc(exc_type, exc_info, tb)))
else:
sys.__excepthook__(exc_type, exc_info, tb)

def generate_exc(exc_type, exc_info, tb):
TB_SKIP = [
os.path.dirname(find_spec(module).origin) for module in SKIP_MODULE_TRACEBACK
]

for tb_line in format_exception(exc_type, exc_info, tb):
for skip_module in TB_SKIP:
if skip_module not in tb_line:
yield tb_line

sys.excepthook = handle_exception

+ 133
- 0
bench/commands/__init__.py Zobrazit soubor

@@ -0,0 +1,133 @@
# imports - third party imports
import click

# imports - module imports
from bench.utils.cli import (
MultiCommandGroup,
print_bench_version,
use_experimental_feature,
setup_verbosity,
)


@click.group(cls=MultiCommandGroup)
@click.option(
"--version",
is_flag=True,
is_eager=True,
callback=print_bench_version,
expose_value=False,
)
@click.option(
"--use-feature",
is_eager=True,
callback=use_experimental_feature,
expose_value=False,
)
@click.option(
"-v",
"--verbose",
is_flag=True,
callback=setup_verbosity,
expose_value=False,
)
def bench_command(bench_path="."):
import bench

bench.set_xhiveframework_version(bench_path=bench_path)


from bench.commands.make import (
drop,
exclude_app_for_update,
get_app,
include_app_for_update,
init,
new_app,
pip,
remove_app,
validate_dependencies,
)

bench_command.add_command(init)
bench_command.add_command(drop)
bench_command.add_command(get_app)
bench_command.add_command(new_app)
bench_command.add_command(remove_app)
bench_command.add_command(exclude_app_for_update)
bench_command.add_command(include_app_for_update)
bench_command.add_command(pip)
bench_command.add_command(validate_dependencies)


from bench.commands.update import (
retry_upgrade,
switch_to_branch,
switch_to_develop,
update,
)

bench_command.add_command(update)
bench_command.add_command(retry_upgrade)
bench_command.add_command(switch_to_branch)
bench_command.add_command(switch_to_develop)


from bench.commands.utils import (
app_cache_helper,
backup_all_sites,
bench_src,
disable_production,
download_translations,
find_benches,
migrate_env,
renew_lets_encrypt,
restart,
set_mariadb_host,
set_nginx_port,
set_redis_cache_host,
set_redis_queue_host,
set_redis_socketio_host,
set_ssl_certificate,
set_ssl_certificate_key,
set_url_root,
start,
)

bench_command.add_command(start)
bench_command.add_command(restart)
bench_command.add_command(set_nginx_port)
bench_command.add_command(set_ssl_certificate)
bench_command.add_command(set_ssl_certificate_key)
bench_command.add_command(set_url_root)
bench_command.add_command(set_mariadb_host)
bench_command.add_command(set_redis_cache_host)
bench_command.add_command(set_redis_queue_host)
bench_command.add_command(set_redis_socketio_host)
bench_command.add_command(download_translations)
bench_command.add_command(backup_all_sites)
bench_command.add_command(renew_lets_encrypt)
bench_command.add_command(disable_production)
bench_command.add_command(bench_src)
bench_command.add_command(find_benches)
bench_command.add_command(migrate_env)
bench_command.add_command(app_cache_helper)

from bench.commands.setup import setup

bench_command.add_command(setup)


from bench.commands.config import config

bench_command.add_command(config)

from bench.commands.git import remote_reset_url, remote_set_url, remote_urls

bench_command.add_command(remote_set_url)
bench_command.add_command(remote_reset_url)
bench_command.add_command(remote_urls)

from bench.commands.install import install

bench_command.add_command(install)

+ 99
- 0
bench/commands/config.py Zobrazit soubor

@@ -0,0 +1,99 @@
# imports - module imports
from bench.config.common_site_config import update_config, put_config

# imports - third party imports
import click


@click.group(help="Change bench configuration")
def config():
pass


@click.command(
"restart_supervisor_on_update",
help="Enable/Disable auto restart of supervisor processes",
)
@click.argument("state", type=click.Choice(["on", "off"]))
def config_restart_supervisor_on_update(state):
update_config({"restart_supervisor_on_update": state == "on"})


@click.command(
"restart_systemd_on_update", help="Enable/Disable auto restart of systemd units"
)
@click.argument("state", type=click.Choice(["on", "off"]))
def config_restart_systemd_on_update(state):
update_config({"restart_systemd_on_update": state == "on"})


@click.command(
"dns_multitenant", help="Enable/Disable bench multitenancy on running bench update"
)
@click.argument("state", type=click.Choice(["on", "off"]))
def config_dns_multitenant(state):
update_config({"dns_multitenant": state == "on"})


@click.command(
"serve_default_site", help="Configure nginx to serve the default site on port 80"
)
@click.argument("state", type=click.Choice(["on", "off"]))
def config_serve_default_site(state):
update_config({"serve_default_site": state == "on"})


@click.command("rebase_on_pull", help="Rebase repositories on pulling")
@click.argument("state", type=click.Choice(["on", "off"]))
def config_rebase_on_pull(state):
update_config({"rebase_on_pull": state == "on"})


@click.command("http_timeout", help="Set HTTP timeout")
@click.argument("seconds", type=int)
def config_http_timeout(seconds):
update_config({"http_timeout": seconds})


@click.command("set-common-config", help="Set value in common config")
@click.option("configs", "-c", "--config", multiple=True, type=(str, str))
def set_common_config(configs):
import ast

common_site_config = {}
for key, value in configs:
if value in ("true", "false"):
value = value.title()
try:
value = ast.literal_eval(value)
except ValueError:
pass

common_site_config[key] = value

update_config(common_site_config, bench_path=".")


@click.command(
"remove-common-config", help="Remove specific keys from current bench's common config"
)
@click.argument("keys", nargs=-1)
def remove_common_config(keys):
from bench.bench import Bench

common_site_config = Bench(".").conf
for key in keys:
if key in common_site_config:
del common_site_config[key]

put_config(common_site_config)


config.add_command(config_restart_supervisor_on_update)
config.add_command(config_restart_systemd_on_update)
config.add_command(config_dns_multitenant)
config.add_command(config_rebase_on_pull)
config.add_command(config_serve_default_site)
config.add_command(config_http_timeout)
config.add_command(set_common_config)
config.add_command(remove_common_config)

+ 37
- 0
bench/commands/git.py Zobrazit soubor

@@ -0,0 +1,37 @@
# imports - standard imports
import os
import subprocess

# imports - module imports
from bench.bench import Bench
from bench.app import get_repo_dir
from bench.utils import set_git_remote_url
from bench.utils.app import get_remote

# imports - third party imports
import click


@click.command('remote-set-url', help="Set app remote url")
@click.argument('git-url')
def remote_set_url(git_url):
set_git_remote_url(git_url)


@click.command('remote-reset-url', help="Reset app remote url to xhiveframework official")
@click.argument('app')
def remote_reset_url(app):
git_url = f"https://lab.membtech.com/xhiveframework/{app}.git"
set_git_remote_url(git_url)


@click.command('remote-urls', help="Show apps remote url")
def remote_urls():
for app in Bench(".").apps:
repo_dir = get_repo_dir(app)

if os.path.exists(os.path.join(repo_dir, '.git')):
remote = get_remote(app)
remote_url = subprocess.check_output(['git', 'config', '--get', f'remote.{remote}.url'], cwd=repo_dir).strip()
print(f"{app}\t{remote_url}")


+ 123
- 0
bench/commands/install.py Zobrazit soubor

@@ -0,0 +1,123 @@
# imports - module imports
from bench.utils import run_playbook
from bench.utils.system import setup_sudoers

# imports - third party imports
import click


extra_vars = {"production": True}


@click.group(help="Install system dependencies for setting up Xhiveframework environment")
def install():
pass


@click.command(
"prerequisites",
help="Installs pre-requisite libraries, essential tools like b2zip, htop, screen, vim, x11-fonts, python libs, cups and Redis",
)
def install_prerequisites():
run_playbook("site.yml", tag="common, redis")


@click.command(
"mariadb", help="Install and setup MariaDB of specified version and root password"
)
@click.option("--mysql_root_password", "--mysql-root-password",
"--mariadb_root_password", "--mariadb-root-password", default="")
@click.option("--version", default="10.3")
def install_mariadb(mysql_root_password, version):
if mysql_root_password:
extra_vars.update(
{
"mysql_root_password": mysql_root_password,
}
)

extra_vars.update({"mariadb_version": version})

run_playbook("site.yml", extra_vars=extra_vars, tag="mariadb")


@click.command("wkhtmltopdf", help="Installs wkhtmltopdf v0.12.3 for linux")
def install_wkhtmltopdf():
run_playbook("site.yml", extra_vars=extra_vars, tag="wkhtmltopdf")


@click.command("nodejs", help="Installs Node.js v8")
def install_nodejs():
run_playbook("site.yml", extra_vars=extra_vars, tag="nodejs")


@click.command("psutil", help="Installs psutil via pip")
def install_psutil():
run_playbook("site.yml", extra_vars=extra_vars, tag="psutil")


@click.command(
"supervisor",
help="Installs supervisor. If user is specified, sudoers is setup for that user",
)
@click.option("--user")
def install_supervisor(user=None):
run_playbook("site.yml", extra_vars=extra_vars, tag="supervisor")
if user:
setup_sudoers(user)


@click.command(
"nginx", help="Installs NGINX. If user is specified, sudoers is setup for that user"
)
@click.option("--user")
def install_nginx(user=None):
run_playbook("site.yml", extra_vars=extra_vars, tag="nginx")
if user:
setup_sudoers(user)


@click.command("virtualbox", help="Installs virtualbox")
def install_virtualbox():
run_playbook("vm_build.yml", tag="virtualbox")


@click.command("packer", help="Installs Oracle virtualbox and packer 1.2.1")
def install_packer():
run_playbook("vm_build.yml", tag="packer")


@click.command(
"fail2ban",
help="Install fail2ban, an intrusion prevention software framework that protects computer servers from brute-force attacks",
)
@click.option(
"--maxretry",
default=6,
help="Number of matches (i.e. value of the counter) which triggers ban action on the IP.",
)
@click.option(
"--bantime",
default=600,
help="The counter is set to zero if no match is found within 'findtime' seconds.",
)
@click.option(
"--findtime",
default=600,
help='Duration (in seconds) for IP to be banned for. Negative number for "permanent" ban.',
)
def install_failtoban(**kwargs):
extra_vars.update(kwargs)
run_playbook("site.yml", extra_vars=extra_vars, tag="fail2ban")


install.add_command(install_prerequisites)
install.add_command(install_mariadb)
install.add_command(install_wkhtmltopdf)
install.add_command(install_nodejs)
install.add_command(install_psutil)
install.add_command(install_supervisor)
install.add_command(install_nginx)
install.add_command(install_failtoban)
install.add_command(install_virtualbox)
install.add_command(install_packer)

+ 272
- 0
bench/commands/make.py Zobrazit soubor

@@ -0,0 +1,272 @@
# imports - third party imports
import click


@click.command("init", help="Initialize a new bench instance in the specified path")
@click.argument("path")
@click.option(
"--version",
"--xhiveframework-branch",
"xhiveframework_branch",
default=None,
help="Clone a particular branch of xhiveframework",
)
@click.option(
"--ignore-exist", is_flag=True, default=False, help="Ignore if Bench instance exists."
)
@click.option(
"--python", type=str, default="python3", help="Path to Python Executable."
)
@click.option(
"--apps_path", default=None, help="path to json files with apps to install after init"
)
@click.option("--xhiveframework-path", default=None, help="path to xhiveframework repo")
@click.option("--clone-from", default=None, help="copy repos from path")
@click.option(
"--clone-without-update", is_flag=True, help="copy repos from path without update"
)
@click.option("--no-procfile", is_flag=True, help="Do not create a Procfile")
@click.option(
"--no-backups",
is_flag=True,
help="Do not set up automatic periodic backups for all sites on this bench",
)
@click.option(
"--skip-redis-config-generation",
is_flag=True,
help="Skip redis config generation if already specifying the common-site-config file",
)
@click.option("--skip-assets", is_flag=True, default=False, help="Do not build assets")
@click.option("--install-app", help="Install particular app after initialization")
@click.option("--verbose", is_flag=True, help="Verbose output during install")
@click.option(
"--dev",
is_flag=True,
default=False,
help="Enable developer mode and install development dependencies.",
)
def init(
path,
apps_path,
xhiveframework_path,
xhiveframework_branch,
no_procfile,
no_backups,
clone_from,
verbose,
skip_redis_config_generation,
clone_without_update,
ignore_exist=False,
skip_assets=False,
python="python3",
install_app=None,
dev=False,
):
import os

from bench.utils import log
from bench.utils.system import init

if not ignore_exist and os.path.exists(path):
log(f"Bench instance already exists at {path}", level=2)
return

try:
init(
path,
apps_path=apps_path, # can be used from --config flag? Maybe config file could have more info?
no_procfile=no_procfile,
no_backups=no_backups,
xhiveframework_path=xhiveframework_path,
xhiveframework_branch=xhiveframework_branch,
install_app=install_app,
clone_from=clone_from,
skip_redis_config_generation=skip_redis_config_generation,
clone_without_update=clone_without_update,
skip_assets=skip_assets,
python=python,
verbose=verbose,
dev=dev,
)
log(f"Bench {path} initialized", level=1)
except SystemExit:
raise
except Exception:
import shutil
import time

from bench.utils import get_traceback

# add a sleep here so that the traceback of other processes doesnt overlap with the prompts
time.sleep(1)
print(get_traceback())

log(f"There was a problem while creating {path}", level=2)
if click.confirm("Do you want to rollback these changes?", abort=True):
log(f'Rolling back Bench "{path}"')
if os.path.exists(path):
shutil.rmtree(path)


@click.command("drop")
@click.argument("path")
def drop(path):
from bench.bench import Bench
from bench.exceptions import BenchNotFoundError, ValidationError

bench = Bench(path)

if not bench.exists:
raise BenchNotFoundError(f"Bench {bench.name} does not exist")

if bench.sites:
raise ValidationError("Cannot remove non-empty bench directory")

bench.drop()

print("Bench dropped")


@click.command(
["get", "get-app"],
help="Clone an app from the internet or filesystem and set it up in your bench",
)
@click.argument("name", nargs=-1) # Dummy argument for backward compatibility
@click.argument("git-url")
@click.option("--branch", default=None, help="branch to checkout")
@click.option("--overwrite", is_flag=True, default=False)
@click.option("--skip-assets", is_flag=True, default=False, help="Do not build assets")
@click.option(
"--soft-link",
is_flag=True,
default=False,
help="Create a soft link to git repo instead of clone.",
)
@click.option(
"--init-bench", is_flag=True, default=False, help="Initialize Bench if not in one"
)
@click.option(
"--resolve-deps",
is_flag=True,
default=False,
help="Resolve dependencies before installing app",
)
@click.option(
"--cache-key",
type=str,
default=None,
help="Caches get-app artifacts if provided (only first 10 chars is used)",
)
@click.option(
"--compress-artifacts",
is_flag=True,
default=False,
help="Whether to gzip get-app artifacts that are to be cached",
)
def get_app(
git_url,
branch,
name=None,
overwrite=False,
skip_assets=False,
soft_link=False,
init_bench=False,
resolve_deps=False,
cache_key=None,
compress_artifacts=False,
):
"clone an app from the internet and set it up in your bench"
from bench.app import get_app

get_app(
git_url,
branch=branch,
skip_assets=skip_assets,
overwrite=overwrite,
soft_link=soft_link,
init_bench=init_bench,
resolve_deps=resolve_deps,
cache_key=cache_key,
compress_artifacts=compress_artifacts,
)


@click.command("new-app", help="Create a new Xhiveframework application under apps folder")
@click.option(
"--no-git",
is_flag=True,
flag_value="--no-git",
help="Do not initialize git repository for the app (available in Xhiveframework v14+)",
)
@click.argument("app-name")
def new_app(app_name, no_git=None):
from bench.app import new_app

new_app(app_name, no_git)


@click.command(
["remove", "rm", "remove-app"],
help=(
"Completely remove app from bench and re-build assets if not installed on any site"
),
)
@click.option("--no-backup", is_flag=True, help="Do not backup app before removing")
@click.option("--force", is_flag=True, help="Force remove app")
@click.argument("app-name")
def remove_app(app_name, no_backup=False, force=False):
from bench.bench import Bench

bench = Bench(".")
bench.uninstall(app_name, no_backup=no_backup, force=force)


@click.command("exclude-app", help="Exclude app from updating")
@click.argument("app_name")
def exclude_app_for_update(app_name):
from bench.app import add_to_excluded_apps_txt

add_to_excluded_apps_txt(app_name)


@click.command("include-app", help="Include app for updating")
@click.argument("app_name")
def include_app_for_update(app_name):
"Include app from updating"
from bench.app import remove_from_excluded_apps_txt

remove_from_excluded_apps_txt(app_name)


@click.command(
"pip",
context_settings={"ignore_unknown_options": True, "help_option_names": []},
help="For pip help use `bench pip help [COMMAND]` or `bench pip [COMMAND] -h`",
)
@click.argument("args", nargs=-1)
@click.pass_context
def pip(ctx, args):
"Run pip commands in bench env"
import os

from bench.utils.bench import get_env_cmd

env_py = get_env_cmd("python")
os.execv(env_py, (env_py, "-m", "pip") + args)


@click.command(
"validate-dependencies",
help="Validates that all requirements specified in xhiveframework-dependencies are met curently.",
)
@click.pass_context
def validate_dependencies(ctx):
"Validate all specified xhiveframework-dependencies."
from bench.bench import Bench
from bench.app import App

bench = Bench(".")

for app_name in bench.apps:
app = App(app_name, bench=bench)
app.validate_app_dependencies(throw=True)

+ 447
- 0
bench/commands/setup.py Zobrazit soubor

@@ -0,0 +1,447 @@
# imports - standard imports
import os
import sys

# imports - third party imports
import click

# imports - module imports
from bench.utils import exec_cmd, run_playbook, which
from bench.utils.cli import SugaredOption


@click.group(help="Setup command group for enabling setting up a Xhiveframework environment")
def setup():
pass


@click.command(
"sudoers", help="Add commands to sudoers list for execution without password"
)
@click.argument("user")
def setup_sudoers(user):
from bench.utils.system import setup_sudoers

setup_sudoers(user)


@click.command("nginx", help="Generate configuration files for NGINX")
@click.option(
"--logging", default="combined", type=click.Choice(["none", "site", "combined"])
)
@click.option(
"--log_format",
help="Specify the log_format for nginx. Use none or '' to not set a value.",
only_if_set=["logging"],
cls=SugaredOption,
default="main",
)
@click.option(
"--yes", help="Yes to regeneration of nginx config file", default=False, is_flag=True
)
def setup_nginx(yes=False, logging="combined", log_format=None):
from bench.config.nginx import make_nginx_conf

make_nginx_conf(bench_path=".", yes=yes, logging=logging, log_format=log_format)


@click.command("reload-nginx", help="Checks NGINX config file and reloads service")
def reload_nginx():
from bench.config.production_setup import reload_nginx

reload_nginx()


@click.command("supervisor", help="Generate configuration for supervisor")
@click.option("--user", help="optional user argument")
@click.option(
"--yes", help="Yes to regeneration of supervisor config", is_flag=True, default=False
)
@click.option(
"--skip-redis", help="Skip redis configuration", is_flag=True, default=False
)
@click.option(
"--skip-supervisord",
help="Skip supervisord configuration",
is_flag=True,
default=False,
)
def setup_supervisor(user=None, yes=False, skip_redis=False, skip_supervisord=False):
from bench.utils import get_cmd_output
from bench.config.supervisor import (
check_supervisord_config,
generate_supervisor_config,
)

if which("supervisorctl") is None:
click.secho("Please install `supervisor` to proceed", fg="red")
sys.exit(1)

if not skip_supervisord and "Permission denied" in get_cmd_output(
"supervisorctl status"
):
check_supervisord_config(user=user)

generate_supervisor_config(bench_path=".", user=user, yes=yes, skip_redis=skip_redis)


@click.command("redis", help="Generates configuration for Redis")
def setup_redis():
from bench.config.redis import generate_config

generate_config(".")


@click.command("fonts", help="Add Xhiveframework fonts to system")
def setup_fonts():
from bench.utils.system import setup_fonts

setup_fonts()


@click.command(
"production", help="Setup Xhiveframework production environment for specific user"
)
@click.argument("user")
@click.option("--yes", help="Yes to regeneration config", is_flag=True, default=False)
def setup_production(user, yes=False):
from bench.config.production_setup import setup_production

setup_production(user=user, yes=yes)


@click.command("backups", help="Add cronjob for bench backups")
def setup_backups():
from bench.bench import Bench

Bench(".").setup.backups()


@click.command("env", help="Setup Python environment for bench")
@click.option(
"--python", type=str, default="python3", help="Path to Python Executable."
)
def setup_env(python="python3"):
from bench.bench import Bench

return Bench(".").setup.env(python=python)


@click.command("firewall", help="Setup firewall for system")
@click.option("--ssh_port")
@click.option("--force")
def setup_firewall(ssh_port=None, force=False):
if not force:
click.confirm(
f"Setting up the firewall will block all ports except 80, 443 and {ssh_port}\nDo you want to continue?",
abort=True,
)

if not ssh_port:
ssh_port = 22

run_playbook("roles/bench/tasks/setup_firewall.yml", {"ssh_port": ssh_port})


@click.command("ssh-port", help="Set SSH Port for system")
@click.argument("port")
@click.option("--force")
def set_ssh_port(port, force=False):
if not force:
click.confirm(
f"This will change your SSH Port to {port}\nDo you want to continue?", abort=True
)

run_playbook("roles/bench/tasks/change_ssh_port.yml", {"ssh_port": port})


@click.command("lets-encrypt", help="Setup lets-encrypt SSL for site")
@click.argument("site")
@click.option("--custom-domain")
@click.option(
"-n",
"--non-interactive",
default=False,
is_flag=True,
help="Run command non-interactively. This flag restarts nginx and runs certbot non interactively. Shouldn't be used on 1'st attempt",
)
def setup_letsencrypt(site, custom_domain, non_interactive):
from bench.config.lets_encrypt import setup_letsencrypt

setup_letsencrypt(site, custom_domain, bench_path=".", interactive=not non_interactive)


@click.command(
"wildcard-ssl", help="Setup wildcard SSL certificate for multi-tenant bench"
)
@click.argument("domain")
@click.option("--email")
@click.option(
"--exclude-base-domain",
default=False,
is_flag=True,
help="SSL Certificate not applicable for base domain",
)
def setup_wildcard_ssl(domain, email, exclude_base_domain):
from bench.config.lets_encrypt import setup_wildcard_ssl

setup_wildcard_ssl(
domain, email, bench_path=".", exclude_base_domain=exclude_base_domain
)


@click.command("procfile", help="Generate Procfile for bench start")
def setup_procfile():
from bench.config.procfile import setup_procfile

setup_procfile(".")


@click.command(
"socketio", help="[DEPRECATED] Setup node dependencies for socketio server"
)
def setup_socketio():
return


@click.command("requirements")
@click.option("--node", help="Update only Node packages", default=False, is_flag=True)
@click.option(
"--python", help="Update only Python packages", default=False, is_flag=True
)
@click.option(
"--dev",
help="Install optional python development dependencies",
default=False,
is_flag=True,
)
@click.argument("apps", nargs=-1)
def setup_requirements(node=False, python=False, dev=False, apps=None):
"""
Setup Python and Node dependencies.

You can optionally specify one or more apps to setup dependencies for.
"""
from bench.bench import Bench

bench = Bench(".")

if not (node or python or dev):
bench.setup.requirements(apps=apps)

elif not node and not dev:
bench.setup.python(apps=apps)

elif not python and not dev:
bench.setup.node(apps=apps)

else:
from bench.utils.bench import install_python_dev_dependencies

install_python_dev_dependencies(apps=apps)

if node:
click.secho(
"--dev flag only supports python dependencies. All node development dependencies are installed by default.",
fg="yellow",
)


@click.command(
"manager",
help="Setup bench-manager.local site with the bench_manager app installed on it",
)
@click.option(
"--yes", help="Yes to regeneration of nginx config file", default=False, is_flag=True
)
@click.option(
"--port", help="Port on which you want to run bench manager", default=23624
)
@click.option("--domain", help="Domain on which you want to run bench manager")
def setup_manager(yes=False, port=23624, domain=None):
from bench.bench import Bench
from bench.config.nginx import make_bench_manager_nginx_conf

create_new_site = True

if "bench-manager.local" in os.listdir("sites"):
create_new_site = click.confirm("Site already exists. Overwrite existing site?")

if create_new_site:
exec_cmd("bench new-site --force bench-manager.local")

if "bench_manager" in os.listdir("apps"):
print("App already exists. Skipping app download.")
else:
exec_cmd("bench get-app bench_manager")

exec_cmd("bench --site bench-manager.local install-app bench_manager")

bench_path = "."
bench = Bench(bench_path)

if bench.conf.get("restart_supervisor_on_update") or bench.conf.get(
"restart_systemd_on_update"
):
# implicates a production setup or so I presume
if not domain:
print(
"Please specify the site name on which you want to host bench-manager using the 'domain' flag"
)
sys.exit(1)

if domain not in bench.sites:
raise Exception("No such site")

make_bench_manager_nginx_conf(bench_path, yes=yes, port=port, domain=domain)


@click.command("config", help="Generate or over-write sites/common_site_config.json")
def setup_config():
from bench.config.common_site_config import setup_config

setup_config(".")


@click.command("add-domain", help="Add a custom domain to a particular site")
@click.argument("domain")
@click.option("--site", prompt=True)
@click.option("--ssl-certificate", help="Absolute path to SSL Certificate")
@click.option("--ssl-certificate-key", help="Absolute path to SSL Certificate Key")
def add_domain(domain, site=None, ssl_certificate=None, ssl_certificate_key=None):
"""Add custom domain to site"""
if not site:
print("Please specify site")
sys.exit(1)

from bench.config.site_config import add_domain

add_domain(site, domain, ssl_certificate, ssl_certificate_key, bench_path=".")


@click.command("remove-domain", help="Remove custom domain from a site")
@click.argument("domain")
@click.option("--site", prompt=True)
def remove_domain(domain, site=None):
if not site:
print("Please specify site")
sys.exit(1)

from bench.config.site_config import remove_domain

remove_domain(site, domain, bench_path=".")


@click.command(
"sync-domains",
help="Check if there is a change in domains. If yes, updates the domains list.",
)
@click.option("--domain", multiple=True)
@click.option("--site", prompt=True)
def sync_domains(domain=None, site=None):
if not site:
print("Please specify site")
sys.exit(1)

try:
domains = list(map(str, domain))
except Exception:
print("Domains should be a json list of strings or dictionaries")
sys.exit(1)

from bench.config.site_config import sync_domains

changed = sync_domains(site, domains, bench_path=".")

# if changed, success, else failure
sys.exit(0 if changed else 1)


@click.command("role", help="Install dependencies via ansible roles")
@click.argument("role")
@click.option("--admin_emails", default="")
@click.option("--mysql_root_password", "--mariadb_root_password")
@click.option("--container", is_flag=True, default=False)
def setup_roles(role, **kwargs):
extra_vars = {"production": True}
extra_vars.update(kwargs)

if role:
run_playbook("site.yml", extra_vars=extra_vars, tag=role)
else:
run_playbook("site.yml", extra_vars=extra_vars)


@click.command(
"fail2ban",
help="Setup fail2ban, an intrusion prevention software framework that protects computer servers from brute-force attacks",
)
@click.option(
"--maxretry",
default=6,
help="Number of matches (i.e. value of the counter) which triggers ban action on the IP. Default is 6 seconds",
)
@click.option(
"--bantime",
default=600,
help="Duration (in seconds) for IP to be banned for. Negative number for 'permanent' ban. Default is 600 seconds",
)
@click.option(
"--findtime",
default=600,
help="The counter is set to zero if match found within 'findtime' seconds doesn't exceed 'maxretry'. Default is 600 seconds",
)
def setup_nginx_proxy_jail(**kwargs):
run_playbook("roles/fail2ban/tasks/configure_nginx_jail.yml", extra_vars=kwargs)


@click.command("systemd", help="Generate configuration for systemd")
@click.option("--user", help="Optional user argument")
@click.option(
"--yes",
help="Yes to regeneration of systemd config files",
is_flag=True,
default=False,
)
@click.option("--stop", help="Stop bench services", is_flag=True, default=False)
@click.option("--create-symlinks", help="Create Symlinks", is_flag=True, default=False)
@click.option("--delete-symlinks", help="Delete Symlinks", is_flag=True, default=False)
def setup_systemd(
user=None, yes=False, stop=False, create_symlinks=False, delete_symlinks=False
):
from bench.config.systemd import generate_systemd_config

generate_systemd_config(
bench_path=".",
user=user,
yes=yes,
stop=stop,
create_symlinks=create_symlinks,
delete_symlinks=delete_symlinks,
)


setup.add_command(setup_sudoers)
setup.add_command(setup_nginx)
setup.add_command(reload_nginx)
setup.add_command(setup_supervisor)
setup.add_command(setup_redis)
setup.add_command(setup_letsencrypt)
setup.add_command(setup_wildcard_ssl)
setup.add_command(setup_production)
setup.add_command(setup_backups)
setup.add_command(setup_env)
setup.add_command(setup_procfile)
setup.add_command(setup_socketio)
setup.add_command(setup_requirements)
setup.add_command(setup_manager)
setup.add_command(setup_config)
setup.add_command(setup_fonts)
setup.add_command(add_domain)
setup.add_command(remove_domain)
setup.add_command(sync_domains)
setup.add_command(setup_firewall)
setup.add_command(set_ssh_port)
setup.add_command(setup_roles)
setup.add_command(setup_nginx_proxy_jail)
setup.add_command(setup_systemd)

+ 101
- 0
bench/commands/update.py Zobrazit soubor

@@ -0,0 +1,101 @@
# imports - third party imports
import click

# imports - module imports
from bench.app import pull_apps
from bench.utils.bench import post_upgrade, patch_sites, build_assets


@click.command(
"update",
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",
)
@click.option("--pull", is_flag=True, help="Pull updates for all the apps in bench")
@click.option("--apps", type=str)
@click.option("--patch", is_flag=True, help="Run migrations for all sites in the bench")
@click.option("--build", is_flag=True, help="Build JS and CSS assets for the bench")
@click.option(
"--requirements",
is_flag=True,
help="Update requirements. If run alone, equivalent to `bench setup requirements`",
)
@click.option(
"--restart-supervisor", is_flag=True, help="Restart supervisor processes after update"
)
@click.option(
"--restart-systemd", is_flag=True, help="Restart systemd units after update"
)
@click.option(
"--no-backup",
is_flag=True,
help="If this flag is set, sites won't be backed up prior to updates. Note: This is not recommended in production.",
)
@click.option(
"--no-compile",
is_flag=True,
help="[DEPRECATED] This flag doesn't do anything now.",
)
@click.option("--force", is_flag=True, help="Forces major version upgrades")
@click.option(
"--reset",
is_flag=True,
help="Hard resets git branch's to their new states overriding any changes and overriding rebase on pull",
)
def update(
pull,
apps,
patch,
build,
requirements,
restart_supervisor,
restart_systemd,
no_backup,
no_compile,
force,
reset,
):
from bench.utils.bench import update

update(
pull=pull,
apps=apps,
patch=patch,
build=build,
requirements=requirements,
restart_supervisor=restart_supervisor,
restart_systemd=restart_systemd,
backup=not no_backup,
compile=not no_compile,
force=force,
reset=reset,
)


@click.command("retry-upgrade", help="Retry a failed upgrade")
@click.option("--version", default=5)
def retry_upgrade(version):
pull_apps()
patch_sites()
build_assets()
post_upgrade(version - 1, version)


@click.command(
"switch-to-branch",
help="Switch all apps to specified branch, or specify apps separated by space",
)
@click.argument("branch")
@click.argument("apps", nargs=-1)
@click.option("--upgrade", is_flag=True)
def switch_to_branch(branch, apps, upgrade=False):
from bench.utils.app import switch_to_branch

switch_to_branch(branch=branch, apps=list(apps), upgrade=upgrade)


@click.command("switch-to-develop")
def switch_to_develop(upgrade=False):
"Switch xhiveframework and xhiveerp to develop branch"
from bench.utils.app import switch_to_develop

switch_to_develop(apps=["xhiveframework", "xhiveerp"])

+ 196
- 0
bench/commands/utils.py Zobrazit soubor

@@ -0,0 +1,196 @@
# imports - standard imports
import os

# imports - third party imports
import click


@click.command("start", help="Start Xhiveframework development processes")
@click.option("--no-dev", is_flag=True, default=False)
@click.option(
"--no-prefix",
is_flag=True,
default=False,
help="Hide process name from bench start log",
)
@click.option("--concurrency", "-c", type=str)
@click.option("--procfile", "-p", type=str)
@click.option("--man", "-m", help="Process Manager of your choice ;)")
def start(no_dev, concurrency, procfile, no_prefix, man):
from bench.utils.system import start

start(
no_dev=no_dev,
concurrency=concurrency,
procfile=procfile,
no_prefix=no_prefix,
procman=man,
)


@click.command("restart", help="Restart supervisor processes or systemd units")
@click.option("--web", is_flag=True, default=False)
@click.option("--supervisor", is_flag=True, default=False)
@click.option("--systemd", is_flag=True, default=False)
def restart(web, supervisor, systemd):
from bench.bench import Bench

if not systemd and not web:
supervisor = True

Bench(".").reload(web, supervisor, systemd)


@click.command("set-nginx-port", help="Set NGINX port for site")
@click.argument("site")
@click.argument("port", type=int)
def set_nginx_port(site, port):
from bench.config.site_config import set_nginx_port

set_nginx_port(site, port)


@click.command("set-ssl-certificate", help="Set SSL certificate path for site")
@click.argument("site")
@click.argument("ssl-certificate-path")
def set_ssl_certificate(site, ssl_certificate_path):
from bench.config.site_config import set_ssl_certificate

set_ssl_certificate(site, ssl_certificate_path)


@click.command("set-ssl-key", help="Set SSL certificate private key path for site")
@click.argument("site")
@click.argument("ssl-certificate-key-path")
def set_ssl_certificate_key(site, ssl_certificate_key_path):
from bench.config.site_config import set_ssl_certificate_key

set_ssl_certificate_key(site, ssl_certificate_key_path)


@click.command("set-url-root", help="Set URL root for site")
@click.argument("site")
@click.argument("url-root")
def set_url_root(site, url_root):
from bench.config.site_config import set_url_root

set_url_root(site, url_root)


@click.command("set-mariadb-host", help="Set MariaDB host for bench")
@click.argument("host")
def set_mariadb_host(host):
from bench.utils.bench import set_mariadb_host

set_mariadb_host(host)


@click.command("set-redis-cache-host", help="Set Redis cache host for bench")
@click.argument("host")
def set_redis_cache_host(host):
"""
Usage: bench set-redis-cache-host localhost:6379/1
"""
from bench.utils.bench import set_redis_cache_host

set_redis_cache_host(host)


@click.command("set-redis-queue-host", help="Set Redis queue host for bench")
@click.argument("host")
def set_redis_queue_host(host):
"""
Usage: bench set-redis-queue-host localhost:6379/2
"""
from bench.utils.bench import set_redis_queue_host

set_redis_queue_host(host)


@click.command("set-redis-socketio-host", help="Set Redis socketio host for bench")
@click.argument("host")
def set_redis_socketio_host(host):
"""
Usage: bench set-redis-socketio-host localhost:6379/3
"""
from bench.utils.bench import set_redis_socketio_host

set_redis_socketio_host(host)


@click.command("download-translations", help="Download latest translations")
def download_translations():
from bench.utils.translation import download_translations_p

download_translations_p()


@click.command(
"renew-lets-encrypt", help="Sets Up latest cron and Renew Let's Encrypt certificate"
)
def renew_lets_encrypt():
from bench.config.lets_encrypt import renew_certs

renew_certs()


@click.command("backup-all-sites", help="Backup all sites in current bench")
def backup_all_sites():
from bench.utils.system import backup_all_sites

backup_all_sites(bench_path=".")


@click.command(
"disable-production", help="Disables production environment for the bench."
)
def disable_production():
from bench.config.production_setup import disable_production

disable_production(bench_path=".")


@click.command(
"src", help="Prints bench source folder path, which can be used as: cd `bench src`"
)
def bench_src():
from bench.cli import src

print(os.path.dirname(src))


@click.command("find", help="Finds benches recursively from location")
@click.argument("location", default="")
def find_benches(location):
from bench.utils import find_benches

find_benches(directory=location)


@click.command(
"migrate-env", help="Migrate Virtual Environment to desired Python Version"
)
@click.argument("python", type=str)
@click.option("--no-backup", "backup", is_flag=True, default=True)
def migrate_env(python, backup=True):
from bench.utils.bench import migrate_env

migrate_env(python=python, backup=backup)


@click.command("app-cache", help="View or remove items belonging to bench get-app cache")
@click.option("--clear", is_flag=True, default=False, help="Remove all items")
@click.option(
"--remove-app",
default="",
help="Removes all items that match provided app name",
)
@click.option(
"--remove-key",
default="",
help="Removes all items that matches provided cache key",
)
def app_cache_helper(clear=False, remove_app="", remove_key=""):
from bench.utils.bench import cache_helper

cache_helper(clear, remove_app, remove_key)

+ 7
- 0
bench/config/__init__.py Zobrazit soubor

@@ -0,0 +1,7 @@
"""Module for setting up system and respective bench configurations"""


def env():
from jinja2 import Environment, PackageLoader

return Environment(loader=PackageLoader("bench.config"))

+ 144
- 0
bench/config/common_site_config.py Zobrazit soubor

@@ -0,0 +1,144 @@
# imports - standard imports
import getpass
import json
import os

default_config = {
"restart_supervisor_on_update": False,
"restart_systemd_on_update": False,
"serve_default_site": True,
"rebase_on_pull": False,
"xhiveframework_user": getpass.getuser(),
"shallow_clone": True,
"background_workers": 1,
"use_redis_auth": False,
"live_reload": True,
}

DEFAULT_MAX_REQUESTS = 5000


def setup_config(bench_path, additional_config=None):
make_pid_folder(bench_path)
bench_config = get_config(bench_path)
bench_config.update(default_config)
bench_config.update(get_gunicorn_workers())
update_config_for_xhiveframework(bench_config, bench_path)
if additional_config:
bench_config.update(additional_config)

put_config(bench_config, bench_path)


def get_config(bench_path):
return get_common_site_config(bench_path)


def get_common_site_config(bench_path):
config_path = get_config_path(bench_path)
if not os.path.exists(config_path):
return {}
with open(config_path) as f:
return json.load(f)


def put_config(config, bench_path="."):
config_path = get_config_path(bench_path)
with open(config_path, "w") as f:
return json.dump(config, f, indent=1, sort_keys=True)


def update_config(new_config, bench_path="."):
config = get_config(bench_path=bench_path)
config.update(new_config)
put_config(config, bench_path=bench_path)


def get_config_path(bench_path):
return os.path.join(bench_path, "sites", "common_site_config.json")


def get_gunicorn_workers():
"""This function will return the maximum workers that can be started depending upon
number of cpu's present on the machine"""
import multiprocessing

return {"gunicorn_workers": multiprocessing.cpu_count() * 2 + 1}


def compute_max_requests_jitter(max_requests: int) -> int:
return int(max_requests * 0.1)


def get_default_max_requests(worker_count: int):
"""Get max requests and jitter config based on number of available workers."""

if worker_count <= 1:
# If there's only one worker then random restart can cause spikes in response times and
# can be annoying. Hence not enabled by default.
return 0
return DEFAULT_MAX_REQUESTS


def update_config_for_xhiveframework(config, bench_path):
ports = make_ports(bench_path)

for key in ("redis_cache", "redis_queue", "redis_socketio"):
if key not in config:
config[key] = f"redis://127.0.0.1:{ports[key]}"

for key in ("webserver_port", "socketio_port", "file_watcher_port"):
if key not in config:
config[key] = ports[key]


def make_ports(bench_path):
from urllib.parse import urlparse

benches_path = os.path.dirname(os.path.abspath(bench_path))

default_ports = {
"webserver_port": 8000,
"socketio_port": 9000,
"file_watcher_port": 6787,
"redis_queue": 11000,
"redis_socketio": 13000,
"redis_cache": 13000,
}

# collect all existing ports
existing_ports = {}
for folder in os.listdir(benches_path):
bench_path = os.path.join(benches_path, folder)
if os.path.isdir(bench_path):
bench_config = get_config(bench_path)
for key in list(default_ports.keys()):
value = bench_config.get(key)

# extract port from redis url
if value and (key in ("redis_cache", "redis_queue", "redis_socketio")):
value = urlparse(value).port

if value:
existing_ports.setdefault(key, []).append(value)

# new port value = max of existing port value + 1
ports = {}
for key, value in list(default_ports.items()):
existing_value = existing_ports.get(key, [])
if existing_value:
value = max(existing_value) + 1

ports[key] = value

# Backward compatbility: always keep redis_cache and redis_socketio port same
# Note: not required from v15
ports["redis_socketio"] = ports["redis_cache"]

return ports


def make_pid_folder(bench_path):
pids_path = os.path.join(bench_path, "config", "pids")
if not os.path.exists(pids_path):
os.makedirs(pids_path)

+ 196
- 0
bench/config/lets_encrypt.py Zobrazit soubor

@@ -0,0 +1,196 @@
# imports - standard imports
import os

# imports - third party imports
import click

# imports - module imports
import bench
from bench.config.nginx import make_nginx_conf
from bench.config.production_setup import service
from bench.config.site_config import get_domains, remove_domain, update_site_config
from bench.bench import Bench
from bench.utils import exec_cmd, which
from bench.utils.bench import update_common_site_config
from bench.exceptions import CommandFailedError


def setup_letsencrypt(site, custom_domain, bench_path, interactive):

site_path = os.path.join(bench_path, "sites", site, "site_config.json")
if not os.path.exists(os.path.dirname(site_path)):
print("No site named " + site)
return

if custom_domain:
domains = get_domains(site, bench_path)
for d in domains:
if isinstance(d, dict) and d["domain"] == custom_domain:
print(f"SSL for Domain {custom_domain} already exists")
return

if custom_domain not in domains:
print(f"No custom domain named {custom_domain} set for site")
return

if interactive:
click.confirm(
"Running this will stop the nginx service temporarily causing your sites to go offline\n"
"Do you want to continue?",
abort=True,
)

if not Bench(bench_path).conf.get("dns_multitenant"):
print("You cannot setup SSL without DNS Multitenancy")
return

create_config(site, custom_domain)
run_certbot_and_setup_ssl(site, custom_domain, bench_path, interactive)
setup_crontab()


def create_config(site, custom_domain):
config = (
bench.config.env()
.get_template("letsencrypt.cfg")
.render(domain=custom_domain or site)
)
config_path = f"/etc/letsencrypt/configs/{custom_domain or site}.cfg"
create_dir_if_missing(config_path)

with open(config_path, "w") as f:
f.write(config)


def run_certbot_and_setup_ssl(site, custom_domain, bench_path, interactive=True):
service("nginx", "stop")

try:
interactive = "" if interactive else "-n"
exec_cmd(
f"{get_certbot_path()} {interactive} --config /etc/letsencrypt/configs/{custom_domain or site}.cfg certonly"
)
except CommandFailedError:
service("nginx", "start")
print("There was a problem trying to setup SSL for your site")
return

ssl_path = f"/etc/letsencrypt/live/{custom_domain or site}/"
ssl_config = {
"ssl_certificate": os.path.join(ssl_path, "fullchain.pem"),
"ssl_certificate_key": os.path.join(ssl_path, "privkey.pem"),
}

if custom_domain:
remove_domain(site, custom_domain, bench_path)
domains = get_domains(site, bench_path)
ssl_config["domain"] = custom_domain
domains.append(ssl_config)
update_site_config(site, {"domains": domains}, bench_path=bench_path)
else:
update_site_config(site, ssl_config, bench_path=bench_path)

make_nginx_conf(bench_path)
service("nginx", "start")


def setup_crontab():
from crontab import CronTab

job_command = (
f'{get_certbot_path()} renew -a nginx --post-hook "systemctl reload nginx"'
)
job_comment = "Renew lets-encrypt every month"
print(f"Setting Up cron job to {job_comment}")

system_crontab = CronTab(user="root")

for job in system_crontab.find_comment(comment=job_comment): # Removes older entries
system_crontab.remove(job)

job = system_crontab.new(command=job_command, comment=job_comment)
job.setall("0 0 */1 * *") # Run at 00:00 every day-of-month
system_crontab.write()


def create_dir_if_missing(path):
if not os.path.exists(os.path.dirname(path)):
os.makedirs(os.path.dirname(path))


def get_certbot_path():
try:
return which("certbot", raise_err=True)
except FileNotFoundError:
raise CommandFailedError(
"Certbot is not installed on your system. Please visit https://certbot.eff.org/instructions for installation instructions, then try again."
)


def renew_certs():
# Needs to be run with sudo
click.confirm(
"Running this will stop the nginx service temporarily causing your sites to go offline\n"
"Do you want to continue?",
abort=True,
)

setup_crontab()

service("nginx", "stop")
exec_cmd(f"{get_certbot_path()} renew")
service("nginx", "start")


def setup_wildcard_ssl(domain, email, bench_path, exclude_base_domain):
def _get_domains(domain):
domain_list = [domain]

if not domain.startswith("*."):
# add wildcard caracter to domain if missing
domain_list.append(f"*.{domain}")
else:
# include base domain based on flag
domain_list.append(domain.replace("*.", ""))

if exclude_base_domain:
domain_list.remove(domain.replace("*.", ""))

return domain_list

if not Bench(bench_path).conf.get("dns_multitenant"):
print("You cannot setup SSL without DNS Multitenancy")
return

domain_list = _get_domains(domain.strip())

email_param = ""
if email:
email_param = f"--email {email}"

try:
exec_cmd(
f"{get_certbot_path()} certonly --manual --preferred-challenges=dns {email_param} \
--server https://acme-v02.api.letsencrypt.org/directory \
--agree-tos -d {' -d '.join(domain_list)}"
)

except CommandFailedError:
print("There was a problem trying to setup SSL")
return

ssl_path = f"/etc/letsencrypt/live/{domain}/"
ssl_config = {
"wildcard": {
"domain": domain,
"ssl_certificate": os.path.join(ssl_path, "fullchain.pem"),
"ssl_certificate_key": os.path.join(ssl_path, "privkey.pem"),
}
}

update_common_site_config(ssl_config)
setup_crontab()

make_nginx_conf(bench_path)
print("Restrting Nginx service")
service("nginx", "restart")

+ 302
- 0
bench/config/nginx.py Zobrazit soubor

@@ -0,0 +1,302 @@
# imports - standard imports
import hashlib
import os
import random
import string

# imports - third party imports
import click

# imports - module imports
import bench
import bench.config
from bench.bench import Bench
from bench.utils import get_bench_name


def make_nginx_conf(bench_path, yes=False, logging=None, log_format=None):
conf_path = os.path.join(bench_path, "config", "nginx.conf")

if not yes and os.path.exists(conf_path):
if not click.confirm(
"nginx.conf already exists and this will overwrite it. Do you want to continue?"
):
return

template = bench.config.env().get_template("nginx.conf")
bench_path = os.path.abspath(bench_path)
sites_path = os.path.join(bench_path, "sites")

config = Bench(bench_path).conf
sites = prepare_sites(config, bench_path)
bench_name = get_bench_name(bench_path)

allow_rate_limiting = config.get("allow_rate_limiting", False)

template_vars = {
"sites_path": sites_path,
"http_timeout": config.get("http_timeout"),
"sites": sites,
"webserver_port": config.get("webserver_port"),
"socketio_port": config.get("socketio_port"),
"bench_name": bench_name,
"error_pages": get_error_pages(),
"allow_rate_limiting": allow_rate_limiting,
# for nginx map variable
"random_string": "".join(random.choice(string.ascii_lowercase) for i in range(7)),
}

if logging and logging != "none":
_log_format = ""
if log_format and log_format != "none":
_log_format = log_format
template_vars["logging"] = {"level": logging, "log_format": _log_format}

if allow_rate_limiting:
template_vars.update(
{
"bench_name_hash": hashlib.sha256(bench_name).hexdigest()[:16],
"limit_conn_shared_memory": get_limit_conn_shared_memory(),
}
)

nginx_conf = template.render(**template_vars)

with open(conf_path, "w") as f:
f.write(nginx_conf)


def make_bench_manager_nginx_conf(bench_path, yes=False, port=23624, domain=None):
from bench.config.site_config import get_site_config

template = bench.config.env().get_template("bench_manager_nginx.conf")
bench_path = os.path.abspath(bench_path)
sites_path = os.path.join(bench_path, "sites")

config = Bench(bench_path).conf
site_config = get_site_config(domain, bench_path=bench_path)
bench_name = get_bench_name(bench_path)

template_vars = {
"port": port,
"domain": domain,
"bench_manager_site_name": "bench-manager.local",
"sites_path": sites_path,
"http_timeout": config.get("http_timeout"),
"webserver_port": config.get("webserver_port"),
"socketio_port": config.get("socketio_port"),
"bench_name": bench_name,
"error_pages": get_error_pages(),
"ssl_certificate": site_config.get("ssl_certificate"),
"ssl_certificate_key": site_config.get("ssl_certificate_key"),
}

bench_manager_nginx_conf = template.render(**template_vars)

conf_path = os.path.join(bench_path, "config", "nginx.conf")

if not yes and os.path.exists(conf_path):
click.confirm(
"nginx.conf already exists and bench-manager configuration will be appended to it. Do you want to continue?",
abort=True,
)

with open(conf_path, "a") as myfile:
myfile.write(bench_manager_nginx_conf)


def prepare_sites(config, bench_path):
sites = {
"that_use_port": [],
"that_use_dns": [],
"that_use_ssl": [],
"that_use_wildcard_ssl": [],
}

domain_map = {}
ports_in_use = {}

dns_multitenant = config.get("dns_multitenant")

shared_port_exception_found = False
sites_configs = get_sites_with_config(bench_path=bench_path)

# preload all preset site ports to avoid conflicts

if not dns_multitenant:
for site in sites_configs:
if site.get("port"):
if not site["port"] in ports_in_use:
ports_in_use[site["port"]] = []
ports_in_use[site["port"]].append(site["name"])

for site in sites_configs:
if dns_multitenant:
domain = site.get("domain")

if domain:
# when site's folder name is different than domain name
domain_map[domain] = site["name"]

site_name = domain or site["name"]

if site.get("wildcard"):
sites["that_use_wildcard_ssl"].append(site_name)

if not sites.get("wildcard_ssl_certificate"):
sites["wildcard_ssl_certificate"] = site["ssl_certificate"]
sites["wildcard_ssl_certificate_key"] = site["ssl_certificate_key"]

elif site.get("ssl_certificate") and site.get("ssl_certificate_key"):
sites["that_use_ssl"].append(site)

else:
sites["that_use_dns"].append(site_name)

else:
if not site.get("port"):
site["port"] = 80
if site["port"] in ports_in_use:
site["port"] = 8001
while site["port"] in ports_in_use:
site["port"] += 1

if site["port"] in ports_in_use and not site["name"] in ports_in_use[site["port"]]:
shared_port_exception_found = True
ports_in_use[site["port"]].append(site["name"])
else:
ports_in_use[site["port"]] = []
ports_in_use[site["port"]].append(site["name"])

sites["that_use_port"].append(site)

if not dns_multitenant and shared_port_exception_found:
message = "Port conflicts found:"
port_conflict_index = 0
for port_number in ports_in_use:
if len(ports_in_use[port_number]) > 1:
port_conflict_index += 1
message += f"\n{port_conflict_index} - Port {port_number} is shared among sites:"
for site_name in ports_in_use[port_number]:
message += f" {site_name}"
raise Exception(message)

if not dns_multitenant:
message = "Port configuration list:"
for site in sites_configs:
message += f"\n\nSite {site['name']} assigned port: {site['port']}"

print(message)

sites["domain_map"] = domain_map

return sites


def get_sites_with_config(bench_path):
from bench.bench import Bench
from bench.config.site_config import get_site_config

bench = Bench(bench_path)
sites = bench.sites
conf = bench.conf
dns_multitenant = conf.get("dns_multitenant")

ret = []
for site in sites:
try:
site_config = get_site_config(site, bench_path=bench_path)
except Exception as e:
strict_nginx = conf.get("strict_nginx")
if strict_nginx:
print(
f"\n\nERROR: The site config for the site {site} is broken.",
"If you want this command to pass, instead of just throwing an error,",
"You may remove the 'strict_nginx' flag from common_site_config.json or set it to 0",
"\n\n",
)
raise e
else:
print(
f"\n\nWARNING: The site config for the site {site} is broken.",
"If you want this command to fail, instead of just showing a warning,",
"You may add the 'strict_nginx' flag to common_site_config.json and set it to 1",
"\n\n",
)
continue

ret.append(
{
"name": site,
"port": site_config.get("nginx_port"),
"ssl_certificate": site_config.get("ssl_certificate"),
"ssl_certificate_key": site_config.get("ssl_certificate_key"),
}
)

if dns_multitenant and site_config.get("domains"):
for domain in site_config.get("domains"):
# domain can be a string or a dict with 'domain', 'ssl_certificate', 'ssl_certificate_key'
if isinstance(domain, str):
domain = {"domain": domain}

domain["name"] = site
ret.append(domain)

use_wildcard_certificate(bench_path, ret)

return ret


def use_wildcard_certificate(bench_path, ret):
"""
stored in common_site_config.json as:
"wildcard": {
"domain": "*.xhiveerp.com",
"ssl_certificate": "/path/to/xhiveerp.com.cert",
"ssl_certificate_key": "/path/to/xhiveerp.com.key"
}
"""
from bench.bench import Bench

config = Bench(bench_path).conf
wildcard = config.get("wildcard")

if not wildcard:
return

domain = wildcard["domain"]
ssl_certificate = wildcard["ssl_certificate"]
ssl_certificate_key = wildcard["ssl_certificate_key"]

# If domain is set as "*" all domains will be included
if domain.startswith("*"):
domain = domain[1:]
else:
domain = "." + domain

for site in ret:
if site.get("ssl_certificate"):
continue

if (site.get("domain") or site["name"]).endswith(domain):
# example: ends with .xhiveerp.com
site["ssl_certificate"] = ssl_certificate
site["ssl_certificate_key"] = ssl_certificate_key
site["wildcard"] = 1


def get_error_pages():
bench_app_path = os.path.abspath(bench.__path__[0])
templates = os.path.join(bench_app_path, "config", "templates")

return {502: os.path.join(templates, "502.html")}


def get_limit_conn_shared_memory():
"""Allocate 2 percent of total virtual memory as shared memory for nginx limit_conn_zone"""
total_vm = (os.sysconf("SC_PAGE_SIZE") * os.sysconf("SC_PHYS_PAGES")) / (
1024 * 1024
) # in MB

return int(0.02 * total_vm)

+ 38
- 0
bench/config/procfile.py Zobrazit soubor

@@ -0,0 +1,38 @@
import os
import platform

import click

import bench
from bench.app import use_rq
from bench.bench import Bench
from bench.utils import which


def setup_procfile(bench_path, yes=False, skip_redis=False):
config = Bench(bench_path).conf
procfile_path = os.path.join(bench_path, "Procfile")

is_mac = platform.system() == "Darwin"
if not yes and os.path.exists(procfile_path):
click.confirm(
"A Procfile already exists and this will overwrite it. Do you want to continue?",
abort=True,
)

procfile = (
bench.config.env()
.get_template("Procfile")
.render(
node=which("node") or which("nodejs"),
use_rq=use_rq(bench_path),
webserver_port=config.get("webserver_port"),
CI=os.environ.get("CI"),
skip_redis=skip_redis,
workers=config.get("workers", {}),
is_mac=is_mac,
)
)

with open(procfile_path, "w") as f:
f.write(procfile)

+ 206
- 0
bench/config/production_setup.py Zobrazit soubor

@@ -0,0 +1,206 @@
# imports - standard imports
import contextlib
import os
import logging
import sys

# imports - module imports
import bench
from bench.config.nginx import make_nginx_conf
from bench.config.supervisor import (
generate_supervisor_config,
check_supervisord_config,
)
from bench.config.systemd import generate_systemd_config
from bench.bench import Bench
from bench.utils import exec_cmd, which, get_bench_name, get_cmd_output, log
from bench.utils.system import fix_prod_setup_perms
from bench.exceptions import CommandFailedError

logger = logging.getLogger(bench.PROJECT_NAME)


def setup_production_prerequisites():
"""Installs ansible, fail2banc, NGINX and supervisor"""
if not which("ansible"):
exec_cmd(f"sudo {sys.executable} -m pip install ansible")
if not which("fail2ban-client"):
exec_cmd("bench setup role fail2ban")
if not which("nginx"):
exec_cmd("bench setup role nginx")
if not which("supervisord"):
exec_cmd("bench setup role supervisor")


def setup_production(user, bench_path=".", yes=False):
print("Setting Up prerequisites...")
setup_production_prerequisites()

conf = Bench(bench_path).conf

if conf.get("restart_supervisor_on_update") and conf.get("restart_systemd_on_update"):
raise Exception(
"You cannot use supervisor and systemd at the same time. Modify your common_site_config accordingly."
)

if conf.get("restart_systemd_on_update"):
print("Setting Up systemd...")
generate_systemd_config(bench_path=bench_path, user=user, yes=yes)
else:
print("Setting Up supervisor...")
check_supervisord_config(user=user)
generate_supervisor_config(bench_path=bench_path, user=user, yes=yes)

print("Setting Up NGINX...")
make_nginx_conf(bench_path=bench_path, yes=yes)
fix_prod_setup_perms(bench_path, xhiveframework_user=user)
remove_default_nginx_configs()

bench_name = get_bench_name(bench_path)
nginx_conf = f"/etc/nginx/conf.d/{bench_name}.conf"

print("Setting Up symlinks and reloading services...")
if conf.get("restart_supervisor_on_update"):
supervisor_conf_extn = "ini" if is_centos7() else "conf"
supervisor_conf = os.path.join(
get_supervisor_confdir(), f"{bench_name}.{supervisor_conf_extn}"
)

# Check if symlink exists, If not then create it.
if not os.path.islink(supervisor_conf):
os.symlink(
os.path.abspath(os.path.join(bench_path, "config", "supervisor.conf")),
supervisor_conf,
)

if not os.path.islink(nginx_conf):
os.symlink(
os.path.abspath(os.path.join(bench_path, "config", "nginx.conf")), nginx_conf
)

if conf.get("restart_supervisor_on_update"):
reload_supervisor()

if os.environ.get("NO_SERVICE_RESTART"):
return

reload_nginx()


def disable_production(bench_path="."):
bench_name = get_bench_name(bench_path)
conf = Bench(bench_path).conf

# supervisorctl
supervisor_conf_extn = "ini" if is_centos7() else "conf"
supervisor_conf = os.path.join(
get_supervisor_confdir(), f"{bench_name}.{supervisor_conf_extn}"
)

if os.path.islink(supervisor_conf):
os.unlink(supervisor_conf)

if conf.get("restart_supervisor_on_update"):
reload_supervisor()

# nginx
nginx_conf = f"/etc/nginx/conf.d/{bench_name}.conf"

if os.path.islink(nginx_conf):
os.unlink(nginx_conf)

reload_nginx()


def service(service_name, service_option):
if os.path.basename(which("systemctl") or "") == "systemctl" and is_running_systemd():
exec_cmd(f"sudo systemctl {service_option} {service_name}")

elif os.path.basename(which("service") or "") == "service":
exec_cmd(f"sudo service {service_name} {service_option}")

else:
# look for 'service_manager' and 'service_manager_command' in environment
service_manager = os.environ.get("BENCH_SERVICE_MANAGER")
if service_manager:
service_manager_command = (
os.environ.get("BENCH_SERVICE_MANAGER_COMMAND")
or f"{service_manager} {service_option} {service}"
)
exec_cmd(service_manager_command)

else:
log(
f"No service manager found: '{service_name} {service_option}' failed to execute",
level=2,
)


def get_supervisor_confdir():
possiblities = (
"/etc/supervisor/conf.d",
"/etc/supervisor.d/",
"/etc/supervisord/conf.d",
"/etc/supervisord.d",
)
for possiblity in possiblities:
if os.path.exists(possiblity):
return possiblity


def remove_default_nginx_configs():
default_nginx_configs = [
"/etc/nginx/conf.d/default.conf",
"/etc/nginx/sites-enabled/default",
]

for conf_file in default_nginx_configs:
if os.path.exists(conf_file):
os.unlink(conf_file)


def is_centos7():
return (
os.path.exists("/etc/redhat-release")
and get_cmd_output(
r"cat /etc/redhat-release | sed 's/Linux\ //g' | cut -d' ' -f3 | cut -d. -f1"
).strip()
== "7"
)


def is_running_systemd():
with open("/proc/1/comm") as f:
comm = f.read().strip()
if comm == "init":
return False
elif comm == "systemd":
return True
return False


def reload_supervisor():
supervisorctl = which("supervisorctl")

with contextlib.suppress(CommandFailedError):
# first try reread/update
exec_cmd(f"{supervisorctl} reread")
exec_cmd(f"{supervisorctl} update")
return
with contextlib.suppress(CommandFailedError):
# something is wrong, so try reloading
exec_cmd(f"{supervisorctl} reload")
return
with contextlib.suppress(CommandFailedError):
# then try restart for centos
service("supervisord", "restart")
return
with contextlib.suppress(CommandFailedError):
# else try restart for ubuntu / debian
service("supervisor", "restart")
return


def reload_nginx():
exec_cmd(f"sudo {which('nginx')} -t")
service("nginx", "reload")

+ 89
- 0
bench/config/redis.py Zobrazit soubor

@@ -0,0 +1,89 @@
# imports - standard imports
import os
import re
import subprocess

# imports - module imports
import bench


def generate_config(bench_path):
from urllib.parse import urlparse
from bench.bench import Bench

config = Bench(bench_path).conf
redis_version = get_redis_version()

ports = {}
for key in ("redis_cache", "redis_queue"):
ports[key] = urlparse(config[key]).port

write_redis_config(
template_name="redis_queue.conf",
context={
"port": ports["redis_queue"],
"bench_path": os.path.abspath(bench_path),
"redis_version": redis_version,
},
bench_path=bench_path,
)

write_redis_config(
template_name="redis_cache.conf",
context={
"maxmemory": config.get("cache_maxmemory", get_max_redis_memory()),
"port": ports["redis_cache"],
"redis_version": redis_version,
},
bench_path=bench_path,
)

# make pids folder
pid_path = os.path.join(bench_path, "config", "pids")
if not os.path.exists(pid_path):
os.makedirs(pid_path)

# ACL feature is introduced in Redis 6.0
if redis_version < 6.0:
return

# make ACL files
acl_rq_path = os.path.join(bench_path, "config", "redis_queue.acl")
acl_redis_cache_path = os.path.join(bench_path, "config", "redis_cache.acl")
open(acl_rq_path, "a").close()
open(acl_redis_cache_path, "a").close()


def write_redis_config(template_name, context, bench_path):
template = bench.config.env().get_template(template_name)

if "config_path" not in context:
context["config_path"] = os.path.abspath(os.path.join(bench_path, "config"))

if "pid_path" not in context:
context["pid_path"] = os.path.join(context["config_path"], "pids")

with open(os.path.join(bench_path, "config", template_name), "w") as f:
f.write(template.render(**context))


def get_redis_version():
import semantic_version

version_string = subprocess.check_output("redis-server --version", shell=True)
version_string = version_string.decode("utf-8").strip()
# extract version number from string
version = re.findall(r"\d+\.\d+", version_string)
if not version:
return None

version = semantic_version.Version(version[0], partial=True)
return float(f"{version.major}.{version.minor}")


def get_max_redis_memory():
try:
max_mem = os.sysconf("SC_PAGE_SIZE") * os.sysconf("SC_PHYS_PAGES")
except ValueError:
max_mem = int(subprocess.check_output(["sysctl", "-n", "hw.memsize"]).strip())
return max(50, int((max_mem / (1024.0**2)) * 0.05))

+ 129
- 0
bench/config/site_config.py Zobrazit soubor

@@ -0,0 +1,129 @@
# imports - standard imports
import json
import os
from collections import defaultdict


def get_site_config(site, bench_path="."):
config_path = os.path.join(bench_path, "sites", site, "site_config.json")
if not os.path.exists(config_path):
return {}
with open(config_path) as f:
return json.load(f)


def put_site_config(site, config, bench_path="."):
config_path = os.path.join(bench_path, "sites", site, "site_config.json")
with open(config_path, "w") as f:
return json.dump(config, f, indent=1)


def update_site_config(site, new_config, bench_path="."):
config = get_site_config(site, bench_path=bench_path)
config.update(new_config)
put_site_config(site, config, bench_path=bench_path)


def set_nginx_port(site, port, bench_path=".", gen_config=True):
set_site_config_nginx_property(
site, {"nginx_port": port}, bench_path=bench_path, gen_config=gen_config
)


def set_ssl_certificate(site, ssl_certificate, bench_path=".", gen_config=True):
set_site_config_nginx_property(
site,
{"ssl_certificate": ssl_certificate},
bench_path=bench_path,
gen_config=gen_config,
)


def set_ssl_certificate_key(site, ssl_certificate_key, bench_path=".", gen_config=True):
set_site_config_nginx_property(
site,
{"ssl_certificate_key": ssl_certificate_key},
bench_path=bench_path,
gen_config=gen_config,
)


def set_site_config_nginx_property(site, config, bench_path=".", gen_config=True):
from bench.config.nginx import make_nginx_conf
from bench.bench import Bench

if site not in Bench(bench_path).sites:
raise Exception("No such site")
update_site_config(site, config, bench_path=bench_path)
if gen_config:
make_nginx_conf(bench_path=bench_path)


def set_url_root(site, url_root, bench_path="."):
update_site_config(site, {"host_name": url_root}, bench_path=bench_path)


def add_domain(site, domain, ssl_certificate, ssl_certificate_key, bench_path="."):
domains = get_domains(site, bench_path)
for d in domains:
if (isinstance(d, dict) and d["domain"] == domain) or d == domain:
print(f"Domain {domain} already exists")
return

if ssl_certificate_key and ssl_certificate:
domain = {
"domain": domain,
"ssl_certificate": ssl_certificate,
"ssl_certificate_key": ssl_certificate_key,
}

domains.append(domain)
update_site_config(site, {"domains": domains}, bench_path=bench_path)


def remove_domain(site, domain, bench_path="."):
domains = get_domains(site, bench_path)
for i, d in enumerate(domains):
if (isinstance(d, dict) and d["domain"] == domain) or d == domain:
domains.remove(d)
break

update_site_config(site, {"domains": domains}, bench_path=bench_path)


def sync_domains(site, domains, bench_path="."):
"""Checks if there is a change in domains. If yes, updates the domains list."""
changed = False
existing_domains = get_domains_dict(get_domains(site, bench_path))
new_domains = get_domains_dict(domains)

if set(existing_domains.keys()) != set(new_domains.keys()):
changed = True

else:
for d in list(existing_domains.values()):
if d != new_domains.get(d["domain"]):
changed = True
break

if changed:
# replace existing domains with this one
update_site_config(site, {"domains": domains}, bench_path=".")

return changed


def get_domains(site, bench_path="."):
return get_site_config(site, bench_path=bench_path).get("domains") or []


def get_domains_dict(domains):
domains_dict = defaultdict(dict)
for d in domains:
if isinstance(d, str):
domains_dict[d] = {"domain": d}

elif isinstance(d, dict):
domains_dict[d["domain"]] = d

return domains_dict

+ 167
- 0
bench/config/supervisor.py Zobrazit soubor

@@ -0,0 +1,167 @@
# imports - standard imports
import getpass
import logging
import os

# imports - third party imports
import click

# imports - module imports
import bench
from bench.app import use_rq
from bench.bench import Bench
from bench.config.common_site_config import (
compute_max_requests_jitter,
get_config,
get_default_max_requests,
get_gunicorn_workers,
update_config,
)
from bench.utils import get_bench_name, which

logger = logging.getLogger(bench.PROJECT_NAME)


def generate_supervisor_config(bench_path, user=None, yes=False, skip_redis=False):
"""Generate supervisor config for respective bench path"""
if not user:
user = getpass.getuser()

config = Bench(bench_path).conf
template = bench.config.env().get_template("supervisor.conf")
bench_dir = os.path.abspath(bench_path)

web_worker_count = config.get(
"gunicorn_workers", get_gunicorn_workers()["gunicorn_workers"]
)
max_requests = config.get(
"gunicorn_max_requests", get_default_max_requests(web_worker_count)
)

config = template.render(
**{
"bench_dir": bench_dir,
"sites_dir": os.path.join(bench_dir, "sites"),
"user": user,
"use_rq": use_rq(bench_path),
"http_timeout": config.get("http_timeout", 120),
"redis_server": which("redis-server"),
"node": which("node") or which("nodejs"),
"redis_cache_config": os.path.join(bench_dir, "config", "redis_cache.conf"),
"redis_queue_config": os.path.join(bench_dir, "config", "redis_queue.conf"),
"webserver_port": config.get("webserver_port", 8000),
"gunicorn_workers": web_worker_count,
"gunicorn_max_requests": max_requests,
"gunicorn_max_requests_jitter": compute_max_requests_jitter(max_requests),
"bench_name": get_bench_name(bench_path),
"background_workers": config.get("background_workers") or 1,
"bench_cmd": which("bench"),
"skip_redis": skip_redis,
"workers": config.get("workers", {}),
"multi_queue_consumption": can_enable_multi_queue_consumption(bench_path),
"supervisor_startretries": 10,
}
)

conf_path = os.path.join(bench_path, "config", "supervisor.conf")
if not yes and os.path.exists(conf_path):
click.confirm(
"supervisor.conf already exists and this will overwrite it. Do you want to continue?",
abort=True,
)

with open(conf_path, "w") as f:
f.write(config)

update_config({"restart_supervisor_on_update": True}, bench_path=bench_path)
update_config({"restart_systemd_on_update": False}, bench_path=bench_path)
sync_socketio_port(bench_path)


def get_supervisord_conf():
"""Returns path of supervisord config from possible paths"""
possibilities = (
"supervisord.conf",
"etc/supervisord.conf",
"/etc/supervisord.conf",
"/etc/supervisor/supervisord.conf",
"/etc/supervisord.conf",
)

for possibility in possibilities:
if os.path.exists(possibility):
return possibility


def sync_socketio_port(bench_path):
# Backward compatbility: always keep redis_cache and redis_socketio port same
common_config = get_config(bench_path=bench_path)

socketio_port = common_config.get("redis_socketio")
cache_port = common_config.get("redis_cache")
if socketio_port and socketio_port != cache_port:
update_config({"redis_socketio": cache_port})


def can_enable_multi_queue_consumption(bench_path: str) -> bool:
try:
from semantic_version import Version

from bench.utils.app import get_current_version

supported_version = Version(major=14, minor=18, patch=0)

xhiveframework_version = Version(get_current_version("xhiveframework", bench_path=bench_path))

return xhiveframework_version > supported_version
except Exception:
return False


def check_supervisord_config(user=None):
"""From bench v5.x, we're moving to supervisor running as user"""
# i don't think bench should be responsible for this but we're way past this now...
# removed updating supervisord conf & reload in Aug 2022 - gavin@xhiveframework.io
import configparser

if not user:
user = getpass.getuser()

supervisord_conf = get_supervisord_conf()
section = "unix_http_server"
updated_values = {"chmod": "0760", "chown": f"{user}:{user}"}
supervisord_conf_changes = ""

if not supervisord_conf:
logger.log("supervisord.conf not found")
return

config = configparser.ConfigParser()
config.read(supervisord_conf)

if section not in config.sections():
config.add_section(section)
action = f"Section {section} Added"
logger.log(action)
supervisord_conf_changes += "\n" + action

for key, value in updated_values.items():
try:
current_value = config.get(section, key)
except configparser.NoOptionError:
current_value = ""

if current_value.strip() != value:
config.set(section, key, value)
action = (
f"Updated supervisord.conf: '{key}' changed from '{current_value}' to '{value}'"
)
logger.log(action)
supervisord_conf_changes += "\n" + action

if not supervisord_conf_changes:
logger.error("supervisord.conf not updated")
contents = "\n".join(f"{x}={y}" for x, y in updated_values.items())
print(
f"Update your {supervisord_conf} with the following values:\n[{section}]\n{contents}"
)

+ 309
- 0
bench/config/systemd.py Zobrazit soubor

@@ -0,0 +1,309 @@
# imports - standard imports
import getpass
import os

# imports - third partyimports
import click

# imports - module imports
import bench
from bench.app import use_rq
from bench.bench import Bench
from bench.config.common_site_config import (
get_gunicorn_workers,
update_config,
get_default_max_requests,
compute_max_requests_jitter,
)
from bench.utils import exec_cmd, which, get_bench_name


def generate_systemd_config(
bench_path,
user=None,
yes=False,
stop=False,
create_symlinks=False,
delete_symlinks=False,
):

if not user:
user = getpass.getuser()

config = Bench(bench_path).conf

bench_dir = os.path.abspath(bench_path)
bench_name = get_bench_name(bench_path)

if stop:
exec_cmd(
f"sudo systemctl stop -- $(systemctl show -p Requires {bench_name}.target | cut -d= -f2)"
)
return

if create_symlinks:
_create_symlinks(bench_path)
return

if delete_symlinks:
_delete_symlinks(bench_path)
return

number_of_workers = config.get("background_workers") or 1
background_workers = []
for i in range(number_of_workers):
background_workers.append(
get_bench_name(bench_path) + "-xhiveframework-default-worker@" + str(i + 1) + ".service"
)

for i in range(number_of_workers):
background_workers.append(
get_bench_name(bench_path) + "-xhiveframework-short-worker@" + str(i + 1) + ".service"
)

for i in range(number_of_workers):
background_workers.append(
get_bench_name(bench_path) + "-xhiveframework-long-worker@" + str(i + 1) + ".service"
)

web_worker_count = config.get(
"gunicorn_workers", get_gunicorn_workers()["gunicorn_workers"]
)
max_requests = config.get(
"gunicorn_max_requests", get_default_max_requests(web_worker_count)
)

bench_info = {
"bench_dir": bench_dir,
"sites_dir": os.path.join(bench_dir, "sites"),
"user": user,
"use_rq": use_rq(bench_path),
"http_timeout": config.get("http_timeout", 120),
"redis_server": which("redis-server"),
"node": which("node") or which("nodejs"),
"redis_cache_config": os.path.join(bench_dir, "config", "redis_cache.conf"),
"redis_queue_config": os.path.join(bench_dir, "config", "redis_queue.conf"),
"webserver_port": config.get("webserver_port", 8000),
"gunicorn_workers": web_worker_count,
"gunicorn_max_requests": max_requests,
"gunicorn_max_requests_jitter": compute_max_requests_jitter(max_requests),
"bench_name": get_bench_name(bench_path),
"worker_target_wants": " ".join(background_workers),
"bench_cmd": which("bench"),
}

if not yes:
click.confirm(
"current systemd configuration will be overwritten. Do you want to continue?",
abort=True,
)

setup_systemd_directory(bench_path)
setup_main_config(bench_info, bench_path)
setup_workers_config(bench_info, bench_path)
setup_web_config(bench_info, bench_path)
setup_redis_config(bench_info, bench_path)

update_config({"restart_systemd_on_update": False}, bench_path=bench_path)
update_config({"restart_supervisor_on_update": False}, bench_path=bench_path)


def setup_systemd_directory(bench_path):
if not os.path.exists(os.path.join(bench_path, "config", "systemd")):
os.makedirs(os.path.join(bench_path, "config", "systemd"))


def setup_main_config(bench_info, bench_path):
# Main config
bench_template = bench.config.env().get_template("systemd/xhiveframework-bench.target")
bench_config = bench_template.render(**bench_info)
bench_config_path = os.path.join(
bench_path, "config", "systemd", bench_info.get("bench_name") + ".target"
)

with open(bench_config_path, "w") as f:
f.write(bench_config)


def setup_workers_config(bench_info, bench_path):
# Worker Group
bench_workers_target_template = bench.config.env().get_template(
"systemd/xhiveframework-bench-workers.target"
)
bench_default_worker_template = bench.config.env().get_template(
"systemd/xhiveframework-bench-xhiveframework-default-worker.service"
)
bench_short_worker_template = bench.config.env().get_template(
"systemd/xhiveframework-bench-xhiveframework-short-worker.service"
)
bench_long_worker_template = bench.config.env().get_template(
"systemd/xhiveframework-bench-xhiveframework-long-worker.service"
)
bench_schedule_worker_template = bench.config.env().get_template(
"systemd/xhiveframework-bench-xhiveframework-schedule.service"
)

bench_workers_target_config = bench_workers_target_template.render(**bench_info)
bench_default_worker_config = bench_default_worker_template.render(**bench_info)
bench_short_worker_config = bench_short_worker_template.render(**bench_info)
bench_long_worker_config = bench_long_worker_template.render(**bench_info)
bench_schedule_worker_config = bench_schedule_worker_template.render(**bench_info)

bench_workers_target_config_path = os.path.join(
bench_path, "config", "systemd", bench_info.get("bench_name") + "-workers.target"
)
bench_default_worker_config_path = os.path.join(
bench_path,
"config",
"systemd",
bench_info.get("bench_name") + "-xhiveframework-default-worker@.service",
)
bench_short_worker_config_path = os.path.join(
bench_path,
"config",
"systemd",
bench_info.get("bench_name") + "-xhiveframework-short-worker@.service",
)
bench_long_worker_config_path = os.path.join(
bench_path,
"config",
"systemd",
bench_info.get("bench_name") + "-xhiveframework-long-worker@.service",
)
bench_schedule_worker_config_path = os.path.join(
bench_path,
"config",
"systemd",
bench_info.get("bench_name") + "-xhiveframework-schedule.service",
)

with open(bench_workers_target_config_path, "w") as f:
f.write(bench_workers_target_config)

with open(bench_default_worker_config_path, "w") as f:
f.write(bench_default_worker_config)

with open(bench_short_worker_config_path, "w") as f:
f.write(bench_short_worker_config)

with open(bench_long_worker_config_path, "w") as f:
f.write(bench_long_worker_config)

with open(bench_schedule_worker_config_path, "w") as f:
f.write(bench_schedule_worker_config)


def setup_web_config(bench_info, bench_path):
# Web Group
bench_web_target_template = bench.config.env().get_template(
"systemd/xhiveframework-bench-web.target"
)
bench_web_service_template = bench.config.env().get_template(
"systemd/xhiveframework-bench-xhiveframework-web.service"
)
bench_node_socketio_template = bench.config.env().get_template(
"systemd/xhiveframework-bench-node-socketio.service"
)

bench_web_target_config = bench_web_target_template.render(**bench_info)
bench_web_service_config = bench_web_service_template.render(**bench_info)
bench_node_socketio_config = bench_node_socketio_template.render(**bench_info)

bench_web_target_config_path = os.path.join(
bench_path, "config", "systemd", bench_info.get("bench_name") + "-web.target"
)
bench_web_service_config_path = os.path.join(
bench_path, "config", "systemd", bench_info.get("bench_name") + "-xhiveframework-web.service"
)
bench_node_socketio_config_path = os.path.join(
bench_path,
"config",
"systemd",
bench_info.get("bench_name") + "-node-socketio.service",
)

with open(bench_web_target_config_path, "w") as f:
f.write(bench_web_target_config)

with open(bench_web_service_config_path, "w") as f:
f.write(bench_web_service_config)

with open(bench_node_socketio_config_path, "w") as f:
f.write(bench_node_socketio_config)


def setup_redis_config(bench_info, bench_path):
# Redis Group
bench_redis_target_template = bench.config.env().get_template(
"systemd/xhiveframework-bench-redis.target"
)
bench_redis_cache_template = bench.config.env().get_template(
"systemd/xhiveframework-bench-redis-cache.service"
)
bench_redis_queue_template = bench.config.env().get_template(
"systemd/xhiveframework-bench-redis-queue.service"
)

bench_redis_target_config = bench_redis_target_template.render(**bench_info)
bench_redis_cache_config = bench_redis_cache_template.render(**bench_info)
bench_redis_queue_config = bench_redis_queue_template.render(**bench_info)

bench_redis_target_config_path = os.path.join(
bench_path, "config", "systemd", bench_info.get("bench_name") + "-redis.target"
)
bench_redis_cache_config_path = os.path.join(
bench_path, "config", "systemd", bench_info.get("bench_name") + "-redis-cache.service"
)
bench_redis_queue_config_path = os.path.join(
bench_path, "config", "systemd", bench_info.get("bench_name") + "-redis-queue.service"
)

with open(bench_redis_target_config_path, "w") as f:
f.write(bench_redis_target_config)

with open(bench_redis_cache_config_path, "w") as f:
f.write(bench_redis_cache_config)

with open(bench_redis_queue_config_path, "w") as f:
f.write(bench_redis_queue_config)


def _create_symlinks(bench_path):
bench_dir = os.path.abspath(bench_path)
etc_systemd_system = os.path.join("/", "etc", "systemd", "system")
config_path = os.path.join(bench_dir, "config", "systemd")
unit_files = get_unit_files(bench_dir)
for unit_file in unit_files:
filename = "".join(unit_file)
exec_cmd(
f'sudo ln -s {config_path}/{filename} {etc_systemd_system}/{"".join(unit_file)}'
)
exec_cmd("sudo systemctl daemon-reload")


def _delete_symlinks(bench_path):
bench_dir = os.path.abspath(bench_path)
etc_systemd_system = os.path.join("/", "etc", "systemd", "system")
unit_files = get_unit_files(bench_dir)
for unit_file in unit_files:
exec_cmd(f'sudo rm {etc_systemd_system}/{"".join(unit_file)}')
exec_cmd("sudo systemctl daemon-reload")


def get_unit_files(bench_path):
bench_name = get_bench_name(bench_path)
unit_files = [
[bench_name, ".target"],
[bench_name + "-workers", ".target"],
[bench_name + "-web", ".target"],
[bench_name + "-redis", ".target"],
[bench_name + "-xhiveframework-default-worker@", ".service"],
[bench_name + "-xhiveframework-short-worker@", ".service"],
[bench_name + "-xhiveframework-long-worker@", ".service"],
[bench_name + "-xhiveframework-schedule", ".service"],
[bench_name + "-xhiveframework-web", ".service"],
[bench_name + "-node-socketio", ".service"],
[bench_name + "-redis-cache", ".service"],
[bench_name + "-redis-queue", ".service"],
]
return unit_files

+ 89
- 0
bench/config/templates/502.html Zobrazit soubor

@@ -0,0 +1,89 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Sorry! We will be back soon.</title>
<style>
body {
font-family: "Helvetica Neue", Helvetica, Arial, "Open Sans", sans-serif;
color: #36414C;
font-weight: 300;
}

.page-container {
max-width: 800px;
padding: 15px;
vertical-align: middle;
position: absolute;
top: 50%;
left: 0;
right: 0;
margin: 0 auto;
transform: translate(0%, -50%);
}

.svg-container {
float: left;
width: 150px;
padding-top: 24px;
}

.message-container {
float: left;
padding-left: 15px;
font-size: 16px;
line-height: 1.6;
}

.message-container h1 {
font-size: 48px;
line-height: 1.2;
font-weight: 200;
}

.message-container .message {
color: #8D99A6;
}

.clearfix {
clear: both;
}

a {
color: #5E64FF;
}

@media (max-width: 767px) {
.svg-container {
float: none;
padding-top: 0px;
}

.message-container {
float: none;
width: 100% !important;
}
}
</style>
</head>
<body>
<div class="page-container">
<div class="svg-container">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns" viewBox="0 0 32 32" version="1.1" x="0px" y="0px"><title>sad-face-avatar-boy-man-11</title><desc>Created with Sketch.</desc><g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage"><path d="M24.5728851,23.0941141 C26.0847779,16.969087 23.5542402,12.1548858 21.4280864,11.0822754 C19.598459,12.7549171 13.9938462,17 10.2890466,17 C12.2343263,15.4384766 12.8488535,12.9394531 12.8488535,12.9394531 C12.8488535,12.9394531 10.9859461,15.7346191 7.24611965,17 C6.45154466,18.5731485 6.67682658,20.3507184 6.78659707,21.5361582 C6.84777749,21.7403698 6.95210509,22.0855229 6.99214296,22.1994274 C7.09055416,22.4793995 7.21210961,22.7924224 7.35775135,23.1301118 C7.774276,24.0958785 8.2986796,25.0616511 8.9372701,25.9603932 C10.738893,28.4959687 13.0675623,30 16,30 C18.9311427,30 21.2399187,28.4973998 23.0104666,25.9636063 C23.6381381,25.065359 24.1509411,24.1000358 24.5559252,23.1346951 C24.5616169,23.1211281 24.5672702,23.1076009 24.5728851,23.0941141 L24.5728851,23.0941141 Z M5.94669386,22.2116429 C4.61458602,20.1217921 3.13011281,13.1987617 4.62664708,8.75830078 C6.40621687,3.47802734 12.6103081,1 15.7729333,1 C18.8013894,1.00000002 21.8450169,1.93994141 23.0552307,3.80615234 C23.0552307,3.80615234 25.0915798,2.75024414 26.9020692,3.80615234 C25.0915798,4.17895508 24.887945,5.19335938 24.887945,5.19335938 C27.9234944,6.90377632 29.4577737,17.0840684 26.1082885,21.6811732 C26.0708438,21.8119773 25.9120331,22.3649335 25.857287,22.526075 C25.7549564,22.8272785 25.6289716,23.1618434 25.4780638,23.5215549 C25.0472763,24.5484017 24.5017812,25.575266 23.8301706,26.5363937 C21.888484,29.3151002 19.2996007,31 16,31 C12.7016943,31 10.0952049,29.3165313 8.12209422,26.5396068 C7.43952798,25.5789739 6.88219633,24.552559 6.43951227,23.5261382 C6.28443097,23.166562 6.15455941,22.832124 6.04872776,22.5310413 C6.02660008,22.4680898 5.98792403,22.3454665 5.94669386,22.2116429 L5.94669386,22.2116429 Z M20.6103625,20.496219 L21.7234973,21.0527864 C21.9704865,21.176281 22.0705987,21.4766175 21.9471041,21.7236068 C21.8236094,21.970596 21.5232729,22.0707082 21.2762837,21.9472136 L19.2762837,20.9472136 C18.9077594,20.7629515 18.9077594,20.2370485 19.2762837,20.0527864 L21.2762837,19.0527864 C21.5232729,18.9292918 21.8236094,19.029404 21.9471041,19.2763932 C22.0705987,19.5233825 21.9704865,19.823719 21.7234973,19.9472136 L20.6103625,20.496219 Z M11.389528,20.496219 L10.2763932,21.0527864 C10.029404,21.176281 9.92929178,21.4766175 10.0527864,21.7236068 C10.176281,21.970596 10.4766175,22.0707082 10.7236068,21.9472136 L12.7236068,20.9472136 C13.0921311,20.7629515 13.0921311,20.2370485 12.7236068,20.0527864 L10.7236068,19.0527864 C10.4766175,18.9292918 10.176281,19.029404 10.0527864,19.2763932 C9.92929178,19.5233825 10.029404,19.823719 10.2763932,19.9472136 L11.389528,20.496219 Z M14.4246316,26.7639848 C14.4725953,26.6868331 14.5938453,26.5444206 14.7863941,26.3975309 C15.1127054,26.1485979 15.512309,26 16,26 C16.487691,26 16.8872946,26.1485979 17.2136059,26.3975309 C17.4061547,26.5444206 17.5274047,26.6868331 17.5753684,26.7639848 C17.7211632,26.9985024 18.0294673,27.0704264 18.2639848,26.9246316 C18.4985024,26.7788368 18.5704264,26.4705327 18.4246316,26.2360152 C18.3171754,26.0631669 18.1191505,25.8305794 17.8201344,25.6024691 C17.3271707,25.2264021 16.7183393,25 16,25 C15.2816607,25 14.6728293,25.2264021 14.1798656,25.6024691 C13.8808495,25.8305794 13.6828246,26.0631669 13.5753684,26.2360152 C13.4295736,26.4705327 13.5014976,26.7788368 13.7360152,26.9246316 C13.9705327,27.0704264 14.2788368,26.9985024 14.4246316,26.7639848 Z" fill="#000000" sketch:type="MSShapeGroup"></path></g></svg>
</div>
<div class="message-container" style="width: calc(100% - 170px);">
<h1>
Sorry! <br>
We will be back soon.
</h1>
<p class="message">
<strong>Don't panic.</strong> It's not you, it's us.<br>
Most likely, our engineers are updating the code,
and it should take a minute for the new code to load into memory.<br><br>
Try refreshing after a minute or two.
</p>
</div>
<div class="clearfix"></div>
</div>
</body>
</html>

+ 18
- 0
bench/config/templates/Procfile Zobrazit soubor

@@ -0,0 +1,18 @@
{% if not skip_redis %}
redis_cache: redis-server config/redis_cache.conf
redis_queue: redis-server config/redis_queue.conf
{% endif %}
web: bench serve {% if webserver_port -%} --port {{ webserver_port }} {%- endif %}

socketio: {{ node }} apps/xhiveframework/socketio.js

{% if not CI %}
watch: bench watch
{% endif %}

schedule: bench schedule
worker: {{ 'OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES NO_PROXY=*' if is_mac else '' }} bench worker 1>> logs/worker.log 2>> logs/worker.error.log
{% for worker_name, worker_details in workers.items() %}
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
{% endfor %}


+ 100
- 0
bench/config/templates/bench_manager_nginx.conf Zobrazit soubor

@@ -0,0 +1,100 @@
server {
listen {{ port }};
server_name {{ domain }};
root {{ sites_path }};


{% if ssl_certificate and ssl_certificate_key %}
ssl on;
ssl_certificate {{ ssl_certificate }};
ssl_certificate_key {{ ssl_certificate_key }};
ssl_session_timeout 5m;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
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";
ssl_prefer_server_ciphers on;
{% endif %}

location /assets {
try_files $uri =404;
}

location ~ ^/protected/(.*) {
internal;
try_files /{{ bench_manager_site_name }}/$1 =404;
}

location /socket.io {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Xhiveframework-Site-Name {{ bench_manager_site_name }};
proxy_set_header Origin $scheme://$http_host;
proxy_set_header Host {{ bench_manager_site_name }};

proxy_pass http://{{ bench_name }}-socketio-server;
}

location / {
try_files /{{ bench_manager_site_name }}/public/$uri @webserver;
}

location @webserver {
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Xhiveframework-Site-Name {{ bench_manager_site_name }};
proxy_set_header Host {{ bench_manager_site_name }};
proxy_set_header X-Use-X-Accel-Redirect True;
proxy_read_timeout {{ http_timeout or 120 }};
proxy_redirect off;

proxy_pass http://{{ bench_name }}-xhiveframework;
}

# error pages
{% for error_code, error_page in error_pages.items() -%}

error_page {{ error_code }} /{{ error_page.split('/')[-1] }};
location /{{ error_code }}.html {
root {{ '/'.join(error_page.split('/')[:-1]) }};
internal;
}

{% endfor -%}

# optimizations
sendfile on;
keepalive_timeout 15;
client_max_body_size 50m;
client_body_buffer_size 16K;
client_header_buffer_size 1k;

# enable gzip compresion
# based on https://mattstauffer.co/blog/enabling-gzip-on-nginx-servers-including-laravel-forge
gzip on;
gzip_http_version 1.1;
gzip_comp_level 5;
gzip_min_length 256;
gzip_proxied any;
gzip_vary on;
gzip_types
application/atom+xml
application/javascript
application/json
application/rss+xml
application/vnd.ms-fontobject
application/x-font-ttf
application/font-woff
application/x-web-app-manifest+json
application/xhtml+xml
application/xml
font/opentype
image/svg+xml
image/x-icon
text/css
text/plain
text/x-component
;
# text/html is always compressed by HttpGzipModule
}



+ 20
- 0
bench/config/templates/frappe_sudoers Zobrazit soubor

@@ -0,0 +1,20 @@
# This file is auto-generated by xhiveframework/bench
# To re-generate this file, run "bench setup sudoers"

{% if service %}
{{ user }} ALL = (root) {{ service }}
{{ user }} ALL = (root) NOPASSWD: {{ service }} nginx *
{% endif %}

{% if systemctl %}
{{ user }} ALL = (root) {{ systemctl }}
{{ user }} ALL = (root) NOPASSWD: {{ systemctl }} * nginx
{% endif %}

{% if nginx %}
{{ user }} ALL = (root) NOPASSWD: {{ nginx }}
{% endif %}

{{ user }} ALL = (root) NOPASSWD: {{ certbot }}
Defaults:{{ user }} !requiretty


+ 19
- 0
bench/config/templates/letsencrypt.cfg Zobrazit soubor

@@ -0,0 +1,19 @@
# This is an example of the kind of things you can do in a configuration file.
# All flags used by the client can be configured here. Run Certbot with
# "--help" to learn more about the available options.

# Use a 4096 bit RSA key instead of 2048
rsa-key-size = 4096

# Uncomment and update to register with the specified e-mail address
#email = email@domain.com

# Uncomment and update to generate certificates for the specified
# domains.
domains = {{ domain }}

# Uncomment to use a text interface instead of ncurses
text = True

# Uncomment to use the standalone authenticator on port 443
authenticator = standalone

+ 237
- 0
bench/config/templates/nginx.conf Zobrazit soubor

@@ -0,0 +1,237 @@
{%- macro nginx_map(from_variable, to_variable, values, default) %}
map {{ from_variable }} {{ to_variable }} {
{% for (from, to) in values.items() -%}
{{ from }} {{ to }};
{% endfor %}

{%- if default -%}
default {{ default }};
{% endif %}
}
{%- endmacro %}

{%- macro server_block(bench_name, port, server_names, site_name, sites_path, ssl_certificate, ssl_certificate_key) %}
server {
{% if ssl_certificate and ssl_certificate_key %}
listen {{ port }} ssl;
listen [::]:{{ port }} ssl;
{% else %}
listen {{ port }};
listen [::]:{{ port }};
{% endif %}

server_name
{% for name in server_names -%}
{{ name }}
{% endfor -%}
;

root {{ sites_path }};

{% if allow_rate_limiting %}
limit_conn per_host_{{ bench_name_hash }} 8;
{% endif %}

proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;

{% if ssl_certificate and ssl_certificate_key %}
ssl_certificate {{ ssl_certificate }};
ssl_certificate_key {{ ssl_certificate_key }};
ssl_session_timeout 5m;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers EECDH+AESGCM:EDH+AESGCM;
ssl_ecdh_curve secp384r1;
ssl_prefer_server_ciphers on;
{% endif %}

add_header X-Frame-Options "SAMEORIGIN";
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload";
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header Referrer-Policy "same-origin, strict-origin-when-cross-origin";

location /assets {
try_files $uri =404;
add_header Cache-Control "max-age=31536000";
}

location ~ ^/protected/(.*) {
internal;
try_files /{{ site_name }}/$1 =404;
}

location /socket.io {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Xhiveframework-Site-Name {{ site_name }};
proxy_set_header Origin $scheme://$http_host;
proxy_set_header Host $host;

proxy_pass http://{{ bench_name }}-socketio-server;
}

location / {

rewrite ^(.+)/$ $1 permanent;
rewrite ^(.+)/index\.html$ $1 permanent;
rewrite ^(.+)\.html$ $1 permanent;

location ~* ^/files/.*.(htm|html|svg|xml) {
add_header Content-disposition "attachment";
try_files /{{ site_name }}/public/$uri @webserver;
}

try_files /{{ site_name }}/public/$uri @webserver;
}

location @webserver {
proxy_http_version 1.1;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Xhiveframework-Site-Name {{ site_name }};
proxy_set_header Host $host;
proxy_set_header X-Use-X-Accel-Redirect True;
proxy_read_timeout {{ http_timeout or 120 }};
proxy_redirect off;

proxy_pass http://{{ bench_name }}-xhiveframework;
}

# error pages
{% for error_code, error_page in error_pages.items() -%}

error_page {{ error_code }} /{{ error_page.split('/')[-1] }};
location /{{ error_code }}.html {
root {{ '/'.join(error_page.split('/')[:-1]) }};
internal;
}

{% endfor -%}

{% if logging %}
{%- if logging.level == "site" -%}

access_log /var/log/nginx/{{ site_name }}_access.log {{ logging.log_format }};
error_log /var/log/nginx/{{ site_name }}_error.log;

{%- elif logging.level == "combined" -%}

access_log /var/log/nginx/access.log {{ logging.log_format }};
error_log /var/log/nginx/error.log;

{%- endif %}
{%- endif %}

# optimizations
sendfile on;
keepalive_timeout 15;
client_max_body_size 50m;
client_body_buffer_size 16K;
client_header_buffer_size 1k;

# enable gzip compresion
# based on https://mattstauffer.co/blog/enabling-gzip-on-nginx-servers-including-laravel-forge
gzip on;
gzip_http_version 1.1;
gzip_comp_level 5;
gzip_min_length 256;
gzip_proxied any;
gzip_vary on;
gzip_types
application/atom+xml
application/javascript
application/json
application/rss+xml
application/vnd.ms-fontobject
application/x-font-ttf
application/font-woff
application/x-web-app-manifest+json
application/xhtml+xml
application/xml
font/opentype
image/svg+xml
image/x-icon
text/css
text/plain
text/x-component
;
# text/html is always compressed by HttpGzipModule
}

{% if ssl_certificate and ssl_certificate_key -%}
# http to https redirect
server {
listen 80;
server_name
{% for name in server_names -%}
{{ name }}
{% endfor -%}
;

return 301 https://$host$request_uri;
}

{% endif %}

{%- endmacro -%}

upstream {{ bench_name }}-xhiveframework {
server 127.0.0.1:{{ webserver_port or 8000 }} fail_timeout=0;
}

upstream {{ bench_name}}-socketio-server {
server 127.0.0.1:{{ socketio_port or 3000 }} fail_timeout=0;
}

{% if allow_rate_limiting %}
limit_conn_zone $host zone=per_host_{{ bench_name_hash }}:{{ limit_conn_shared_memory }}m;
{% endif %}

# setup maps
{%- set site_name_variable="$host" %}
{% if sites.domain_map -%}
{# we append these variables with a random string as there could be multiple benches #}
{%- set site_name_variable="$site_name_{0}".format(random_string) -%}
{{ nginx_map(from_variable="$host", to_variable=site_name_variable, values=sites.domain_map, default="$host") }}
{%- endif %}

# server blocks
{% if sites.that_use_dns -%}

{{ server_block(bench_name, port=80, server_names=sites.that_use_dns, site_name=site_name_variable, sites_path=sites_path) }}

{%- endif %}

{% if sites.that_use_wildcard_ssl -%}

{{ server_block(bench_name, port=443, server_names=sites.that_use_wildcard_ssl,
site_name=site_name_variable, sites_path=sites_path,
ssl_certificate=sites.wildcard_ssl_certificate,
ssl_certificate_key=sites.wildcard_ssl_certificate_key) }}

{%- endif %}

{%- if sites.that_use_ssl -%}
{% for site in sites.that_use_ssl -%}

{{ server_block(bench_name, port=443, server_names=[site.domain or site.name],
site_name=site_name_variable, sites_path=sites_path,
ssl_certificate=site.ssl_certificate, ssl_certificate_key=site.ssl_certificate_key) }}

{% endfor %}
{%- endif %}

{% if sites.that_use_port -%}
{%- for site in sites.that_use_port -%}

{{ server_block(bench_name, port=site.port, server_names=[site.name], site_name=site.name, sites_path=sites_path) }}

{%- endfor %}
{% endif %}

+ 46
- 0
bench/config/templates/nginx_default.conf Zobrazit soubor

@@ -0,0 +1,46 @@
# For more information on configuration, see:
# * Official English Documentation: http://nginx.org/en/docs/
# * Official Russian Documentation: http://nginx.org/ru/docs/

user nginx;
worker_processes 1;

error_log /var/log/nginx/error.log;
#error_log /var/log/nginx/error.log notice;
#error_log /var/log/nginx/error.log info;

pid /run/nginx.pid;


events {
worker_connections 1024;
}


http {
include /etc/nginx/mime.types;
default_type application/octet-stream;

log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';

access_log /var/log/nginx/access.log main;

sendfile on;
#tcp_nopush on;

#keepalive_timeout 0;
keepalive_timeout 65;

server_names_hash_bucket_size 64;

#gzip on;

index index.html index.htm;

# Load modular configuration files from the /etc/nginx/conf.d directory.
# See http://nginx.org/en/docs/ngx_core_module.html#include
# for more information.
include /etc/nginx/conf.d/*.conf;
}

+ 14
- 0
bench/config/templates/redis_cache.conf Zobrazit soubor

@@ -0,0 +1,14 @@
dbfilename redis_cache.rdb
dir {{ pid_path }}
pidfile {{ pid_path }}/redis_cache.pid
bind 127.0.0.1
port {{ port }}
maxmemory {{ maxmemory }}mb
maxmemory-policy allkeys-lru
appendonly no
{% if redis_version and redis_version >= 2.2 %}
save ""
{% endif %}
{% if redis_version and redis_version >= 6.0 %}
aclfile {{ config_path }}/redis_cache.acl
{% endif %}

+ 8
- 0
bench/config/templates/redis_queue.conf Zobrazit soubor

@@ -0,0 +1,8 @@
dbfilename redis_queue.rdb
dir {{ pid_path }}
pidfile {{ pid_path }}/redis_queue.pid
bind 127.0.0.1
port {{ port }}
{% if redis_version and redis_version >= 6.0 %}
aclfile {{ config_path }}/redis_queue.acl
{% endif %}

+ 151
- 0
bench/config/templates/supervisor.conf Zobrazit soubor

@@ -0,0 +1,151 @@
; Notes:
; priority=1 --> Lower priorities indicate programs that start first and shut down last
; killasgroup=true --> send kill signal to child processes too

; graceful timeout should always be lower than stopwaitsecs to avoid orphan gunicorn workers.
[program:{{ bench_name }}-xhiveframework-web]
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 xhiveframework.app:application --preload
priority=4
autostart=true
autorestart=true
stdout_logfile={{ bench_dir }}/logs/web.log
stderr_logfile={{ bench_dir }}/logs/web.error.log
stopwaitsecs=40
killasgroup=true
user={{ user }}
directory={{ sites_dir }}
startretries={{ supervisor_startretries }}

[program:{{ bench_name }}-xhiveframework-schedule]
command={{ bench_cmd }} schedule
priority=3
autostart=true
autorestart=true
stdout_logfile={{ bench_dir }}/logs/schedule.log
stderr_logfile={{ bench_dir }}/logs/schedule.error.log
user={{ user }}
directory={{ bench_dir }}
startretries={{ supervisor_startretries }}

{% if not multi_queue_consumption %}
[program:{{ bench_name }}-xhiveframework-default-worker]
command={{ bench_cmd }} worker --queue default
priority=4
autostart=true
autorestart=true
stdout_logfile={{ bench_dir }}/logs/worker.log
stderr_logfile={{ bench_dir }}/logs/worker.error.log
user={{ user }}
stopwaitsecs=1560
directory={{ bench_dir }}
killasgroup=true
numprocs={{ background_workers }}
process_name=%(program_name)s-%(process_num)d
startretries={{ supervisor_startretries }}
{% endif %}

[program:{{ bench_name }}-xhiveframework-short-worker]
command={{ bench_cmd }} worker --queue short{{',default' if multi_queue_consumption else ''}}
priority=4
autostart=true
autorestart=true
stdout_logfile={{ bench_dir }}/logs/worker.log
stderr_logfile={{ bench_dir }}/logs/worker.error.log
user={{ user }}
stopwaitsecs=360
directory={{ bench_dir }}
killasgroup=true
numprocs={{ background_workers }}
process_name=%(program_name)s-%(process_num)d
startretries={{ supervisor_startretries }}

[program:{{ bench_name }}-xhiveframework-long-worker]
command={{ bench_cmd }} worker --queue long{{',default,short' if multi_queue_consumption else ''}}
priority=4
autostart=true
autorestart=true
stdout_logfile={{ bench_dir }}/logs/worker.log
stderr_logfile={{ bench_dir }}/logs/worker.error.log
user={{ user }}
stopwaitsecs=1560
directory={{ bench_dir }}
killasgroup=true
numprocs={{ background_workers }}
process_name=%(program_name)s-%(process_num)d
startretries={{ supervisor_startretries }}

{% for worker_name, worker_details in workers.items() %}
[program:{{ bench_name }}-xhiveframework-{{ worker_name }}-worker]
command={{ bench_cmd }} worker --queue {{ worker_name }}
priority=4
autostart=true
autorestart=true
stdout_logfile={{ bench_dir }}/logs/worker.log
stderr_logfile={{ bench_dir }}/logs/worker.error.log
user={{ user }}
stopwaitsecs={{ worker_details["timeout"] }}
directory={{ bench_dir }}
killasgroup=true
numprocs={{ worker_details["background_workers"] or background_workers }}
process_name=%(program_name)s-%(process_num)d
startretries={{ supervisor_startretries }}
{% endfor %}


{% if not skip_redis %}
[program:{{ bench_name }}-redis-cache]
command={{ redis_server }} {{ redis_cache_config }}
priority=1
autostart=true
autorestart=true
stdout_logfile={{ bench_dir }}/logs/redis-cache.log
stderr_logfile={{ bench_dir }}/logs/redis-cache.error.log
user={{ user }}
directory={{ sites_dir }}
startretries={{ supervisor_startretries }}

[program:{{ bench_name }}-redis-queue]
command={{ redis_server }} {{ redis_queue_config }}
priority=1
autostart=true
autorestart=true
stdout_logfile={{ bench_dir }}/logs/redis-queue.log
stderr_logfile={{ bench_dir }}/logs/redis-queue.error.log
user={{ user }}
directory={{ sites_dir }}
startretries={{ supervisor_startretries }}
{% endif %}

{% if node %}
[program:{{ bench_name }}-node-socketio]
command={{ node }} {{ bench_dir }}/apps/xhiveframework/socketio.js
priority=4
autostart=true
autorestart=true
stdout_logfile={{ bench_dir }}/logs/node-socketio.log
stderr_logfile={{ bench_dir }}/logs/node-socketio.error.log
user={{ user }}
directory={{ bench_dir }}
startretries={{ supervisor_startretries }}
{% endif %}

[group:{{ bench_name }}-web]
programs={{ bench_name }}-xhiveframework-web {%- if node -%} ,{{ bench_name }}-node-socketio {%- endif%}


{% if multi_queue_consumption %}

[group:{{ bench_name }}-workers]
programs={{ bench_name }}-xhiveframework-schedule,{{ bench_name }}-xhiveframework-short-worker,{{ bench_name }}-xhiveframework-long-worker{%- for worker_name in workers -%},{{ bench_name }}-xhiveframework-{{ worker_name }}-worker{%- endfor %}

{% else %}

[group:{{ bench_name }}-workers]
programs={{ bench_name }}-xhiveframework-schedule,{{ bench_name }}-xhiveframework-default-worker,{{ bench_name }}-xhiveframework-short-worker,{{ bench_name }}-xhiveframework-long-worker{%- for worker_name in workers -%},{{ bench_name }}-xhiveframework-{{ worker_name }}-worker{%- endfor %}

{% endif %}

{% if not skip_redis %}
[group:{{ bench_name }}-redis]
programs={{ bench_name }}-redis-cache,{{ bench_name }}-redis-queue
{% endif %}

+ 12
- 0
bench/config/templates/systemd/frappe-bench-frappe-default-worker.service Zobrazit soubor

@@ -0,0 +1,12 @@
[Unit]
Description="{{ bench_name }}-xhiveframework-default-worker %I"
PartOf={{ bench_name }}-workers.target

[Service]
User={{ user }}
Group={{ user }}
Restart=always
ExecStart={{ bench_cmd }} worker --queue default
StandardOutput=file:{{ bench_dir }}/logs/worker.log
StandardError=file:{{ bench_dir }}/logs/worker.error.log
WorkingDirectory={{ bench_dir }}

+ 12
- 0
bench/config/templates/systemd/frappe-bench-frappe-long-worker.service Zobrazit soubor

@@ -0,0 +1,12 @@
[Unit]
Description="{{ bench_name }}-xhiveframework-short-worker %I"
PartOf={{ bench_name }}-workers.target

[Service]
User={{ user }}
Group={{ user }}
Restart=always
ExecStart={{ bench_cmd }} worker --queue long
StandardOutput=file:{{ bench_dir }}/logs/worker.log
StandardError=file:{{ bench_dir }}/logs/worker.error.log
WorkingDirectory={{ bench_dir }}

+ 12
- 0
bench/config/templates/systemd/frappe-bench-frappe-schedule.service Zobrazit soubor

@@ -0,0 +1,12 @@
[Unit]
Description="{{ bench_name }}-xhiveframework-schedule"
PartOf={{ bench_name }}-workers.target

[Service]
User={{ user }}
Group={{ user }}
Restart=always
ExecStart={{ bench_cmd }} schedule
StandardOutput=file:{{ bench_dir }}/logs/schedule.log
StandardError=file:{{ bench_dir }}/logs/schedule.error.log
WorkingDirectory={{ bench_dir }}

+ 12
- 0
bench/config/templates/systemd/frappe-bench-frappe-short-worker.service Zobrazit soubor

@@ -0,0 +1,12 @@
[Unit]
Description="{{ bench_name }}-xhiveframework-short-worker %I"
PartOf={{ bench_name }}-workers.target

[Service]
User={{ user }}
Group={{ user }}
Restart=always
ExecStart={{ bench_cmd }} worker --queue short
StandardOutput=file:{{ bench_dir }}/logs/worker.log
StandardError=file:{{ bench_dir }}/logs/worker.error.log
WorkingDirectory={{ bench_dir }}

+ 12
- 0
bench/config/templates/systemd/frappe-bench-frappe-web.service Zobrazit soubor

@@ -0,0 +1,12 @@
[Unit]
Description="{{ bench_name }}-xhiveframework-web"
PartOf={{ bench_name }}-web.target

[Service]
User={{ user }}
Group={{ user }}
Restart=always
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 }} xhiveframework.app:application --preload
StandardOutput=file:{{ bench_dir }}/logs/web.log
StandardError=file:{{ bench_dir }}/logs/web.error.log
WorkingDirectory={{ sites_dir }}

+ 13
- 0
bench/config/templates/systemd/frappe-bench-node-socketio.service Zobrazit soubor

@@ -0,0 +1,13 @@
[Unit]
After={{ bench_name }}-xhiveframework-web.service
Description="{{ bench_name }}-node-socketio"
PartOf={{ bench_name }}-web.target

[Service]
User={{ user }}
Group={{ user }}
Restart=always
ExecStart={{ node }} {{ bench_dir }}/apps/xhiveframework/socketio.js
StandardOutput=file:{{ bench_dir }}/logs/node-socketio.log
StandardError=file:{{ bench_dir }}/logs/node-socketio.error.log
WorkingDirectory={{ bench_dir }}

+ 12
- 0
bench/config/templates/systemd/frappe-bench-redis-cache.service Zobrazit soubor

@@ -0,0 +1,12 @@
[Unit]
Description="{{ bench_name }}-redis-cache"
PartOf={{ bench_name }}-redis.target

[Service]
User={{ user }}
Group={{ user }}
Restart=always
ExecStart={{ redis_server }} {{ redis_cache_config }}
StandardOutput=file:{{ bench_dir }}/logs/redis-cache.log
StandardError=file:{{ bench_dir }}/logs/redis-cache.error.log
WorkingDirectory={{ sites_dir }}

+ 12
- 0
bench/config/templates/systemd/frappe-bench-redis-queue.service Zobrazit soubor

@@ -0,0 +1,12 @@
[Unit]
Description="{{ bench_name }}-redis-queue"
PartOf={{ bench_name }}-redis.target

[Service]
User={{ user }}
Group={{ user }}
Restart=always
ExecStart={{ redis_server }} {{ redis_queue_config }}
StandardOutput=file:{{ bench_dir }}/logs/redis-queue.log
StandardError=file:{{ bench_dir }}/logs/redis-queue.error.log
WorkingDirectory={{ sites_dir }}

+ 6
- 0
bench/config/templates/systemd/frappe-bench-redis.target Zobrazit soubor

@@ -0,0 +1,6 @@
[Unit]
After=network.target
Wants={{ bench_name }}-redis-cache.service {{ bench_name }}-redis-queue.service

[Install]
WantedBy=multi-user.target

+ 6
- 0
bench/config/templates/systemd/frappe-bench-web.target Zobrazit soubor

@@ -0,0 +1,6 @@
[Unit]
After=network.target
Wants={{ bench_name }}-xhiveframework-web.service {{ bench_name }}-node-socketio.service

[Install]
WantedBy=multi-user.target

+ 6
- 0
bench/config/templates/systemd/frappe-bench-workers.target Zobrazit soubor

@@ -0,0 +1,6 @@
[Unit]
After=network.target
Wants={{ worker_target_wants }}

[Install]
WantedBy=multi-user.target

+ 6
- 0
bench/config/templates/systemd/frappe-bench.target Zobrazit soubor

@@ -0,0 +1,6 @@
[Unit]
After=network.target
Requires={{ bench_name }}-web.target {{ bench_name }}-workers.target {{ bench_name }}-redis.target

[Install]
WantedBy=multi-user.target

+ 42
- 0
bench/exceptions.py Zobrazit soubor

@@ -0,0 +1,42 @@
class InvalidBranchException(Exception):
pass


class InvalidRemoteException(Exception):
pass


class PatchError(Exception):
pass


class CommandFailedError(Exception):
pass


class BenchNotFoundError(Exception):
pass


class ValidationError(Exception):
pass


class AppNotInstalledError(ValidationError):
pass


class CannotUpdateReleaseBench(ValidationError):
pass


class FeatureDoesNotExistError(CommandFailedError):
pass


class NotInBenchDirectoryError(Exception):
pass


class VersionNotFound(Exception):
pass

+ 38
- 0
bench/patches/__init__.py Zobrazit soubor

@@ -0,0 +1,38 @@
import os
import importlib


def run(bench_path):
source_patch_file = os.path.join(
os.path.dirname(os.path.abspath(__file__)), "patches.txt"
)
target_patch_file = os.path.join(os.path.abspath(bench_path), "patches.txt")

with open(source_patch_file) as f:
patches = [
p.strip()
for p in f.read().splitlines()
if p.strip() and not p.strip().startswith("#")
]

executed_patches = []
if os.path.exists(target_patch_file):
with open(target_patch_file) as f:
executed_patches = f.read().splitlines()

try:
for patch in patches:
if patch not in executed_patches:
module = importlib.import_module(patch.split()[0])
execute = getattr(module, "execute")
result = execute(bench_path)

if not result:
executed_patches.append(patch)

finally:
with open(target_patch_file, "w") as f:
f.write("\n".join(executed_patches))

# end with an empty line
f.write("\n")

+ 10
- 0
bench/patches/patches.txt Zobrazit soubor

@@ -0,0 +1,10 @@
bench.patches.v3.deprecate_old_config
bench.patches.v3.celery_to_rq
bench.patches.v3.redis_bind_ip
bench.patches.v4.update_node
bench.patches.v4.update_socketio
bench.patches.v4.install_yarn #2
bench.patches.v5.fix_user_permissions
bench.patches.v5.fix_backup_cronjob
bench.patches.v5.set_live_reload_config
bench.patches.v5.update_archived_sites

+ 0
- 0
bench/patches/v5/__init__.py Zobrazit soubor


+ 15
- 0
bench/patches/v5/fix_backup_cronjob.py Zobrazit soubor

@@ -0,0 +1,15 @@
from bench.config.common_site_config import get_config
from crontab import CronTab


def execute(bench_path):
"""
This patch fixes a cron job that would backup sites every minute per 6 hours
"""

user = get_config(bench_path=bench_path).get("xhiveframework_user")
user_crontab = CronTab(user=user)

for job in user_crontab.find_comment("bench auto backups set for every 6 hours"):
job.every(6).hours()
user_crontab.write()

+ 61
- 0
bench/patches/v5/fix_user_permissions.py Zobrazit soubor

@@ -0,0 +1,61 @@
# imports - standard imports
import getpass
import os
import subprocess

# imports - module imports
from bench.cli import change_uid_msg
from bench.config.production_setup import get_supervisor_confdir, is_centos7, service
from bench.config.common_site_config import get_config
from bench.utils import exec_cmd, get_bench_name, get_cmd_output


def is_sudoers_set():
"""Check if bench sudoers is set"""
cmd = ["sudo", "-n", "bench"]
bench_warn = False

with open(os.devnull, "wb") as f:
return_code_check = not subprocess.call(cmd, stdout=f)

if return_code_check:
try:
bench_warn = change_uid_msg in get_cmd_output(cmd, _raise=False)
except subprocess.CalledProcessError:
bench_warn = False
finally:
return_code_check = return_code_check and bench_warn

return return_code_check


def is_production_set(bench_path):
"""Check if production is set for current bench"""
production_setup = False
bench_name = get_bench_name(bench_path)

supervisor_conf_extn = "ini" if is_centos7() else "conf"
supervisor_conf_file_name = f"{bench_name}.{supervisor_conf_extn}"
supervisor_conf = os.path.join(get_supervisor_confdir(), supervisor_conf_file_name)

if os.path.exists(supervisor_conf):
production_setup = production_setup or True

nginx_conf = f"/etc/nginx/conf.d/{bench_name}.conf"

if os.path.exists(nginx_conf):
production_setup = production_setup or True

return production_setup


def execute(bench_path):
"""This patch checks if bench sudoers is set and regenerate supervisor and sudoers files"""
user = get_config(".").get("xhiveframework_user") or getpass.getuser()

if is_sudoers_set():
if is_production_set(bench_path):
exec_cmd(f"sudo bench setup supervisor --yes --user {user}")
service("supervisord", "restart")

exec_cmd(f"sudo bench setup sudoers {user}")

+ 5
- 0
bench/patches/v5/set_live_reload_config.py Zobrazit soubor

@@ -0,0 +1,5 @@
from bench.config.common_site_config import update_config


def execute(bench_path):
update_config({"live_reload": True}, bench_path)

+ 52
- 0
bench/patches/v5/update_archived_sites.py Zobrazit soubor

@@ -0,0 +1,52 @@
"""
Deprecate archived_sites folder for consistency. This change is
only for Xhiveframework v14 benches. If not a v14 bench yet, skip this
patch and try again later.

1. Rename folder `./archived_sites` to `./archived/sites`
2. Create a symlink `./archived_sites` => `./archived/sites`

Corresponding changes in xhiveframework/xhiveframework via https://lab.membtech.com/xhiveframework/xhiveframework15/pull/15060
"""
import os
from pathlib import Path

import click
from bench.utils.app import get_current_version
from semantic_version import Version


def execute(bench_path):
xhiveframework_version = Version(get_current_version("xhiveframework"))

if xhiveframework_version.major < 14 or os.name != "posix":
# Returning False means patch has been skipped
return False

pre_patch_dir = os.getcwd()
old_directory = Path(bench_path, "archived_sites")
new_directory = Path(bench_path, "archived", "sites")

if not old_directory.exists():
return False

if old_directory.is_symlink():
return True

os.chdir(bench_path)

if not os.path.exists(new_directory):
os.makedirs(new_directory)

old_directory.rename(new_directory)

click.secho(f"Archived sites are now stored under {new_directory}")

if not os.listdir(old_directory):
os.rmdir(old_directory)

os.symlink(new_directory, old_directory)

click.secho(f"Symlink {old_directory} that points to {new_directory}")

os.chdir(pre_patch_dir)

+ 10
- 0
bench/playbooks/README.md Zobrazit soubor

@@ -0,0 +1,10 @@
# Deploying a, developer/production-ready XhiveERP website with Ansible

## Supported Platforms
- Debian 8, 9
- Ubuntu 14.04, 16.04
- CentOS 7

## Notes for maintainers
- For MariaDB playbooks refer https://github.com/PCextreme/ansible-role-mariadb
- Any changes made in relation to a role should be dont inside the role and not outside it

+ 29
- 0
bench/playbooks/create_user.yml Zobrazit soubor

@@ -0,0 +1,29 @@
---

- hosts: localhost
become: yes
become_user: root
tasks:
- name: Create user
user:
name: '{{ xhiveframework_user }}'
generate_ssh_key: yes

- name: Set home folder perms
file:
path: '{{ user_directory }}'
mode: 'o+rx'
owner: '{{ xhiveframework_user }}'
group: '{{ xhiveframework_user }}'
recurse: yes

- name: Set /tmp/.bench folder perms
file:
path: '{{ repo_path }}'
owner: '{{ xhiveframework_user }}'
group: '{{ xhiveframework_user }}'
recurse: yes

- name: Change default shell to bash
shell: "chsh {{ xhiveframework_user }} -s $(which bash)"
...

+ 41
- 0
bench/playbooks/macosx.yml Zobrazit soubor

@@ -0,0 +1,41 @@
---
- hosts: localhost
become: yes
become_user: root

vars:
bench_repo_path: "/Users/{{ ansible_user_id }}/.bench"
bench_path: "/Users/{{ ansible_user_id }}/xhiveframework-bench"

tasks:
- name: install prequisites
homebrew:
name:
- cmake
- redis
- mariadb
- nodejs
state: present

- name: install wkhtmltopdf
homebrew_cask:
name:
- wkhtmltopdf
state: present

- name: configure mariadb
include: roles/mariadb/tasks/main.yml
vars:
mysql_conf_tpl: roles/mariadb/files/mariadb_config.cnf

- name: Install MySQLdb in global env
pip: name=mysql-python version=1.2.5

# setup xhiveframework-bench
- include: includes/setup_bench.yml

# setup development environment
- include: includes/setup_dev_env.yml
when: not production

...

+ 8
- 0
bench/playbooks/roles/bash_screen_wall/files/screen_wall.sh Zobrazit soubor

@@ -0,0 +1,8 @@
if [ $TERM != 'screen' ]
then
PS1='HEY! USE SCREEN '$PS1
fi

sw() {
screen -x $1 || screen -S $1
}

+ 4
- 0
bench/playbooks/roles/bash_screen_wall/tasks/main.yml Zobrazit soubor

@@ -0,0 +1,4 @@
---
- name: Setup bash screen wall
copy: src=screen_wall.sh dest=/etc/profile.d/screen_wall.sh
...

+ 20
- 0
bench/playbooks/roles/bench/tasks/change_ssh_port.yml Zobrazit soubor

@@ -0,0 +1,20 @@
---
- name: Change ssh port
gather_facts: false
hosts: localhost
user: root
tasks:
- name: change sshd config
lineinfile: >
dest=/etc/ssh/sshd_config
regexp="^Port"
line="Port {{ ssh_port }}"
state=present

- name: restart ssh
service: name=sshd state=reloaded

- name: Change ansible ssh port to 2332
set_fact:
ansible_ssh_port: '{{ ssh_port }}'
...

+ 82
- 0
bench/playbooks/roles/bench/tasks/main.yml Zobrazit soubor

@@ -0,0 +1,82 @@
---
- name: Check if /tmp/.bench exists
stat:
path: /tmp/.bench
register: tmp_bench

- name: Check if bench_repo_path exists
stat:
path: '{{ bench_repo_path }}'
register: bench_repo_register

- name: move /tmp/.bench if it exists
command: 'cp -R /tmp/.bench {{ bench_repo_path }}'
when: tmp_bench.stat.exists and not bench_repo_register.stat.exists

- name: install bench
pip:
name: '{{ bench_repo_path }}'
extra_args: '-e'
become: yes
become_user: root

- name: Overwrite bench if required
file:
state: absent
path: "{{ bench_path }}"
when: overwrite

- name: Check whether bench exists
stat:
path: "{{ bench_path }}"
register: bench_stat

- name: Fix permissions
become_user: root
command: chown {{ xhiveframework_user }} -R {{ user_directory }}

- name: python3 bench init for develop
command: bench init {{ bench_path }} --xhiveframework-path {{ xhiveframework_repo_url }} --xhiveframework-branch {{ xhiveframework_branch }} --python {{ python }}
args:
creates: "{{ bench_path }}"
when: not bench_stat.stat.exists and not production

- name: python3 bench init for production
command: bench init {{ bench_path }} --xhiveframework-path {{ xhiveframework_repo_url }} --xhiveframework-branch {{ xhiveframework_branch }} --python {{ python }}
args:
creates: "{{ bench_path }}"
when: not bench_stat.stat.exists and production

# setup common_site_config
- name: setup config
command: bench setup config
args:
creates: "{{ bench_path }}/sites/common_site_config.json"
chdir: "{{ bench_path }}"

- include_tasks: setup_inputrc.yml

# Setup Procfile
- name: Setup Procfile
command: bench setup procfile
args:
creates: "{{ bench_path }}/Procfile"
chdir: "{{ bench_path }}"

# Setup Redis env for RQ
- name: Setup Redis
command: bench setup redis
args:
creates: "{{ bench_path }}/config/redis_socketio.conf"
chdir: "{{ bench_path }}"

# Setup an XhiveERP site
- include_tasks: setup_xhiveerp.yml
when: not run_travis

# Setup Bench for production environment
- include_tasks: setup_bench_production.yml
vars:
bench_path: "{{ user_directory }}/{{ bench_name }}"
when: not run_travis and production
...

+ 28
- 0
bench/playbooks/roles/bench/tasks/setup_bench_production.yml Zobrazit soubor

@@ -0,0 +1,28 @@
---
- name: Setup production
become: yes
become_user: root
command: bench setup production {{ xhiveframework_user }} --yes
args:
chdir: '{{ bench_path }}'

- name: Setup Sudoers
become: yes
become_user: root
command: bench setup sudoers {{ xhiveframework_user }}
args:
chdir: '{{ bench_path }}'

- name: Set correct permissions on bench.log
file:
path: '{{ bench_path }}/logs/bench.log'
owner: '{{ xhiveframework_user }}'
group: '{{ xhiveframework_user }}'
become: yes
become_user: root

- name: Restart the bench
command: bench restart
args:
chdir: '{{ bench_path }}'
...

+ 29
- 0
bench/playbooks/roles/bench/tasks/setup_erpnext.yml Zobrazit soubor

@@ -0,0 +1,29 @@
---
- name: Check if XhiveERP App exists
stat: path="{{ bench_path }}/apps/xhiveerp"
register: app

- name: Get the XhiveERP app
command: bench get-app xhiveerp {{ xhiveerp_repo_url }} --branch {{ xhiveerp_branch }}
args:
creates: "{{ bench_path }}/apps/xhiveerp"
chdir: "{{ bench_path }}"
when: not app.stat.exists and not without_xhiveerp

- name: Check whether the site already exists
stat: path="{{ bench_path }}/sites/{{ site }}"
register: site_folder
when: not without_site

- name: Create a new site
command: "bench new-site {{ site }} --admin-password '{{ admin_password }}' --mariadb-root-password '{{ mysql_root_password }}'"
args:
chdir: "{{ bench_path }}"
when: not without_site and not site_folder.stat.exists

- name: Install XhiveERP to default site
command: "bench --site {{ site }} install-app xhiveerp"
args:
chdir: "{{ bench_path }}"
when: not without_site and not without_xhiveerp
...

+ 53
- 0
bench/playbooks/roles/bench/tasks/setup_firewall.yml Zobrazit soubor

@@ -0,0 +1,53 @@
---
- name: Setup Firewall
user: root
hosts: localhost

tasks:
# For CentOS
- name: Enable SELinux
selinux: policy=targeted state=permissive
when: ansible_distribution == 'CentOS'

- name: Install firewalld
yum: name=firewalld state=present
when: ansible_distribution == 'CentOS'

- name: Enable Firewall
service: name=firewalld state=started enabled=yes
when: ansible_distribution == 'CentOS'

- name: Add firewall rules
firewalld: port={{ item }}/tcp permanent=true state=enabled
with_items:
- 80
- 443
- "{{ ssh_port }}"
when: ansible_distribution == 'CentOS'

- name: Restart Firewall
service: name=firewalld state=restarted enabled=yes
when: ansible_distribution == 'CentOS'

# For Ubuntu / Debian
- name: Install ufw
apt:
state: present
force: yes
pkg:
- python-selinux
- ufw
when: ansible_distribution == 'Ubuntu' or ansible_distribution == 'Debian'

- name: Enable Firewall
ufw: state=enabled policy=deny
when: ansible_distribution == 'Ubuntu' or ansible_distribution == 'Debian'

- name: Add firewall rules
ufw: rule=allow proto=tcp port={{ item }}
with_items:
- 80
- 443
- "{{ ssh_port }}"
when: ansible_distribution == 'Ubuntu' or ansible_distribution == 'Debian'
...

+ 11
- 0
bench/playbooks/roles/bench/tasks/setup_inputrc.yml Zobrazit soubor

@@ -0,0 +1,11 @@
---
- name: insert/update inputrc for history
blockinfile:
dest: "{{ user_directory }}/.inputrc"
create: yes
block: |
## arrow up
"\e[A":history-search-backward
## arrow down
"\e[B":history-search-forward
...

+ 54
- 0
bench/playbooks/roles/common/tasks/debian.yml Zobrazit soubor

@@ -0,0 +1,54 @@
---

- name: Setup OpenSSL dependancy
pip: name=pyOpenSSL version=16.2.0

- name: install pillow prerequisites for Debian < 8
apt:
pkg:
- libjpeg8-dev
- libtiff4-dev
- tcl8.5-dev
- tk8.5-dev
state: present
when: ansible_distribution_version is version_compare('8', 'lt')

- name: install pillow prerequisites for Debian 8
apt:
pkg:
- libjpeg62-turbo-dev
- libtiff5-dev
- tcl8.5-dev
- tk8.5-dev
state: present
when: ansible_distribution_version is version_compare('8', 'eq')

- name: install pillow prerequisites for Debian 9
apt:
pkg:
- libjpeg62-turbo-dev
- libtiff5-dev
- tcl8.5-dev
- tk8.5-dev
state: present
when: ansible_distribution_version is version_compare('9', 'eq')


- name: install pillow prerequisites for Debian >= 10
apt:
pkg:
- libjpeg62-turbo-dev
- libtiff5-dev
- tcl8.6-dev
- tk8.6-dev
state: present
when: ansible_distribution_version is version_compare('10', 'ge')

- name: install pdf prerequisites debian
apt:
pkg:
- libssl-dev
state: present
force: yes

...

+ 44
- 0
bench/playbooks/roles/common/tasks/debian_family.yml Zobrazit soubor

@@ -0,0 +1,44 @@
---

- name: Install prerequisites using apt-get
become: yes
become_user: root
apt:
pkg:
- dnsmasq
- fontconfig
- git # Version control
- htop # Server stats
- libcrypto++-dev
- libfreetype6-dev
- liblcms2-dev
- libwebp-dev
- libxext6
- libxrender1
- libxslt1-dev
- libxslt1.1
- libffi-dev
- ntp # Clock synchronization
- postfix # Mail Server
- python3-dev # Installing python developer suite
- python-tk
- screen # To aid ssh sessions with connectivity problems
- vim # Is that supposed to be a question!?
- xfonts-75dpi
- xfonts-base
- zlib1g-dev
- apt-transport-https
- libsasl2-dev
- libldap2-dev
- libcups2-dev
- pv # Show progress during database restore
state: present
force: yes

- include_tasks: debian.yml
when: ansible_distribution == 'Debian'

- include_tasks: ubuntu.yml
when: ansible_distribution == 'Ubuntu'

...

+ 39
- 0
bench/playbooks/roles/common/tasks/macos.yml Zobrazit soubor

@@ -0,0 +1,39 @@
---

- hosts: localhost
become: yes
become_user: root
vars:
bench_repo_path: "/Users/{{ ansible_user_id }}/.bench"
bench_path: "/Users/{{ ansible_user_id }}/xhiveframework-bench"
tasks:
# install pre-requisites
- name: install prequisites
homebrew:
name:
- cmake
- redis
- mariadb
- nodejs
state: present

# install wkhtmltopdf
- name: cask installs
homebrew_cask:
name:
- wkhtmltopdf
state: present

- name: configure mariadb
include_tasks: roles/mariadb/tasks/main.yml
vars:
mysql_conf_tpl: roles/mariadb/files/mariadb_config.cnf

# setup xhiveframework-bench
- include_tasks: includes/setup_bench.yml

# setup development environment
- include_tasks: includes/setup_dev_env.yml
when: not production

...

+ 9
- 0
bench/playbooks/roles/common/tasks/main.yml Zobrazit soubor

@@ -0,0 +1,9 @@
---
# Install's prerequisites, like fonts, image libraries, vim, screen, python3-dev

- include_tasks: debian_family.yml
when: ansible_os_family == 'Debian'

- include_tasks: redhat_family.yml
when: ansible_os_family == "RedHat"
...

+ 52
- 0
bench/playbooks/roles/common/tasks/redhat_family.yml Zobrazit soubor

@@ -0,0 +1,52 @@
---

- name: Install IUS repo for python 3.6
become: yes
become_user: root
yum:
name: https://repo.ius.io/ius-release-el7.rpm
state: present

- name: "Setup prerequisites using yum"
become: yes
become_user: root
yum:
name:
- bzip2-devel
- cronie
- dnsmasq
- freetype-devel
- git
- htop
- lcms2-devel
- libjpeg-devel
- libtiff-devel
- libffi-devel
- libwebp-devel
- libXext
- libXrender
- libzip-devel
- libffi-devel
- ntp
- openssl-devel
- postfix
- python36u
- python-devel
- python-setuptools
- python-pip
- redis
- screen
- sudo
- tcl-devel
- tk-devel
- vim
- which
- xorg-x11-fonts-75dpi
- xorg-x11-fonts-Type1
- zlib-devel
- openssl-devel
- openldap-devel
- libselinux-python
- cups-libs
state: present
...

+ 41
- 0
bench/playbooks/roles/common/tasks/ubuntu.yml Zobrazit soubor

@@ -0,0 +1,41 @@
---

- name: install pillow prerequisites for Ubuntu < 14.04
apt:
pkg:
- libjpeg8-dev
- libtiff4-dev
- tcl8.5-dev
- tk8.5-dev
state: present
force: yes
when: ansible_distribution_version is version_compare('14.04', 'lt')

- name: install pillow prerequisites for Ubuntu >= 14.04
apt:
pkg:
- libjpeg8-dev
- libtiff5-dev
- tcl8.6-dev
- tk8.6-dev
state: present
force: yes
when: ansible_distribution_version is version_compare('14.04', 'ge')

- name: install pdf prerequisites for Ubuntu < 18.04
apt:
pkg:
- libssl-dev
state: present
force: yes
when: ansible_distribution_version is version_compare('18.04', 'lt')

- name: install pdf prerequisites for Ubuntu >= 18.04
apt:
pkg:
- libssl1.1
state: present
force: yes
when: ansible_distribution_version is version_compare('18.04', 'ge')

...

+ 4
- 0
bench/playbooks/roles/dns_caching/handlers/main.yml Zobrazit soubor

@@ -0,0 +1,4 @@
---
- name: restart network manager
service: name=NetworkManager state=restarted
...

+ 20
- 0
bench/playbooks/roles/dns_caching/tasks/main.yml Zobrazit soubor

@@ -0,0 +1,20 @@
---
- name: Check NetworkManager.conf exists
stat:
path: /etc/NetworkManager/NetworkManager.conf
register: result

- name: Unmask NetworkManager service
command: systemctl unmask NetworkManager
when: result.stat.exists

- name: Add dnsmasq to network config
lineinfile: >
dest=/etc/NetworkManager/NetworkManager.conf
regexp="dns="
line="dns=dnsmasq"
state=present
when: result.stat.exists
notify:
- restart network manager
...

+ 5
- 0
bench/playbooks/roles/fail2ban/defaults/main.yml Zobrazit soubor

@@ -0,0 +1,5 @@
---
fail2ban_nginx_access_log: /var/log/nginx/*access.log
maxretry: 6
bantime: 600
findtime: 600

+ 3
- 0
bench/playbooks/roles/fail2ban/handlers/main.yml Zobrazit soubor

@@ -0,0 +1,3 @@
---
- name: restart fail2ban
service: name=fail2ban state=restarted

+ 14
- 0
bench/playbooks/roles/fail2ban/tasks/configure_nginx_jail.yml Zobrazit soubor

@@ -0,0 +1,14 @@
- name: Configure fail2ban jail options
hosts: localhost
become: yes
become_user: root
vars_files:
- ../defaults/main.yml
tasks:
- name: Setup filter
template: src="../templates/nginx-proxy-filter.conf.j2" dest="/etc/fail2ban/filter.d/nginx-proxy.conf"
- name: Setup jail
template: src="../templates/nginx-proxy-jail.conf.j2" dest="/etc/fail2ban/jail.d/nginx-proxy.conf"
- name: restart service
service: name=fail2ban state=restarted

+ 28
- 0
bench/playbooks/roles/fail2ban/tasks/main.yml Zobrazit soubor

@@ -0,0 +1,28 @@
---
- name: Install fail2ban
yum: name=fail2ban state=present
when: ansible_distribution == 'CentOS'

- name: Install fail2ban
apt: name=fail2ban state=present
when: ansible_distribution == 'Debian' or ansible_distribution == 'Ubuntu'

- name: Enable fail2ban
service: name=fail2ban enabled=yes

- name: Create jail.d
file: path=/etc/fail2ban/jail.d state=directory

- name: Setup filters
template: src="{{item}}-filter.conf.j2" dest="/etc/fail2ban/filter.d/{{item}}.conf"
with_items:
- nginx-proxy
notify:
- restart fail2ban

- name: setup jails
template: src="{{item}}-jail.conf.j2" dest="/etc/fail2ban/jail.d/{{item}}.conf"
with_items:
- nginx-proxy
notify:
- restart fail2ban

+ 10
- 0
bench/playbooks/roles/fail2ban/templates/nginx-proxy-filter.conf.j2 Zobrazit soubor

@@ -0,0 +1,10 @@
# Block IPs trying to use server as proxy.
[Definition]
failregex = <HOST>.*\" 400
<HOST>.*"[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]
<HOST>.*".*supports_implicit_sdk_logging.*" 4[\d][\d]
<HOST>.*".*activities?advertiser_tracking_enabled.*" 4[\d][\d]
<HOST>.*".*/picture?type=normal.*" 4[\d][\d]
<HOST>.*".*/announce.php?info_hash=.*" 4[\d][\d]

ignoreregex =

+ 9
- 0
bench/playbooks/roles/fail2ban/templates/nginx-proxy-jail.conf.j2 Zobrazit soubor

@@ -0,0 +1,9 @@
## block hosts trying to abuse our server as a forward proxy
[nginx-proxy]
enabled = true
filter = nginx-proxy
logpath = {{ fail2ban_nginx_access_log }}
action = iptables-multiport[name=NoNginxProxy, port="http,https"]
maxretry = {{ maxretry }}
bantime = {{ bantime }}
findtime = {{ findtime }}

+ 32
- 0
bench/playbooks/roles/frappe_selinux/files/frappe_selinux.te Zobrazit soubor

@@ -0,0 +1,32 @@
module xhiveframework_selinux 1.0;

require {
type user_home_dir_t;
type httpd_t;
type user_home_t;
type soundd_port_t;
class tcp_socket name_connect;
class lnk_file read;
class dir { getattr search };
class file { read open };
}

#============= httpd_t ==============

#!!!! This avc is allowed in the current policy
allow httpd_t soundd_port_t:tcp_socket name_connect;

#!!!! This avc is allowed in the current policy
allow httpd_t user_home_dir_t:dir search;

#!!!! This avc is allowed in the current policy
allow httpd_t user_home_t:dir { getattr search };

#!!!! This avc can be allowed using the boolean 'httpd_read_user_content'
allow httpd_t user_home_t:file open;

#!!!! This avc is allowed in the current policy
allow httpd_t user_home_t:file read;

#!!!! This avc is allowed in the current policy
allow httpd_t user_home_t:lnk_file read;

+ 25
- 0
bench/playbooks/roles/frappe_selinux/tasks/main.yml Zobrazit soubor

@@ -0,0 +1,25 @@
---
- name: Install deps
yum:
name:
- policycoreutils-python
- selinux-policy-devel
state: present
when: ansible_distribution == 'CentOS'

- name: Check enabled SELinux modules
shell: semanage module -l
register: enabled_modules
when: ansible_distribution == 'CentOS'

- name: Copy xhiveframework_selinux policy
copy: src=xhiveframework_selinux.te dest=/root/xhiveframework_selinux.te
register: dest_xhiveframework_selinux_te
when: ansible_distribution == 'CentOS'

- name: Compile xhiveframework_selinux policy
shell: "make -f /usr/share/selinux/devel/Makefile xhiveframework_selinux.pp && semodule -i xhiveframework_selinux.pp"
args:
chdir: /root/
when: "ansible_distribution == 'CentOS' and enabled_modules.stdout.find('xhiveframework_selinux') == -1 or dest_xhiveframework_selinux_te.changed"
...

+ 4
- 0
bench/playbooks/roles/locale/defaults/main.yml Zobrazit soubor

@@ -0,0 +1,4 @@
---
locale_keymap: us
locale_lang: en_US.utf8
...

+ 21
- 0
bench/playbooks/roles/locale/tasks/main.yml Zobrazit soubor

@@ -0,0 +1,21 @@
---
- name: Check current locale
shell: localectl
register: locale_test
when: ansible_distribution == 'Centos' or ansible_distribution == 'Ubuntu'

- name: Set Locale
command: "localectl set-locale LANG={{ locale_lang }}"
when: (ansible_distribution == 'Centos' or ansible_distribution == 'Ubuntu') and locale_test.stdout.find('LANG=locale_lang') == -1

- name: Set keymap
command: "localectl set-keymap {{ locale_keymap }}"
when: "(ansible_distribution == 'Centos' or ansible_distribution == 'Ubuntu') and locale_test.stdout.find('Keymap:locale_keymap') == -1"

- name: Set Locale as en_US
lineinfile: dest=/etc/environment backup=yes line="{{ item }}"
with_items:
- "LC_ALL=en_US.UTF-8"
- "LC_CTYPE=en_US.UTF-8"
- "LANG=en_US.UTF-8"
...

+ 4
- 0
bench/playbooks/roles/logwatch/defaults/main.yml Zobrazit soubor

@@ -0,0 +1,4 @@
---
logwatch_emails: "{{ admin_emails }}"
logwatch_detail: High
...

+ 13
- 0
bench/playbooks/roles/logwatch/tasks/main.yml Zobrazit soubor

@@ -0,0 +1,13 @@
---
- name: Install logwatch
yum: name=logwatch state=present
when: ansible_distribution == 'CentOS'

- name: Install logwatch on Ubuntu or Debian
apt: name=logwatch state=present
when: ansible_distribution == 'Debian' or ansible_distribution == 'Ubuntu'

- name: Copy logwatch config
template: src=logwatch.conf.j2 dest=/etc/logwatch/conf/logwatch.conf backup=yes
when: admin_emails is defined
...

+ 2
- 0
bench/playbooks/roles/logwatch/templates/logwatch.conf.j2 Zobrazit soubor

@@ -0,0 +1,2 @@
MailTo = {{ logwatch_emails }}
Detail = {{ logwatch_detail }}

+ 63
- 0
bench/playbooks/roles/mariadb/README.md Zobrazit soubor

@@ -0,0 +1,63 @@
# Ansible Role: MariaDB

Installs MariaDB

## Supported platforms

```
CentOS 6 & 7
Ubuntu 14.04
Ubuntu 16.04
Debain 9
```

## Post install

Run `mariadb-secure-installation`

## Requirements

None

## Role Variables

MariaDB version:

```
mariadb_version: 10.2
```

Configuration template:

```
mysql_conf_tpl: change_me
```

Configuration filename:

```
mysql_conf_file: settings.cnf
```

### Experimental unattended mariadb-secure-installation

```
ansible-playbook release.yml --extra-vars "mysql_secure_installation=true mysql_root_password=your_very_secret_password"
```

## Dependencies

None

## Example Playbook

```
- hosts: servers
roles:
- { role: mariadb }
```

## Credits

- [Attila van der Velde](https://github.com/vdvm)


+ 5
- 0
bench/playbooks/roles/mariadb/defaults/main.yml Zobrazit soubor

@@ -0,0 +1,5 @@
---
mysql_conf_tpl: change_me
mysql_conf_file: settings.cnf

mysql_secure_installation: false

+ 14
- 0
bench/playbooks/roles/mariadb/files/debian_mariadb_config.cnf Zobrazit soubor

@@ -0,0 +1,14 @@
[mysqld]
innodb-file-format=barracuda
innodb-file-per-table=1
innodb-large-prefix=1
character-set-client-handshake = FALSE
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
max_allowed_packet=256M

[mysql]
default-character-set = utf8mb4

[mysqldump]
max_allowed_packet=256M

+ 64
- 0
bench/playbooks/roles/mariadb/files/mariadb_config.cnf Zobrazit soubor

@@ -0,0 +1,64 @@
[mysqld]

# GENERAL #
user = mysql
default-storage-engine = InnoDB
socket = /var/lib/mysql/mysql.sock
pid-file = /var/lib/mysql/mysql.pid

# MyISAM #
key-buffer-size = 32M
myisam-recover = FORCE,BACKUP

# SAFETY #
max-allowed-packet = 256M
max-connect-errors = 1000000
innodb = FORCE

# DATA STORAGE #
datadir = /var/lib/mysql/

# BINARY LOGGING #
log-bin = /var/lib/mysql/mysql-bin
expire-logs-days = 14
sync-binlog = 1

# REPLICATION #
server-id = 1

# CACHES AND LIMITS #
tmp-table-size = 32M
max-heap-table-size = 32M
query-cache-type = 0
query-cache-size = 0
max-connections = 500
thread-cache-size = 50
open-files-limit = 65535
table-definition-cache = 4096
table-open-cache = 10240

# INNODB #
innodb-flush-method = O_DIRECT
innodb-log-files-in-group = 2
innodb-log-file-size = 512M
innodb-flush-log-at-trx-commit = 1
innodb-file-per-table = 1
innodb-buffer-pool-size = {{ (ansible_memtotal_mb*0.685)|round|int }}M
innodb-file-format = barracuda
innodb-large-prefix = 1
collation-server = utf8mb4_unicode_ci
character-set-server = utf8mb4
character-set-client-handshake = FALSE
max_allowed_packet = 256M

# LOGGING #
log-error = /var/lib/mysql/mysql-error.log
log-queries-not-using-indexes = 0
slow-query-log = 1
slow-query-log-file = /var/lib/mysql/mysql-slow.log

[mysql]
default-character-set = utf8mb4

[mysqldump]
max_allowed_packet=256M

+ 3
- 0
bench/playbooks/roles/mariadb/handlers/main.yml Zobrazit soubor

@@ -0,0 +1,3 @@
---
- name: restart mariadb
service: name=mariadb state=restarted

Některé soubory nejsou zobrazny, neboť je v této revizi změněno mnoho souborů

Načítá se…
Zrušit
Uložit