├── tests ├── __init__.py ├── cli │ ├── __init__.py │ ├── ext │ │ └── __init__.py │ ├── plugins │ │ └── __init__.py │ ├── 21_test_site_info.py │ ├── 23_test_site_show.py │ ├── 20_test_site_enable.py │ ├── 19_test_site_disable.py │ ├── 22_test_site_list.py │ ├── 27_test_info.py │ ├── 28_test_secure.py │ ├── 14_test_stack_services_stop.py │ ├── 15_test_stack_services_start.py │ ├── 17_test_stack_services_status.py │ ├── 16_test_stack_services_restart.py │ ├── 25_test_clean.py │ ├── 29_test_site_delete.py │ ├── 13_test_stack_install.py │ ├── 30_test_stack_remove.py │ ├── 24_test_site_update.py │ ├── 31_test_stack_purge.py │ └── 18_test_site_create.py ├── core │ └── __init__.py └── issue.sh ├── wo ├── cli │ ├── __init__.py │ ├── ext │ │ └── __init__.py │ ├── plugins │ │ ├── __init__.py │ │ ├── import_slow_log.py │ │ ├── maintenance.py │ │ ├── models.py │ │ ├── clean.py │ │ ├── sitedb.py │ │ ├── update.py │ │ ├── sync.py │ │ └── stack_migrate.py │ ├── templates │ │ ├── __init__.py │ │ ├── blockips.mustache │ │ ├── fail2ban-wp.mustache │ │ ├── fail2ban-forbidden.mustache │ │ ├── traffic-advice.mustache │ │ ├── wo-ufw.mustache │ │ ├── force-ssl.mustache │ │ ├── php-fpm.mustache │ │ ├── acl.mustache │ │ ├── ssl.mustache │ │ ├── php.mustache │ │ ├── freshclam.mustache │ │ ├── wo-kernel-script.mustache │ │ ├── wo-kernel-service.mustache │ │ ├── wpsubdir.mustache │ │ ├── info_mysql.mustache │ │ ├── info_nginx.mustache │ │ ├── publicsuffix.mustache │ │ ├── fail2ban.mustache │ │ ├── siteinfo.mustache │ │ ├── php-pool.mustache │ │ ├── wpsc.mustache │ │ ├── wpce.mustache │ │ ├── wpfc.mustache │ │ ├── tweaks.mustache │ │ ├── wprocket.mustache │ │ ├── fastcgi.mustache │ │ ├── cloudflare.mustache │ │ ├── proftpd-tls.mustache │ │ ├── brotli.mustache │ │ ├── gzip.mustache │ │ ├── webp.mustache │ │ ├── avif.mustache │ │ ├── redis.mustache │ │ ├── wo-update.mustache │ │ ├── stub_status.mustache │ │ ├── info_php.mustache │ │ ├── 22222.mustache │ │ ├── ufw.mustache │ │ ├── wo-plus.mustache │ │ ├── sshd.mustache │ │ ├── virtualconf.mustache │ │ ├── cf-update.mustache │ │ ├── map-wp.mustache │ │ ├── nextcloud.mustache │ │ ├── nginx-core.mustache │ │ ├── locations.mustache │ │ ├── wpcommon.mustache │ │ ├── proftpd.mustache │ │ ├── upstream.mustache │ │ ├── mime.mustache │ │ └── my.mustache │ ├── controllers │ │ ├── __init__.py │ │ └── base.py │ ├── bootstrap.py │ └── main.py ├── core │ ├── __init__.py │ ├── exc.py │ ├── wpcli.py │ ├── random.py │ ├── extract.py │ ├── nginx.py │ ├── sendmail.py │ ├── database.py │ ├── checkfqdn.py │ ├── template.py │ ├── stackconf.py │ ├── nginxhashbucket.py │ ├── domainvalidate.py │ ├── cron.py │ ├── addswap.py │ ├── download.py │ ├── logging.py │ ├── shellexec.py │ ├── git.py │ ├── apt_repo.py │ ├── mysql.py │ └── logwatch.py ├── utils │ ├── __init__.py │ └── test.py └── __init__.py ├── logo.png ├── favicon.ico ├── MANIFEST.in ├── .editorconfig ├── requirements.txt ├── .github ├── dependabot.yml ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md ├── stale.yml ├── FUNDING.yml └── workflows │ ├── pypi.yml │ ├── stale.yml │ └── main.yml ├── config ├── plugins.d │ ├── log.conf │ ├── site.conf │ ├── clean.conf │ ├── debug.conf │ ├── secure.conf │ ├── stack.conf │ ├── update.conf │ ├── maintenance.conf │ ├── import_slow_log.conf │ └── info.conf └── wo.conf ├── setup.cfg ├── LICENSE ├── .travis.yml ├── CONTRIBUTING.md ├── setup.py └── CODE_OF_CONDUCT.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /wo/cli/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/cli/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /wo/cli/ext/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /wo/core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /wo/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/cli/ext/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /wo/cli/plugins/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /wo/cli/templates/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/cli/plugins/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /wo/cli/controllers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /wo/__init__.py: -------------------------------------------------------------------------------- 1 | __import__('pkg_resources').declare_namespace(__name__) 2 | -------------------------------------------------------------------------------- /wo/cli/templates/blockips.mustache: -------------------------------------------------------------------------------- 1 | # Block IP Address 2 | # deny 1.1.1.1; 3 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fireman03151/best_WordPress/HEAD/logo.png -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fireman03151/best_WordPress/HEAD/favicon.ico -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include *.py 2 | include setup.cfg 3 | include README.md CHANGELOG.md LICENSE 4 | include *.txt 5 | -------------------------------------------------------------------------------- /wo/cli/templates/fail2ban-wp.mustache: -------------------------------------------------------------------------------- 1 | [Definition] 2 | failregex = ^.* "POST .*/wp-login.php([/\?#\\].*)? HTTP/.*" 200 3 | ignoreregex = 4 | -------------------------------------------------------------------------------- /wo/cli/templates/fail2ban-forbidden.mustache: -------------------------------------------------------------------------------- 1 | [Definition] 2 | failregex = ^ \[error\] \d+#\d+: .* forbidden .*, client: , .*$ 3 | ignoreregex = 4 | -------------------------------------------------------------------------------- /wo/cli/templates/traffic-advice.mustache: -------------------------------------------------------------------------------- 1 | [{ 2 | "user_agent": "prefetch-proxy", 3 | "google_prefetch_proxy_eap": { 4 | "fraction": 1.0 5 | } 6 | }] -------------------------------------------------------------------------------- /wo/cli/templates/wo-ufw.mustache: -------------------------------------------------------------------------------- 1 | [WordOps] 2 | title=WordOps(WO) 3 | description=Command-line tool that ease WordPress site and server management 4 | ports=22222/tcp -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = false -------------------------------------------------------------------------------- /wo/cli/templates/force-ssl.mustache: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | listen [::]:80; 4 | server_name {{domains}}; 5 | return 301 https://$host$request_uri; 6 | } 7 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | cement==2.10.12 2 | pynginxconfig==0.3.4 3 | PyMySQL==1.1.0 4 | psutil==5.9.7 5 | sh==2.0.6 6 | SQLAlchemy==2.0.25 7 | requests>=2.20.0 8 | distro==1.9.0 9 | argcomplete==3.2.1 10 | colorlog==6.8.0 11 | -------------------------------------------------------------------------------- /wo/cli/templates/php-fpm.mustache: -------------------------------------------------------------------------------- 1 | [global] 2 | pid = {{pid}} 3 | error_log = {{error_log}} 4 | log_level = notice 5 | emergency_restart_threshold = 10 6 | emergency_restart_interval = 1m 7 | process_control_timeout = 10s 8 | include = {{include}} 9 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: pip 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | ignore: 9 | - dependency-name: cement 10 | versions: 11 | - ">= 3.a, < 4" 12 | -------------------------------------------------------------------------------- /wo/cli/templates/acl.mustache: -------------------------------------------------------------------------------- 1 | # WordOps (wo) protect locations using 2 | # HTTP authentication || IP address 3 | satisfy any; 4 | auth_basic "Restricted Area"; 5 | auth_basic_user_file htpasswd-wo; 6 | # Allowed IP Address List 7 | allow 127.0.0.1; 8 | allow ::1; 9 | deny all; 10 | -------------------------------------------------------------------------------- /config/plugins.d/log.conf: -------------------------------------------------------------------------------- 1 | ### Example Plugin Configuration for WordOps 2 | 3 | [log] 4 | 5 | ### If enabled, load a plugin named `example` either from the Python module 6 | ### `wo.cli.plugins.example` or from the file path 7 | ### `/var/lib/wo/plugins/example.py` 8 | enable_plugin = true 9 | -------------------------------------------------------------------------------- /config/plugins.d/site.conf: -------------------------------------------------------------------------------- 1 | ### Example Plugin Configuration for WordOps 2 | 3 | [site] 4 | 5 | ### If enabled, load a plugin named `example` either from the Python module 6 | ### `wo.cli.plugins.example` or from the file path 7 | ### `/var/lib/wo/plugins/example.py` 8 | enable_plugin = true 9 | -------------------------------------------------------------------------------- /config/plugins.d/clean.conf: -------------------------------------------------------------------------------- 1 | ### Example Plugin Configuration for WordOps 2 | 3 | [clean] 4 | 5 | ### If enabled, load a plugin named `example` either from the Python module 6 | ### `wo.cli.plugins.example` or from the file path 7 | ### `/var/lib/wo/plugins/example.py` 8 | enable_plugin = true 9 | -------------------------------------------------------------------------------- /config/plugins.d/debug.conf: -------------------------------------------------------------------------------- 1 | ### Example Plugin Configuration for WordOps 2 | 3 | [debug] 4 | 5 | ### If enabled, load a plugin named `example` either from the Python module 6 | ### `wo.cli.plugins.example` or from the file path 7 | ### `/var/lib/wo/plugins/example.py` 8 | enable_plugin = true 9 | -------------------------------------------------------------------------------- /config/plugins.d/secure.conf: -------------------------------------------------------------------------------- 1 | ### Example Plugin Configuration for WordOps 2 | 3 | [secure] 4 | 5 | ### If enabled, load a plugin named `example` either from the Python module 6 | ### `wo.cli.plugins.example` or from the file path 7 | ### `/var/lib/wo/plugins/example.py` 8 | enable_plugin = true 9 | -------------------------------------------------------------------------------- /config/plugins.d/stack.conf: -------------------------------------------------------------------------------- 1 | ### Example Plugin Configuration for WordOps 2 | 3 | [stack] 4 | 5 | ### If enabled, load a plugin named `example` either from the Python module 6 | ### `wo.cli.plugins.example` or from the file path 7 | ### `/var/lib/wo/plugins/example.py` 8 | enable_plugin = true 9 | -------------------------------------------------------------------------------- /config/plugins.d/update.conf: -------------------------------------------------------------------------------- 1 | ### Example Plugin Configuration for WordOps 2 | 3 | [update] 4 | 5 | ### If enabled, load a plugin named `example` either from the Python module 6 | ### `wo.cli.plugins.example` or from the file path 7 | ### `/var/lib/wo/plugins/example.py` 8 | enable_plugin = true 9 | -------------------------------------------------------------------------------- /tests/cli/21_test_site_info.py: -------------------------------------------------------------------------------- 1 | from wo.utils import test 2 | from wo.cli.main import WOTestApp 3 | 4 | 5 | class CliTestCaseSiteInfo(test.WOTestCase): 6 | 7 | def test_wo_cli_site_info(self): 8 | with WOTestApp(argv=['site', 'info', 'html.com']) as app: 9 | app.run() 10 | -------------------------------------------------------------------------------- /tests/cli/23_test_site_show.py: -------------------------------------------------------------------------------- 1 | from wo.utils import test 2 | from wo.cli.main import WOTestApp 3 | 4 | 5 | class CliTestCaseSiteShow(test.WOTestCase): 6 | 7 | def test_wo_cli_show_edit(self): 8 | with WOTestApp(argv=['site', 'show', 'html.com']) as app: 9 | app.run() 10 | -------------------------------------------------------------------------------- /wo/cli/templates/ssl.mustache: -------------------------------------------------------------------------------- 1 | listen 443 ssl http2; 2 | listen [::]:443 ssl http2; 3 | ssl_certificate {{ssl_live_path}}/{{domain}}/fullchain.pem; 4 | ssl_certificate_key {{ssl_live_path}}/{{domain}}/key.pem; 5 | ssl_trusted_certificate {{ssl_live_path}}/{{domain}}/ca.pem; 6 | ssl_stapling_verify on; -------------------------------------------------------------------------------- /config/plugins.d/maintenance.conf: -------------------------------------------------------------------------------- 1 | ### Example Plugin Configuration for WordOps 2 | 3 | [maintenance] 4 | 5 | ### If enabled, load a plugin named `example` either from the Python module 6 | ### `wo.cli.plugins.example` or from the file path 7 | ### `/var/lib/wo/plugins/example.py` 8 | enable_plugin = true 9 | -------------------------------------------------------------------------------- /tests/cli/20_test_site_enable.py: -------------------------------------------------------------------------------- 1 | from wo.utils import test 2 | from wo.cli.main import WOTestApp 3 | 4 | 5 | class CliTestCaseSiteEnable(test.WOTestCase): 6 | 7 | def test_wo_cli_site_enable(self): 8 | with WOTestApp(argv=['site', 'enable', 'html.com']) as app: 9 | app.run() 10 | -------------------------------------------------------------------------------- /config/plugins.d/import_slow_log.conf: -------------------------------------------------------------------------------- 1 | ### Example Plugin Configuration for WordOps 2 | 3 | [import_slow_log] 4 | 5 | ### If enabled, load a plugin named `example` either from the Python module 6 | ### `wo.cli.plugins.example` or from the file path 7 | ### `/var/lib/wo/plugins/example.py` 8 | enable_plugin = true 9 | -------------------------------------------------------------------------------- /tests/cli/19_test_site_disable.py: -------------------------------------------------------------------------------- 1 | from wo.utils import test 2 | from wo.cli.main import WOTestApp 3 | 4 | 5 | class CliTestCaseSiteDisable(test.WOTestCase): 6 | 7 | def test_wo_cli_site_disable(self): 8 | with WOTestApp(argv=['site', 'disable', 'html.com']) as app: 9 | app.run() 10 | -------------------------------------------------------------------------------- /wo/cli/bootstrap.py: -------------------------------------------------------------------------------- 1 | """WordOps bootstrapping.""" 2 | 3 | # All built-in application controllers should be imported, and registered 4 | # in this file in the same way as WOBaseController. 5 | 6 | 7 | from wo.cli.controllers.base import WOBaseController 8 | 9 | 10 | def load(app): 11 | app.handler.register(WOBaseController) 12 | -------------------------------------------------------------------------------- /wo/cli/templates/php.mustache: -------------------------------------------------------------------------------- 1 | # PHP NGINX CONFIGURATION - WordOps {{release}} 2 | # DO NOT MODIFY, ALL CHANGES WILL BE LOST AFTER AN WordOps (wo) UPDATE 3 | location / { 4 | try_files $uri $uri/ /index.php$is_args$args; 5 | } 6 | location ~ \.php$ { 7 | try_files $uri =404; 8 | include fastcgi_params; 9 | fastcgi_pass {{upstream}}; 10 | } 11 | -------------------------------------------------------------------------------- /wo/cli/templates/freshclam.mustache: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # WordOps ClamAV freshclam script 3 | # script path after installation /opt/freshcham.sh 4 | 5 | if [ -x /etc/init.d/clamav-freshclam ]; then 6 | { 7 | /etc/init.d/clamav-freshclam stop 8 | freshclam 9 | /etc/init.d/clamav-freshclam start 10 | } >> /var/log/wo/clamav.log 2>&1 11 | fi 12 | -------------------------------------------------------------------------------- /config/plugins.d/info.conf: -------------------------------------------------------------------------------- 1 | ### Example Plugin Configuration for WordOps 2 | 3 | [info] 4 | 5 | ### If enabled, load a plugin named `example` either from the Python module 6 | ### `wo.cli.plugins.example` or from the file path 7 | ### `/var/lib/wo/plugins/example.py` 8 | enable_plugin = true 9 | 10 | ### Additional plugin configuration settings 11 | foo = bar 12 | -------------------------------------------------------------------------------- /wo/cli/templates/wo-kernel-script.mustache: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # WordOps Kernel tweaks script 3 | # script path after installation /opt/wo-kernel.sh 4 | 5 | # Netdata Memory tweak 6 | echo 1 >/sys/kernel/mm/ksm/run 7 | echo 1000 >/sys/kernel/mm/ksm/sleep_millisecs 8 | # Redis disable transparent_hugepage 9 | echo never > /sys/kernel/mm/transparent_hugepage/enabled 10 | -------------------------------------------------------------------------------- /wo/cli/templates/wo-kernel-service.mustache: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=WordOps kernel tweaks 3 | # append here other services you want netdata to wait for them to start 4 | After=syslog.target network.target remote-fs.target nss-lookup.target 5 | 6 | [Service] 7 | Type=simple 8 | User=root 9 | ExecStart=/opt/wo-kernel.sh 10 | StandardOutput=null 11 | 12 | [Install] 13 | WantedBy=multi-user.target 14 | Alias=wo-kernel.service -------------------------------------------------------------------------------- /wo/cli/templates/wpsubdir.mustache: -------------------------------------------------------------------------------- 1 | # WPSUBDIRECTORY NGINX CONFIGURATION 2 | # DO NOT MODIFY, ALL CHANGES WILL BE LOST AFTER AN WordOps (wo) UPDATE 3 | if (!-e $request_filename) { 4 | # Redirect wp-admin to wp-admin/ 5 | rewrite /wp-admin$ $scheme://$host$uri/ permanent; 6 | # Redirect wp-* files/folders 7 | rewrite ^(/[^/]+)?(/wp-.*) $2 last; 8 | # Redirect other php files 9 | rewrite ^(/[^/]+)?(/.*\.php) $2 last; 10 | } 11 | -------------------------------------------------------------------------------- /tests/cli/22_test_site_list.py: -------------------------------------------------------------------------------- 1 | from wo.utils import test 2 | from wo.cli.main import WOTestApp 3 | 4 | 5 | class CliTestCaseSiteList(test.WOTestCase): 6 | 7 | def test_wo_cli_site_list_enable(self): 8 | with WOTestApp(argv=['site', 'list', '--enabled']) as app: 9 | app.run() 10 | 11 | def test_wo_cli_site_list_disable(self): 12 | with WOTestApp(argv=['site', 'list', '--disabled']) as app: 13 | app.run() 14 | -------------------------------------------------------------------------------- /wo/cli/templates/info_mysql.mustache: -------------------------------------------------------------------------------- 1 | 2 | MySQL ({{version}}) on {{host}}: 3 | 4 | port {{port}} 5 | wait_timeout {{wait_timeout}} 6 | interactive_timeout {{interactive_timeout}} 7 | max_used_connections {{max_used_connections}} 8 | datadir {{datadir}} 9 | socket {{socket}} 10 | my.cnf [PATH] /etc/mysql/conf.d/my.cnf 11 | -------------------------------------------------------------------------------- /wo/cli/templates/info_nginx.mustache: -------------------------------------------------------------------------------- 1 | 2 | NGINX ({{version}}): 3 | 4 | user {{user}} 5 | worker_processes {{worker_processes}} 6 | worker_connections {{worker_connections}} 7 | keepalive_timeout {{keepalive_timeout}} 8 | fastcgi_read_timeout {{fastcgi_read_timeout}} 9 | client_max_body_size {{client_max_body_size}} 10 | allow {{allow}} 11 | -------------------------------------------------------------------------------- /wo/utils/test.py: -------------------------------------------------------------------------------- 1 | """Testing utilities for WordOps""" 2 | from cement.utils import test 3 | from wo.cli.main import WOTestApp 4 | 5 | 6 | class WOTestCase(test.CementTestCase): 7 | app_class = WOTestApp 8 | 9 | def setUp(self): 10 | """Override setup actions (for every test).""" 11 | super(WOTestCase, self).setUp() 12 | 13 | def tearDown(self): 14 | """Override teardown actions (for every test).""" 15 | super(WOTestCase, self).tearDown() 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | This issue tracker is only for issues directly related to WordOps. 3 | Please use for support questions. 4 | 5 | If you feel the issue is a WordOps specific issue, please attach the output of the following commands. 6 | 7 | System Information 8 | 9 | - [ ] `lsb_release -a` 10 | - [ ] `wo info` 11 | - [ ] `nginx -V` 12 | - [ ] `wo -v` 13 | - [ ] `wp cli info --allow-root` 14 | - [ ] `curl -sL git.io/fjAp3 | sudo -E bash -` 15 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 10 | 11 | ##### Summary 12 | 13 | ##### Additional Information 14 | -------------------------------------------------------------------------------- /wo/cli/templates/publicsuffix.mustache: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # WordOps script to download public suffix list from Github 3 | 4 | # check if curl is available 5 | if ! { command -v curl; }; then 6 | apt-get update && apt-get install curl -qq > /dev/null 2>&1 7 | fi 8 | # download the list 9 | rm -f /var/lib/wo/public_suffix_list.dat 10 | curl -sL -m 30 --retry 3 -k https://raw.githubusercontent.com/publicsuffix/list/master/public_suffix_list.dat | sed '/^\/\//d' | sed '/^$/d' | sed 's/^\s+//g' > /var/lib/wo/public_suffix_list.dat 11 | -------------------------------------------------------------------------------- /tests/cli/27_test_info.py: -------------------------------------------------------------------------------- 1 | from wo.utils import test 2 | from wo.cli.main import WOTestApp 3 | 4 | 5 | class CliTestCaseInfo(test.WOTestCase): 6 | 7 | def test_wo_cli_info_mysql(self): 8 | with WOTestApp(argv=['info', '--mysql']) as app: 9 | app.run() 10 | 11 | def test_wo_cli_info_php(self): 12 | with WOTestApp(argv=['info', '--php']) as app: 13 | app.run() 14 | 15 | def test_wo_cli_info_nginx(self): 16 | with WOTestApp(argv=['info', '--nginx']) as app: 17 | app.run() 18 | -------------------------------------------------------------------------------- /wo/core/exc.py: -------------------------------------------------------------------------------- 1 | """WordOps exception classes.""" 2 | 3 | 4 | class WOError(Exception): 5 | """Generic errors.""" 6 | 7 | def __init__(self, msg): 8 | Exception.__init__(self) 9 | self.msg = msg 10 | 11 | def __str__(self): 12 | return self.msg 13 | 14 | 15 | class WOConfigError(WOError): 16 | """Config related errors.""" 17 | pass 18 | 19 | 20 | class WORuntimeError(WOError): 21 | """Generic runtime errors.""" 22 | pass 23 | 24 | 25 | class WOArgumentError(WOError): 26 | """Argument related errors.""" 27 | pass 28 | -------------------------------------------------------------------------------- /wo/core/wpcli.py: -------------------------------------------------------------------------------- 1 | """WordPress utilities for WordOps""" 2 | from wo.core.logging import Log 3 | from wo.core.shellexec import WOShellExec 4 | from wo.core.variables import WOVar 5 | 6 | 7 | class WOWp: 8 | """WordPress utilities for WordOps""" 9 | 10 | def wpcli(self, command): 11 | """WP-CLI wrapper""" 12 | try: 13 | WOShellExec.cmd_exec( 14 | self, '{0} --allow-root '.format(WOVar.wo_wpcli_path) + 15 | '{0}'.format(command)) 16 | except Exception: 17 | Log.error(self, "WP-CLI command failed") 18 | -------------------------------------------------------------------------------- /tests/cli/28_test_secure.py: -------------------------------------------------------------------------------- 1 | from wo.utils import test 2 | from wo.cli.main import WOTestApp 3 | 4 | 5 | class CliTestCaseSecure(test.WOTestCase): 6 | 7 | def test_wo_cli_secure_auth(self): 8 | with WOTestApp(argv=['secure', '--auth', 'abc', 'superpass']) as app: 9 | app.run() 10 | 11 | def test_wo_cli_secure_port(self): 12 | with WOTestApp(argv=['secure', '--port', '22222']) as app: 13 | app.run() 14 | 15 | def test_wo_cli_secure_ip(self): 16 | with WOTestApp(argv=['secure', '--ip', '172.16.0.1']) as app: 17 | app.run() 18 | -------------------------------------------------------------------------------- /wo/core/random.py: -------------------------------------------------------------------------------- 1 | import random 2 | import string 3 | 4 | 5 | class RANDOM: 6 | """Random strings generator""" 7 | 8 | def long(self): 9 | long_random = ''.join([random.choice 10 | (string.ascii_letters + string.digits) 11 | for n in range(24)]) 12 | return long_random 13 | 14 | def short(self): 15 | short_random = ''.join([random.choice 16 | (string.ascii_letters + string.digits) 17 | for n in range(24)]) 18 | return short_random 19 | -------------------------------------------------------------------------------- /wo/core/extract.py: -------------------------------------------------------------------------------- 1 | """WordOps Extract Core """ 2 | import os 3 | import tarfile 4 | 5 | from wo.core.logging import Log 6 | 7 | 8 | class WOExtract(): 9 | """Method to extract from tar.gz file""" 10 | 11 | def extract(self, file, path): 12 | """Function to extract tar.gz file""" 13 | try: 14 | tar = tarfile.open(file) 15 | tar.extractall(path=path) 16 | tar.close() 17 | os.remove(file) 18 | return True 19 | except tarfile.TarError as e: 20 | Log.debug(self, "{0}".format(e)) 21 | Log.error(self, 'Unable to extract file \{0}'.format(file)) 22 | return False 23 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | 2 | [nosetests] 3 | verbosity=2 4 | debug=0 5 | detailed-errors=1 6 | with-coverage=1 7 | cover-package=wo 8 | cover-inclusive=1 9 | cover-erase=1 10 | cover-html=1 11 | cover-html-dir=coverage_report/ 12 | where=tests/ 13 | 14 | [metadata] 15 | license-file = LICENSE 16 | 17 | [flake8] 18 | ignore = F405,W504,S322,S404,S603,s607,s602,C901 19 | max-line-length = 120 20 | exclude = 21 | # No need to traverse our git directory 22 | .git, 23 | # There's no value in checking cache directories 24 | __pycache__, 25 | # This contains our built documentation 26 | build, 27 | # This contains builds of flake8 that we don't want to check 28 | dist 29 | max-complexity = 10 -------------------------------------------------------------------------------- /wo/cli/templates/fail2ban.mustache: -------------------------------------------------------------------------------- 1 | [DEFAULT] 2 | ignoreip = 127.0.0.1/8 ::1 3 | 4 | [recidive] 5 | enabled = true 6 | 7 | {{#nginx}}[nginx-http-auth] 8 | enabled = true 9 | logpath = /var/log/nginx/*error*.log 10 | 11 | [nginx-botsearch] 12 | enabled = true 13 | logpath = /var/log/nginx/*access*.log 14 | 15 | [wo-wordpress] 16 | enabled = true 17 | filter = wo-wordpress 18 | action = iptables-multiport[name="wo-wordpress", port="http,https"] 19 | logpath = /var/log/nginx/*access*.log 20 | maxretry = 5 21 | 22 | [nginx-forbidden] 23 | enabled = true 24 | filter = nginx-forbidden 25 | action = iptables-multiport[name="nginx-forbidden", port="http,https"] 26 | logpath = /var/log/nginx/*error*.log{{/nginx}} -------------------------------------------------------------------------------- /wo/cli/templates/siteinfo.mustache: -------------------------------------------------------------------------------- 1 | Information about {{domain}} ({{domain_type}}): 2 | 3 | 4 | Nginx configuration {{type}} {{enable}} 5 | {{#php_version}}PHP Version {{php_version}}{{/php_version}} 6 | 7 | {{#ssl}}SSL {{ssl}}{{/ssl}}{{#sslprovider}} 8 | SSL PROVIDER {{sslprovider}}{{/sslprovider}}{{#sslexpiry}} 9 | SSL EXPIRY DATE {{sslexpiry}}{{/sslexpiry}} 10 | 11 | access_log {{accesslog}} 12 | error_log {{errorlog}} 13 | {{#webroot}}Webroot {{webroot}}{{/webroot}} 14 | 15 | {{#dbname}} 16 | DB_NAME {{dbname}}{{/dbname}} 17 | {{#dbname}}DB_USER {{dbuser}}{{/dbname}} 18 | {{#dbname}}DB_PASS {{dbpass}}{{/dbname}} 19 | {{#tablepref}}table_prefix {{tableprefix}}{{/tablepref}} 20 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | --- 2 | only: issues 3 | limitPerRun: 30 4 | daysUntilStale: 45 5 | daysUntilClose: 60 6 | exemptLabels: 7 | - bug 8 | - In progress 9 | - planned 10 | - enhancement 11 | exemptProjects: true 12 | exemptMilestones: true 13 | staleLabel: stale 14 | markComment: > 15 | Currently WordOps team doesn't have enough capacity to work on this issue. 16 | We will be more than glad to accept a pull request with a solution to problem described here. 17 | This issue will be closed after another 60 days of inactivity. 18 | closeComment: > 19 | This issue has been automatically closed due to extended period of inactivity. 20 | Please reopen if it is still valid. Thank you for your contributions. 21 | -------------------------------------------------------------------------------- /wo/cli/templates/php-pool.mustache: -------------------------------------------------------------------------------- 1 | [{{pool}}] 2 | user = {{user}} 3 | group = {{group}} 4 | listen = {{listen}} 5 | listen.owner = {{listenuser}} 6 | listen.group = {{listengroup}} 7 | pm = ondemand 8 | pm.max_children = 50 9 | pm.start_servers = 10 10 | pm.min_spare_servers = 5 11 | pm.max_spare_servers = 15 12 | ping.path = /ping 13 | pm.status_path = /status 14 | pm.max_requests = 1500 15 | request_terminate_timeout = 300 16 | chdir = / 17 | prefix = /var/run/php 18 | listen.mode = 0660 19 | listen.backlog = 32768 20 | catch_workers_output = yes 21 | 22 | 23 | {{#openbasedir}}php_admin_value[open_basedir] = "/var/www/:/usr/share/php/:/tmp/:/var/run/nginx-cache/:/dev/urandom:/dev/shm:/var/lib/php/sessions/"{{/openbasedir}} 24 | -------------------------------------------------------------------------------- /wo/cli/templates/wpsc.mustache: -------------------------------------------------------------------------------- 1 | # WPSC NGINX CONFIGURATION - WordOps {{release}} 2 | # DO NOT MODIFY, ALL CHANGES WILL BE LOST AFTER AN WordOps (wo) UPDATE 3 | # $cache_uri variable set in /etc/nginx/conf.d/map-wp.conf 4 | 5 | # Use cached or actual file if they exists, Otherwise pass request to WordPress 6 | location / { 7 | # If we add index.php?$args its break WooCommerce like plugins 8 | # Ref: #330 9 | try_files /wp-content/cache/supercache/$http_host/$cache_uri/index.html $uri $uri/ /index.php; 10 | } 11 | location ~ \.php$ { 12 | try_files $uri =404; 13 | include fastcgi_params; 14 | fastcgi_pass {{upstream}}; 15 | # Following line is needed by WP Super Cache plugin 16 | fastcgi_param SERVER_NAME $http_host; 17 | } 18 | -------------------------------------------------------------------------------- /wo/core/nginx.py: -------------------------------------------------------------------------------- 1 | """WordOps Nginx Manager""" 2 | import subprocess 3 | 4 | from wo.core.logging import Log 5 | 6 | 7 | def check_config(self): 8 | """Check Nginx configuration and return boolean""" 9 | Log.debug(self, "Testing Nginx configuration ") 10 | # Check Nginx configuration before executing command 11 | sub = subprocess.Popen('nginx -t', stdout=subprocess.PIPE, 12 | stderr=subprocess.PIPE, shell=True) 13 | output, error_output = sub.communicate() 14 | if 'emerg' in str(error_output): 15 | Log.debug(self, "Nginx configuration check failed") 16 | return False 17 | else: 18 | Log.debug(self, "Nginx configuration check was successful") 19 | return True 20 | -------------------------------------------------------------------------------- /wo/cli/templates/wpce.mustache: -------------------------------------------------------------------------------- 1 | # WPCE NGINX CONFIGURATION - WordOps {{release}} 2 | # DO NOT MODIFY, ALL CHANGES WILL BE LOST AFTER AN WordOps (wo) UPDATE 3 | # $cache_uri variable set in /etc/nginx/conf.d/map-wp.conf 4 | # Use cached or actual file if they exists, Otherwise pass request to WordPress 5 | location / { 6 | try_files /wp-content/cache/cache-enabler/${http_host}${cache_uri}index.html $uri $uri/ /index.php$is_args$args; 7 | } 8 | location ~ \.php$ { 9 | try_files $uri =404; 10 | include fastcgi_params; 11 | fastcgi_pass {{upstream}}; 12 | } 13 | location ~ /wp-content/cache/cache-enabler/*\.html$ { 14 | etag on; 15 | add_header Vary "Accept-Encoding, Cookie"; 16 | access_log off; 17 | log_not_found off; 18 | expires 10h; 19 | } -------------------------------------------------------------------------------- /wo/cli/controllers/base.py: -------------------------------------------------------------------------------- 1 | """WordOps base controller.""" 2 | 3 | from cement.core.controller import CementBaseController, expose 4 | 5 | from wo.core.variables import WOVar 6 | 7 | VERSION = WOVar.wo_version 8 | 9 | BANNER = """ 10 | WordOps v%s 11 | Copyright (c) 2024 WordOps. 12 | """ % VERSION 13 | 14 | 15 | class WOBaseController(CementBaseController): 16 | class Meta: 17 | label = 'base' 18 | description = ("An essential toolset that eases WordPress " 19 | "site and server administration with Nginx") 20 | arguments = [ 21 | (['-v', '--version'], dict(action='version', version=BANNER)), 22 | ] 23 | 24 | @expose(hide=True) 25 | def default(self): 26 | self.app.args.print_help() 27 | -------------------------------------------------------------------------------- /tests/cli/14_test_stack_services_stop.py: -------------------------------------------------------------------------------- 1 | from wo.utils import test 2 | from wo.cli.main import WOTestApp 3 | 4 | 5 | class CliTestCaseStackStop(test.WOTestCase): 6 | 7 | def test_wo_cli_stack_services_stop_nginx(self): 8 | with WOTestApp(argv=['stack', 'stop', '--nginx']) as app: 9 | app.run() 10 | 11 | def test_wo_cli_stack_services_stop_php_fpm(self): 12 | with WOTestApp(argv=['stack', 'stop', '--php']) as app: 13 | app.run() 14 | 15 | def test_wo_cli_stack_services_stop_mysql(self): 16 | with WOTestApp(argv=['stack', 'stop', '--mysql']) as app: 17 | app.run() 18 | 19 | def test_wo_cli_stack_services_stop_all(self): 20 | with WOTestApp(argv=['stack', 'stop']) as app: 21 | app.run() 22 | -------------------------------------------------------------------------------- /tests/cli/15_test_stack_services_start.py: -------------------------------------------------------------------------------- 1 | from wo.utils import test 2 | from wo.cli.main import WOTestApp 3 | 4 | 5 | class CliTestCaseStackStart(test.WOTestCase): 6 | 7 | def test_wo_cli_stack_services_start_nginx(self): 8 | with WOTestApp(argv=['stack', 'start', '--nginx']) as app: 9 | app.run() 10 | 11 | def test_wo_cli_stack_services_start_php_fpm(self): 12 | with WOTestApp(argv=['stack', 'start', '--php']) as app: 13 | app.run() 14 | 15 | def test_wo_cli_stack_services_start_mysql(self): 16 | with WOTestApp(argv=['stack', 'start', '--mysql']) as app: 17 | app.run() 18 | 19 | def test_wo_cli_stack_services_start_all(self): 20 | with WOTestApp(argv=['stack', 'start']) as app: 21 | app.run() 22 | -------------------------------------------------------------------------------- /wo/cli/templates/wpfc.mustache: -------------------------------------------------------------------------------- 1 | # WPFC NGINX CONFIGURATION - WordOps {{release}} 2 | # DO NOT MODIFY, ALL CHANGES WILL BE LOST AFTER AN WordOps (wo) UPDATE 3 | # $skip_cache variable set in /etc/nginx/conf.d/map-wp.conf 4 | 5 | add_header X-fastcgi-cache $upstream_cache_status; 6 | # Use cached or actual file if they exists, Otherwise pass request to WordPress 7 | location / { 8 | try_files $uri $uri/ /index.php$is_args$args; 9 | } 10 | location ~ \.php$ { 11 | try_files $uri =404; 12 | include fastcgi_params; 13 | fastcgi_pass {{upstream}}; 14 | fastcgi_cache_bypass $skip_cache; 15 | fastcgi_no_cache $skip_cache; 16 | fastcgi_cache WORDPRESS; 17 | } 18 | location ~ /purge(/.*) { 19 | fastcgi_cache_purge WORDPRESS "$scheme$request_method$host$1"; 20 | access_log off; 21 | } 22 | -------------------------------------------------------------------------------- /tests/cli/17_test_stack_services_status.py: -------------------------------------------------------------------------------- 1 | from wo.utils import test 2 | from wo.cli.main import WOTestApp 3 | 4 | 5 | class CliTestCaseStackStatus(test.WOTestCase): 6 | 7 | def test_wo_cli_stack_services_status_nginx(self): 8 | with WOTestApp(argv=['stack', 'status', '--nginx']) as app: 9 | app.run() 10 | 11 | def test_wo_cli_stack_services_status_php_fpm(self): 12 | with WOTestApp(argv=['stack', 'status', '--php']) as app: 13 | app.run() 14 | 15 | def test_wo_cli_stack_services_status_mysql(self): 16 | with WOTestApp(argv=['stack', 'status', '--mysql']) as app: 17 | app.run() 18 | 19 | def test_wo_cli_stack_services_status_all(self): 20 | with WOTestApp(argv=['stack', 'status']) as app: 21 | app.run() 22 | -------------------------------------------------------------------------------- /tests/cli/16_test_stack_services_restart.py: -------------------------------------------------------------------------------- 1 | from wo.utils import test 2 | from wo.cli.main import WOTestApp 3 | 4 | 5 | class CliTestCaseStackRestart(test.WOTestCase): 6 | 7 | def test_wo_cli_stack_services_restart_nginx(self): 8 | with WOTestApp(argv=['stack', 'restart', '--nginx']) as app: 9 | app.run() 10 | 11 | def test_wo_cli_stack_services_restart_php_fpm(self): 12 | with WOTestApp(argv=['stack', 'restart', '--php']) as app: 13 | app.run() 14 | 15 | def test_wo_cli_stack_services_restart_mysql(self): 16 | with WOTestApp(argv=['stack', 'restart', '--mysql']) as app: 17 | app.run() 18 | 19 | def test_wo_cli_stack_services_restart_all(self): 20 | with WOTestApp(argv=['stack', 'restart']) as app: 21 | app.run() 22 | -------------------------------------------------------------------------------- /wo/cli/templates/tweaks.mustache: -------------------------------------------------------------------------------- 1 | # NGINX Tweaks - WordOps {{release}} 2 | directio 4m; 3 | directio_alignment 512; 4 | large_client_header_buffers 8 64k; 5 | 6 | postpone_output 1460; 7 | proxy_buffers 8 32k; 8 | proxy_buffer_size 64k; 9 | 10 | sendfile on; 11 | sendfile_max_chunk 512k; 12 | 13 | tcp_nopush on; 14 | tcp_nodelay on; 15 | 16 | keepalive_requests 500; 17 | keepalive_disable msie6; 18 | 19 | lingering_time 20s; 20 | lingering_timeout 5s; 21 | 22 | open_file_cache max=50000 inactive=60s; 23 | open_file_cache_errors off; 24 | open_file_cache_min_uses 2; 25 | open_file_cache_valid 120s; 26 | open_log_file_cache max=10000 inactive=30s min_uses=2; 27 | 28 | ssl_dyn_rec_size_hi 4229; 29 | ssl_dyn_rec_size_lo 1369; 30 | ssl_dyn_rec_threshold 40; 31 | ssl_dyn_rec_timeout 1000; 32 | -------------------------------------------------------------------------------- /tests/cli/25_test_clean.py: -------------------------------------------------------------------------------- 1 | from wo.utils import test 2 | from wo.cli.main import WOTestApp 3 | 4 | 5 | class CliTestCaseClean(test.WOTestCase): 6 | 7 | def test_wo_cli_clean(self): 8 | with WOTestApp(argv=['clean']) as app: 9 | app.run() 10 | 11 | def test_wo_cli_clean_fastcgi(self): 12 | with WOTestApp(argv=['clean', '--fastcgi']) as app: 13 | app.run() 14 | 15 | def test_wo_cli_clean_all(self): 16 | with WOTestApp(argv=['clean', '--all']) as app: 17 | app.run() 18 | 19 | def test_wo_cli_clean_opcache(self): 20 | with WOTestApp(argv=['clean', '--opcache']) as app: 21 | app.run() 22 | 23 | def test_wo_cli_clean_redis(self): 24 | with WOTestApp(argv=['clean', '--redis']) as app: 25 | app.run() 26 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: wordops 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/workflows/pypi.yml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v1 12 | - name: Set up Python 13 | uses: actions/setup-python@v1 14 | with: 15 | python-version: '3.x' 16 | - name: Install dependencies 17 | run: | 18 | python3 -m pip install --upgrade pip 19 | python3 -m pip install setuptools wheel twine 20 | python3 -m pip install --upgrade setuptools wheel twine 21 | - name: Build and publish 22 | env: 23 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 24 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 25 | run: | 26 | python3 setup.py sdist bdist_wheel 27 | twine upload dist/* 28 | -------------------------------------------------------------------------------- /wo/cli/templates/wprocket.mustache: -------------------------------------------------------------------------------- 1 | # WPROCKET NGINX CONFIGURATION - WordOps {{release}} 2 | # DO NOT MODIFY, ALL CHANGES WILL BE LOST AFTER AN WordOps (wo) UPDATE 3 | # $cache_uri variable set in /etc/nginx/conf.d/map-wp.conf 4 | # Use cached or actual file if they exists, Otherwise pass request to WordPress 5 | location / { 6 | try_files /wp-content/cache/wp-rocket/$http_host/$cache_uri/index${mobile_prefix}${https_prefix}.html /wp-content/cache/wp-rocket/$http_host/$cache_uri/index${https_prefix}.html $uri $uri/ /index.php$is_args$args; 7 | } 8 | location ~ \.php$ { 9 | try_files $uri =404; 10 | include fastcgi_params; 11 | fastcgi_pass {{upstream}}; 12 | } 13 | location ~ /wp-content/cache/wp-rocket/*\.html$ { 14 | etag on; 15 | gzip_static on; 16 | add_header Vary "Accept-Encoding, Cookie"; 17 | access_log off; 18 | log_not_found off; 19 | expires 10h; 20 | } 21 | -------------------------------------------------------------------------------- /wo/cli/templates/fastcgi.mustache: -------------------------------------------------------------------------------- 1 | # FastCGI cache settings 2 | fastcgi_cache_path /var/run/nginx-cache levels=1:2 keys_zone=WORDPRESS:50m inactive=6h max_size=256M; 3 | fastcgi_cache_key "$scheme$request_method$host$request_uri"; 4 | fastcgi_cache_use_stale error timeout invalid_header updating http_500 http_503; 5 | fastcgi_cache_lock on; 6 | fastcgi_cache_lock_age 5s; 7 | fastcgi_cache_lock_timeout 5s; 8 | fastcgi_cache_methods GET HEAD; 9 | fastcgi_cache_background_update on; 10 | fastcgi_cache_valid 200 24h; 11 | fastcgi_cache_valid 301 302 30m; 12 | fastcgi_cache_valid 499 502 503 1m; 13 | fastcgi_cache_valid 404 1h; 14 | fastcgi_cache_valid any 1h; 15 | fastcgi_buffers 16 16k; 16 | fastcgi_buffer_size 32k; 17 | fastcgi_param SERVER_NAME $http_host; 18 | fastcgi_ignore_headers Cache-Control Expires Set-Cookie; 19 | fastcgi_keep_conn on; 20 | # only available with Nginx 1.15.6 and earlier 21 | fastcgi_socket_keepalive on; 22 | -------------------------------------------------------------------------------- /wo/cli/templates/cloudflare.mustache: -------------------------------------------------------------------------------- 1 | # WordOps (wo) set visitors real ip with Cloudflare 2 | set_real_ip_from 173.245.48.0/20; 3 | set_real_ip_from 103.21.244.0/22; 4 | set_real_ip_from 103.22.200.0/22; 5 | set_real_ip_from 103.31.4.0/22; 6 | set_real_ip_from 141.101.64.0/18; 7 | set_real_ip_from 108.162.192.0/18; 8 | set_real_ip_from 190.93.240.0/20; 9 | set_real_ip_from 188.114.96.0/20; 10 | set_real_ip_from 197.234.240.0/22; 11 | set_real_ip_from 198.41.128.0/17; 12 | set_real_ip_from 162.158.0.0/15; 13 | set_real_ip_from 172.64.0.0/13; 14 | set_real_ip_from 131.0.72.0/22; 15 | set_real_ip_from 104.16.0.0/13; 16 | set_real_ip_from 104.24.0.0/14; 17 | set_real_ip_from 2400:cb00::/32; 18 | set_real_ip_from 2606:4700::/32; 19 | set_real_ip_from 2803:f800::/32; 20 | set_real_ip_from 2405:b500::/32; 21 | set_real_ip_from 2405:8100::/32; 22 | set_real_ip_from 2a06:98c0::/29; 23 | set_real_ip_from 2c0f:f248::/32; 24 | real_ip_header CF-Connecting-IP; 25 | -------------------------------------------------------------------------------- /tests/cli/29_test_site_delete.py: -------------------------------------------------------------------------------- 1 | from wo.utils import test 2 | from wo.cli.main import WOTestApp 3 | 4 | 5 | class CliTestCaseSiteDelete(test.WOTestCase): 6 | 7 | def test_wo_cli_site_detele(self): 8 | with WOTestApp(argv=['site', 'delete', 'html.com', 9 | '--force']) as app: 10 | app.run() 11 | 12 | def test_wo_cli_site_detele_all(self): 13 | with WOTestApp(argv=['site', 'delete', 'wp.com', 14 | '--all', '--force']) as app: 15 | app.run() 16 | 17 | def test_wo_cli_site_detele_db(self): 18 | with WOTestApp(argv=['site', 'delete', 'mysql.com', 19 | '--db', '--force']) as app: 20 | app.run() 21 | 22 | def test_wo_cli_site_detele_files(self): 23 | with WOTestApp(argv=['site', 'delete', 'php.com', 24 | '--files', '--force']) as app: 25 | app.run() 26 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: 'Close stale issues and PRs' 2 | on: 3 | schedule: 4 | - cron: '30 1 * * *' 5 | 6 | jobs: 7 | stale: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/stale@v8 11 | with: 12 | stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.' 13 | stale-pr-message: 'This PR is stale because it has been open 45 days with no activity. Remove stale label or comment or this will be closed in 10 days.' 14 | close-issue-message: 'This issue was closed because it has been stalled for 5 days with no activity.' 15 | close-pr-message: 'This PR was closed because it has been stalled for 10 days with no activity.' 16 | days-before-issue-stale: 30 17 | days-before-pr-stale: 45 18 | days-before-issue-close: 5 19 | days-before-pr-close: 10 20 | operations-per-run: 100 21 | -------------------------------------------------------------------------------- /tests/issue.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # ------------------------------------------------------------------------- 3 | # WordOps support script 4 | # ------------------------------------------------------------------------- 5 | # Website: https://wordops.net 6 | # GitHub: https://github.com/WordOps/WordOps 7 | # Copyright (c) 2019 - WordOps 8 | # This script is licensed under M.I.T 9 | # ------------------------------------------------------------------------- 10 | # curl -sL git.io/fjAp3 | sudo -E bash - 11 | # ------------------------------------------------------------------------- 12 | # Version 3.9.8.4 - 2019-08-28 13 | # ------------------------------------------------------------------------- 14 | 15 | if [ -f /var/log/wo/wordops.log ]; then 16 | cd /var/log/wo/ || exit 1 17 | wo_link=$(curl -sL --upload-file wordops.log https://transfer.vtbox.net/wordops.txt) 18 | echo 19 | echo "Here the link to provide in your github issue : $wo_link" 20 | echo 21 | cd || exit 1 22 | fi 23 | -------------------------------------------------------------------------------- /wo/cli/templates/proftpd-tls.mustache: -------------------------------------------------------------------------------- 1 | 2 | 3 | TLSEngine on 4 | TLSRequired on 5 | TLSLog /var/log/proftpd/tls.log 6 | 7 | TLSDHParamFile /etc/proftpd/dhparams.pem 8 | 9 | # intermediate configuration from ssl-config.mozilla.org 10 | TLSProtocol TLSv1.2 11 | TLSCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384 12 | TLSServerCipherPreference off 13 | TLSSessionTickets off 14 | TLSOptions NoCertRequest AllowClientRenegotiations NoSessionReuseRequired 15 | 16 | TLSRSACertificateFile /etc/proftpd/ssl/proftpd.crt 17 | TLSRSACertificateKeyFile /etc/proftpd/ssl/proftpd.key 18 | 19 | TLSVerifyClient off 20 | RequireValidShell no 21 | 22 | -------------------------------------------------------------------------------- /wo/cli/templates/brotli.mustache: -------------------------------------------------------------------------------- 1 | ## 2 | # Brotli Settings 3 | ## 4 | 5 | brotli on; 6 | brotli_static on; 7 | brotli_comp_level 6; 8 | brotli_types 9 | application/atom+xml 10 | application/geo+json 11 | application/javascript 12 | application/json 13 | application/ld+json 14 | application/manifest+json 15 | application/rdf+xml 16 | application/rss+xml 17 | application/vnd.ms-fontobject 18 | application/wasm 19 | application/x-font-opentype 20 | application/x-font-truetype 21 | application/x-font-ttf 22 | application/x-javascript 23 | application/x-web-app-manifest+json 24 | application/xhtml+xml 25 | application/xml 26 | application/xml+rss 27 | font/eot 28 | font/opentype 29 | font/otf 30 | image/bmp 31 | image/svg+xml 32 | image/vnd.microsoft.icon 33 | image/x-icon 34 | image/x-win-bitmap 35 | text/cache-manifest 36 | text/calendar 37 | text/css 38 | text/javascript 39 | text/markdown 40 | text/plain 41 | text/vcard 42 | text/vnd.rim.location.xloc 43 | text/vtt 44 | text/x-component 45 | text/x-cross-domain-policy 46 | text/xml; 47 | -------------------------------------------------------------------------------- /wo/cli/plugins/import_slow_log.py: -------------------------------------------------------------------------------- 1 | from cement.core.controller import CementBaseController, expose 2 | 3 | from wo.core.logging import Log 4 | 5 | 6 | def wo_import_slow_log_hook(app): 7 | pass 8 | 9 | 10 | class WOImportslowlogController(CementBaseController): 11 | class Meta: 12 | label = 'import_slow_log' 13 | stacked_on = 'base' 14 | stacked_type = 'nested' 15 | description = 'Import MySQL slow log to Anemometer database' 16 | usage = "wo import-slow-log" 17 | 18 | @expose(hide=True) 19 | def default(self): 20 | Log.info(self, "This command is deprecated." 21 | " You can use this command instead, " + 22 | Log.ENDC + Log.BOLD + "\n`wo debug --import-slow-log`" + 23 | Log.ENDC) 24 | 25 | 26 | def load(app): 27 | # register the plugin class.. this only happens if the plugin is enabled 28 | app.handler.register(WOImportslowlogController) 29 | 30 | # register a hook (function) to run after arguments are parsed. 31 | app.hook.register('post_argument_parsing', wo_import_slow_log_hook) 32 | -------------------------------------------------------------------------------- /wo/cli/templates/gzip.mustache: -------------------------------------------------------------------------------- 1 | ## 2 | # Gzip Settings 3 | ## 4 | 5 | gzip on; 6 | gzip_disable "msie6"; 7 | 8 | gzip_vary on; 9 | gzip_static on; 10 | gzip_proxied any; 11 | gzip_comp_level 6; 12 | gzip_buffers 16 8k; 13 | gzip_http_version 1.1; 14 | gzip_types 15 | application/atom+xml 16 | application/geo+json 17 | application/javascript 18 | application/json 19 | application/ld+json 20 | application/manifest+json 21 | application/rdf+xml 22 | application/rss+xml 23 | application/vnd.ms-fontobject 24 | application/wasm 25 | application/x-font-opentype 26 | application/x-font-truetype 27 | application/x-font-ttf 28 | application/x-javascript 29 | application/x-web-app-manifest+json 30 | application/xhtml+xml 31 | application/xml 32 | application/xml+rss 33 | font/eot 34 | font/opentype 35 | font/otf 36 | image/bmp 37 | image/svg+xml 38 | image/vnd.microsoft.icon 39 | image/x-icon 40 | image/x-win-bitmap 41 | text/cache-manifest 42 | text/calendar 43 | text/css 44 | text/javascript 45 | text/markdown 46 | text/plain 47 | text/vcard 48 | text/vnd.rim.location.xloc 49 | text/vtt 50 | text/x-component 51 | text/x-cross-domain-policy 52 | text/xml; -------------------------------------------------------------------------------- /wo/core/sendmail.py: -------------------------------------------------------------------------------- 1 | import os 2 | import smtplib 3 | from email import encoders 4 | from email.mime.base import MIMEBase 5 | from email.mime.multipart import MIMEMultipart 6 | from email.mime.text import MIMEText 7 | from email.utils import COMMASPACE, formatdate 8 | 9 | 10 | def WOSendMail(send_from, send_to, subject, text, files, server="localhost", 11 | port=587, username='', password='', isTls=True): 12 | msg = MIMEMultipart() 13 | msg['From'] = send_from 14 | msg['To'] = send_to 15 | msg['Date'] = formatdate(localtime=True) 16 | msg['Subject'] = subject 17 | 18 | msg.attach(MIMEText(text)) 19 | 20 | for file in files: 21 | part = MIMEBase('application', "octet-stream") 22 | with open(file, 'rb') as f: 23 | part.set_payload(f.read()) 24 | encoders.encode_base64(part) 25 | part.add_header('Content-Disposition', 'attachment; filename="{0}"' 26 | .format(os.path.basename(f))) 27 | msg.attach(part) 28 | 29 | smtp = smtplib.SMTP(server, port) 30 | if isTls: 31 | smtp.starttls() 32 | 33 | smtp.sendmail(send_from, send_to, msg.as_string()) 34 | smtp.quit() 35 | -------------------------------------------------------------------------------- /wo/cli/templates/webp.mustache: -------------------------------------------------------------------------------- 1 | # WEBP NGINX CONFIGURATION - WordOps {{release}} 2 | # DO NOT MODIFY, ALL CHANGES WILL BE LOST AFTER AN WordOps (wo) UPDATE 3 | 4 | map $http_accept $webp_suffix_valid { 5 | default 1; 6 | "~*webp" 0; 7 | } 8 | 9 | map $realip_remote_addr $webp_suffix_cf { 10 | default 0; 11 | 103.21.244.0/22 1; 12 | 103.22.200.0/22 1; 13 | 103.31.4.0/22 1; 14 | 104.16.0.0/12 1; 15 | 108.162.192.0/18 1; 16 | 131.0.72.0/22 1; 17 | 141.101.64.0/18 1; 18 | 162.158.0.0/15 1; 19 | 172.64.0.0/13 1; 20 | 173.245.48.0/20 1; 21 | 188.114.96.0/20 1; 22 | 190.93.240.0/20 1; 23 | 197.234.240.0/22 1; 24 | 198.41.128.0/17 1; 25 | 199.27.128.0/21 1; 26 | 2400:cb00::/32 1; 27 | 2405:8100::/32 1; 28 | 2405:b500::/32 1; 29 | 2606:4700::/32 1; 30 | 2803:f800::/32 1; 31 | 2a06:98c0::/29 1; 32 | 2c0f:f248::/32 1; 33 | 34 | } 35 | 36 | map $webp_suffix_cf$webp_suffix_valid $webp_suffix { 37 | default ""; 38 | 00 ".webp"; 39 | } -------------------------------------------------------------------------------- /wo/cli/templates/avif.mustache: -------------------------------------------------------------------------------- 1 | # avif NGINX CONFIGURATION - WordOps {{release}} 2 | # DO NOT MODIFY, ALL CHANGES WILL BE LOST AFTER AN WordOps (wo) UPDATE 3 | 4 | map $http_accept $avif_suffix_valid { 5 | default 1; 6 | "~*avif" 0; 7 | } 8 | 9 | map $realip_remote_addr $avif_suffix_cf { 10 | default 0; 11 | 103.21.244.0/22 1; 12 | 103.22.200.0/22 1; 13 | 103.31.4.0/22 1; 14 | 104.16.0.0/12 1; 15 | 108.162.192.0/18 1; 16 | 131.0.72.0/22 1; 17 | 141.101.64.0/18 1; 18 | 162.158.0.0/15 1; 19 | 172.64.0.0/13 1; 20 | 173.245.48.0/20 1; 21 | 188.114.96.0/20 1; 22 | 190.93.240.0/20 1; 23 | 197.234.240.0/22 1; 24 | 198.41.128.0/17 1; 25 | 199.27.128.0/21 1; 26 | 2400:cb00::/32 1; 27 | 2405:8100::/32 1; 28 | 2405:b500::/32 1; 29 | 2606:4700::/32 1; 30 | 2803:f800::/32 1; 31 | 2a06:98c0::/29 1; 32 | 2c0f:f248::/32 1; 33 | 34 | } 35 | 36 | map $avif_suffix_cf$avif_suffix_valid $avif_suffix { 37 | default ".notexists"; 38 | 00 ".avif"; 39 | } -------------------------------------------------------------------------------- /wo/core/database.py: -------------------------------------------------------------------------------- 1 | """WordOps generic database creation module""" 2 | from sqlalchemy import create_engine 3 | from sqlalchemy.ext.declarative import declarative_base 4 | from sqlalchemy.orm import scoped_session, sessionmaker 5 | 6 | from wo.core.variables import WOVar 7 | 8 | # db_path = self.app.config.get('site', 'db_path') 9 | engine = create_engine(WOVar.wo_db_uri, convert_unicode=True) 10 | db_session = scoped_session(sessionmaker(autocommit=False, 11 | autoflush=False, 12 | bind=engine)) 13 | Base = declarative_base() 14 | Base.query = db_session.query_property() 15 | 16 | 17 | def init_db(app): 18 | """ 19 | Initializes and creates all tables from models into the database 20 | """ 21 | # import all modules here that might define models so that 22 | # they will be registered properly on the metadata. Otherwise 23 | # # you will have to import them first before calling init_db() 24 | # import wo.core.models 25 | try: 26 | app.log.info("Initializing WordOps Database") 27 | Base.metadata.create_all(bind=engine) 28 | except Exception as e: 29 | app.log.debug("{0}".format(e)) 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018-2019 WordOps (https://github.com/WordOps/WordOps/graphs/contributors) 4 | Copyright (C) 2011-2017 EE Development Group (https://github.com/ee/ee/contributors) 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /wo/core/checkfqdn.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | from wo.core.shellexec import WOShellExec 4 | from wo.core.variables import WOVar 5 | 6 | 7 | def check_fqdn(self, wo_host): 8 | """FQDN check with WordOps, for mail server hostname must be FQDN""" 9 | # wo_host=os.popen("hostname -f | tr -d '\n'").read() 10 | if '.' in wo_host: 11 | WOVar.wo_fqdn = wo_host 12 | with open('/etc/hostname', encoding='utf-8', mode='w') as hostfile: 13 | hostfile.write(wo_host) 14 | 15 | WOShellExec.cmd_exec(self, "sed -i \"1i\\127.0.0.1 {0}\" /etc/hosts" 16 | .format(wo_host)) 17 | if WOVar.wo_distro == 'debian': 18 | WOShellExec.cmd_exec(self, "/etc/init.d/hostname.sh start") 19 | else: 20 | WOShellExec.cmd_exec(self, "service hostname restart") 21 | 22 | else: 23 | wo_host = input("Enter hostname [fqdn]:") 24 | check_fqdn(self, wo_host) 25 | 26 | 27 | def check_fqdn_ip(self): 28 | """Check if server hostname resolved server IP""" 29 | x = requests.get('http://v4.wordops.eu') 30 | ip = (x.text).strip() 31 | 32 | wo_fqdn = WOVar.wo_fqdn 33 | y = requests.get('http://v4.wordops.eu/dns/{0}/'.format(wo_fqdn)) 34 | ip_fqdn = (y.text).strip() 35 | 36 | return bool(ip == ip_fqdn) 37 | -------------------------------------------------------------------------------- /wo/cli/templates/redis.mustache: -------------------------------------------------------------------------------- 1 | # Redis NGINX CONFIGURATION - WordOps {{release}} 2 | # DO NOT MODIFY, ALL CHANGES WILL BE LOST AFTER AN WordOps (wo) UPDATE 3 | # $skip_cache variable set in /etc/nginx/conf.d/map-wp.conf 4 | 5 | # Use cached or actual file if they exists, Otherwise pass request to WordPress 6 | location / { 7 | try_files $uri $uri/ /index.php$is_args$args; 8 | } 9 | 10 | location /redis-fetch { 11 | internal ; 12 | set $redis_key $args; 13 | redis_pass redis; 14 | } 15 | location /redis-store { 16 | internal ; 17 | set_unescape_uri $key $arg_key ; 18 | redis2_query set $key $echo_request_body; 19 | redis2_query expire $key 14400; 20 | redis2_pass redis; 21 | 22 | } 23 | 24 | location ~ \.php$ { 25 | set $key "nginx-cache:$scheme$request_method$host$request_uri"; 26 | try_files $uri =404; 27 | 28 | srcache_fetch_skip $skip_cache; 29 | srcache_store_skip $skip_cache; 30 | 31 | srcache_response_cache_control off; 32 | 33 | set_escape_uri $escaped_key $key; 34 | 35 | srcache_fetch GET /redis-fetch $key; 36 | srcache_store PUT /redis-store key=$escaped_key; 37 | 38 | more_set_headers 'X-SRCache-Fetch-Status $srcache_fetch_status'; 39 | more_set_headers 'X-SRCache-Store-Status $srcache_store_status'; 40 | 41 | include fastcgi_params; 42 | fastcgi_pass {{upstream}}; 43 | } 44 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - updating-configuration 7 | - master 8 | pull_request: 9 | branches: 10 | - master 11 | release: 12 | types: [published] 13 | schedule: 14 | - cron: '0 0 * * 0' 15 | 16 | jobs: 17 | my_job: 18 | name: test WordOps 19 | runs-on: ${{ matrix.os }} 20 | strategy: 21 | matrix: 22 | os: [ubuntu-20.04, ubuntu-22.04] 23 | 24 | steps: 25 | - uses: actions/checkout@v3 26 | - name: Prepare VM 27 | run: | 28 | unset LANG 29 | sudo apt update -qq > /dev/null 2>&1 30 | sudo LC_ALL=C.UTF-8 add-apt-repository ppa:ondrej/php -y > /dev/null 2>&1 31 | sudo rm -rf /etc/mysql /var/lib/mysql 32 | sudo apt-get purge --option=Dpkg::options::=--force-all --assume-yes graphviz* redis* php* mysql* nginx* > /dev/null 2>&1 33 | sudo apt-get install -qq git ccze tree > /dev/null 2>&1 34 | sudo apt-get -qq autoremove --purge > /dev/null 2>&1 35 | sudo bash -c 'echo -e "[user]\n\tname = abc\n\temail = root@localhost.com" > $HOME/.gitconfig' 36 | - name: Install WordOps 37 | run: sudo timeout 1800 bash install --travis 38 | - name: Run tests 39 | run: sudo timeout 1800 bash tests/travis.sh --actions 40 | - name: Display log 41 | run: sudo cat /var/log/wo/test.log | ccze -A 42 | -------------------------------------------------------------------------------- /tests/cli/13_test_stack_install.py: -------------------------------------------------------------------------------- 1 | from wo.utils import test 2 | from wo.cli.main import WOTestApp 3 | 4 | 5 | class CliTestCaseStackInstall(test.WOTestCase): 6 | 7 | def test_wo_cli_stack_install_nginx(self): 8 | with WOTestApp(argv=['stack', 'install', '--nginx']) as app: 9 | app.run() 10 | 11 | def test_wo_cli_stack_install_php(self): 12 | with WOTestApp(argv=['stack', 'install', '--php']) as app: 13 | app.run() 14 | 15 | def test_wo_cli_stack_install_php73(self): 16 | with WOTestApp(argv=['stack', 'install', '--php73']) as app: 17 | app.run() 18 | 19 | def test_wo_cli_stack_install_mysql(self): 20 | with WOTestApp(argv=['stack', 'install', '--mysql']) as app: 21 | app.run() 22 | 23 | def test_wo_cli_stack_install_wpcli(self): 24 | with WOTestApp(argv=['stack', 'install', '--wpcli']) as app: 25 | app.run() 26 | 27 | def test_wo_cli_stack_install_phpmyadmin(self): 28 | with WOTestApp(argv=['stack', 'install', '--phpmyadmin']) as app: 29 | app.run() 30 | 31 | def test_wo_cli_stack_install_adminer(self): 32 | with WOTestApp(argv=['stack', 'install', '--adminer']) as app: 33 | app.run() 34 | 35 | def test_wo_cli_stack_install_utils(self): 36 | with WOTestApp(argv=['stack', 'install', '--utils']) as app: 37 | app.run() 38 | -------------------------------------------------------------------------------- /tests/cli/30_test_stack_remove.py: -------------------------------------------------------------------------------- 1 | from wo.utils import test 2 | from wo.cli.main import WOTestApp 3 | 4 | 5 | class CliTestCaseStackRemove(test.WOTestCase): 6 | 7 | def test_wo_cli_stack_remove_admin(self): 8 | with WOTestApp(argv=['stack', 'remove', '--admin', '--force']) as app: 9 | app.run() 10 | 11 | def test_wo_cli_stack_remove_nginx(self): 12 | with WOTestApp(argv=['stack', 'remove', '--nginx', '--force']) as app: 13 | app.run() 14 | 15 | def test_wo_cli_stack_remove_php(self): 16 | with WOTestApp(argv=['stack', 'remove', '--php', '--force']) as app: 17 | app.run() 18 | 19 | def test_wo_cli_stack_remove_mysql(self): 20 | with WOTestApp(argv=['stack', 'remove', '--mysql', '--force']) as app: 21 | app.run() 22 | 23 | def test_wo_cli_stack_remove_wpcli(self): 24 | with WOTestApp(argv=['stack', 'remove', '--wpcli', '--force']) as app: 25 | app.run() 26 | 27 | def test_wo_cli_stack_remove_phpmyadmin(self): 28 | with WOTestApp(argv=['stack', 'remove', 29 | '--phpmyadmin', '--force']) as app: 30 | app.run() 31 | 32 | def test_wo_cli_stack_remove_adminer(self): 33 | with WOTestApp( 34 | argv=['stack', 'remove', '--adminer', '--force']) as app: 35 | app.run() 36 | 37 | def test_wo_cli_stack_remove_utils(self): 38 | with WOTestApp(argv=['stack', 'remove', '--utils', '--force']) as app: 39 | app.run() 40 | -------------------------------------------------------------------------------- /wo/cli/plugins/maintenance.py: -------------------------------------------------------------------------------- 1 | """Maintenance Plugin for WordOps""" 2 | 3 | from cement.core.controller import CementBaseController, expose 4 | 5 | from wo.core.aptget import WOAptGet 6 | from wo.core.logging import Log 7 | 8 | 9 | def wo_maintenance_hook(app): 10 | pass 11 | 12 | 13 | class WOMaintenanceController(CementBaseController): 14 | class Meta: 15 | label = 'maintenance' 16 | stacked_on = 'base' 17 | stacked_type = 'nested' 18 | description = ('update server packages to latest version') 19 | usage = "wo maintenance" 20 | 21 | @expose(hide=True) 22 | def default(self): 23 | 24 | try: 25 | Log.info(self, "updating apt-cache, please wait...") 26 | WOAptGet.update(self) 27 | Log.info(self, "updating packages, please wait...") 28 | WOAptGet.dist_upgrade(self) 29 | Log.info(self, "cleaning-up packages, please wait...") 30 | WOAptGet.auto_remove(self) 31 | WOAptGet.auto_clean(self) 32 | except OSError as e: 33 | Log.debug(self, str(e)) 34 | Log.error(self, "Package updates failed !") 35 | except Exception as e: 36 | Log.debug(self, str(e)) 37 | Log.error(self, "Packages updates failed !") 38 | 39 | 40 | def load(app): 41 | # register the plugin class.. this only happens if the plugin is enabled 42 | app.handler.register(WOMaintenanceController) 43 | # register a hook (function) to run after arguments are parsed. 44 | app.hook.register('post_argument_parsing', wo_maintenance_hook) 45 | -------------------------------------------------------------------------------- /wo/cli/templates/wo-update.mustache: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # script to update motd when a new WordOps release is available on Debian/Ubuntu 3 | # the script is added in /etc/update-motd.d 4 | 5 | safe_print() { 6 | cat "$1" | head -n 10 | tr -d '\000-\011\013\014\016-\037' | cut -c -80 7 | } 8 | 9 | # Ensure sane defaults 10 | [ -n "$URL" ] || URL="https://api.github.com/repos/WordOps/WordOps/releases/latest" 11 | [ -n "$WAIT" ] || WAIT=5 12 | [ -n "$CACHE" ] || CACHE="/var/cache/motd-wo" 13 | 14 | # Generate our temp files, clean up when done 15 | NEWS=$(mktemp) || exit 1 16 | ERR=$(mktemp) || exit 1 17 | CLOUD=$(mktemp) || exit 1 18 | trap "rm -f $NEWS $ERR $CLOUD" HUP INT QUIT ILL TRAP BUS TERM 19 | 20 | if [ -n "$(command -v curl)" ]; then 21 | LATEST_RELEASE=$(curl -m 5 --retry 3 -sL "$URL" | jq -r '.tag_name' 2>&1) 22 | fi 23 | if [ -n "$(command -v wo)" ]; then 24 | CURRENT_RELEASE=$(wo -v 2>&1 | grep v | awk -F " " '{print $2}') 25 | fi 26 | if [ -n "$CURRENT_RELEASE" ] && [ -n "$LATEST_RELEASE" ]; then 27 | if [ "$CURRENT_RELEASE" != "$LATEST_RELEASE" ]; then 28 | # display message with motd-news on Ubuntu 29 | echo '*** A new WordOps release is available ***' >"$NEWS" 2>"$ERR" 30 | 31 | echo 32 | # At most, 10 lines of text, remove control characters, print at most 80 characters per line 33 | safe_print "$NEWS" 34 | # Try to update the cache 35 | safe_print "$NEWS" 2>/dev/null >$CACHE || true 36 | else 37 | # clean news 38 | echo '' >"$NEWS" 2>"$ERR" 39 | safe_print "$NEWS" 2>/dev/null >$CACHE || true 40 | fi 41 | fi 42 | -------------------------------------------------------------------------------- /tests/cli/24_test_site_update.py: -------------------------------------------------------------------------------- 1 | from wo.utils import test 2 | from wo.cli.main import WOTestApp 3 | 4 | 5 | class CliTestCaseSiteUpdate(test.WOTestCase): 6 | 7 | def test_wo_cli_site_update_html(self): 8 | with WOTestApp(argv=['site', 'update', 'php.com', 9 | '--html']) as app: 10 | app.run() 11 | 12 | def test_wo_cli_site_update_php(self): 13 | with WOTestApp(argv=['site', 'update', 'html.com', 14 | '--php']) as app: 15 | app.run() 16 | 17 | def test_wo_cli_site_update_mysql(self): 18 | with WOTestApp(argv=['site', 'update', 'mysql.com', 19 | '--html']) as app: 20 | app.run() 21 | 22 | def test_wo_cli_site_update_wp(self): 23 | with WOTestApp(argv=['site', 'update', 'mysql.com', 24 | '--wp']) as app: 25 | app.run() 26 | 27 | def test_wo_cli_site_update_wpsubdir(self): 28 | with WOTestApp(argv=['site', 'update', 'wp.com', 29 | '--wpsubdir']) as app: 30 | app.run() 31 | 32 | def test_wo_cli_site_update_wpsubdomain(self): 33 | with WOTestApp(argv=['site', 'update', 'wpsubdir.com', 34 | '--wpsubdomain']) as app: 35 | app.run() 36 | 37 | def test_wo_cli_site_update_wpfc(self): 38 | with WOTestApp(argv=['site', 'update', 'wpsc.com', 39 | '--wpfc']) as app: 40 | app.run() 41 | 42 | def test_wo_cli_site_update_wpsc(self): 43 | with WOTestApp(argv=['site', 'update', 'wpfc.com', 44 | '--wpsc']) as app: 45 | app.run() 46 | -------------------------------------------------------------------------------- /wo/cli/templates/stub_status.mustache: -------------------------------------------------------------------------------- 1 | # Stub status NGINX CONFIGURATION 2 | # DO NOT MODIFY, ALL CHANGES WILL BE LOST AFTER AN WordOps (wo) UPDATE 3 | {{#phpconf}} 4 | upstream phpstatus { 5 | server unix:/run/php/php72-fpm.sock; 6 | } 7 | upstream php73opcache { 8 | server unix:/run/php/php73-fpm.sock; 9 | } 10 | upstream php74opcache { 11 | server unix:/run/php/php74-fpm.sock; 12 | } 13 | {{/phpconf}} 14 | server { 15 | listen 127.0.0.1:80; 16 | server_name 127.0.0.1 localhost; 17 | access_log off; 18 | log_not_found off; 19 | root /var/www/22222/htdocs; 20 | allow 127.0.0.1; 21 | deny all; 22 | location ~ /(stub_status|nginx_status) { 23 | stub_status on; 24 | allow 127.0.0.1; 25 | deny all; 26 | access_log off; 27 | log_not_found off; 28 | } 29 | {{#phpconf}} 30 | location ~ /(status|ping) { 31 | include fastcgi_params; 32 | fastcgi_pass phpstatus; 33 | access_log off; 34 | log_not_found off; 35 | } 36 | location / { 37 | try_files $uri $uri/ /index.php$is_args$args; 38 | } 39 | location /cache/opcache/php72.php { 40 | try_files $uri =404; 41 | include fastcgi_params; 42 | fastcgi_pass phpstatus; 43 | access_log off; 44 | log_not_found off; 45 | } 46 | location /cache/opcache/php73.php { 47 | try_files $uri =404; 48 | include fastcgi_params; 49 | fastcgi_pass php73opcache; 50 | access_log off; 51 | log_not_found off; 52 | } 53 | location /cache/opcache/php74.php { 54 | try_files $uri =404; 55 | include fastcgi_params; 56 | fastcgi_pass php74opcache; 57 | access_log off; 58 | log_not_found off; 59 | } 60 | {{/phpconf}} 61 | } 62 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: shell 2 | 3 | jobs: 4 | include: 5 | - if: branch = master 6 | os: linux 7 | dist: xenial 8 | - if: branch = master 9 | os: linux 10 | dist: bionic 11 | - if: branch = master 12 | os: linux 13 | dist: focal 14 | 15 | notifications: 16 | webhooks: 17 | secure: "JiGtzYplTyFg/L6Rsi7ptEQIV29O5qCWU2Zf5pLITsQrBrQO4cIXXp9G4Z+cenXjfIiqbqIgU0US3zXeIAl4g14xdfzmMYeMMwuKBpI8afMYv8MD6ldoP0MTFHQfROE6OXxKLVUvZn1R0oLLU1fzVSI0qGjNkt20cf/Lrt/reH/zS5hAI92kWI3u2zPu7Zn/g/a8MO/Y3Iv7v1PSQaVkVJVqtOK3U2GJqhIv2G1AVcaPb7Nh/V2zm2dDYBVT0UotBnlBUcUXbEMP77D9pjtWXd1/0rWuJIHixMjwUybpZqY75UMee5INynU6OZRsv029LRHAIMkWhfBkdVN/U5jhQJzui14+vRQrb5nfUMG8Cd8INojDlu6dk/ps2GzTCCXBITeMQKAouUoHD2LEbsNp17xi1K4ZlKb3+0lrOAiS4JYFE6wOo4yMlLTYoquYSqk7AuxuUS8A5OD5MYxhk9uafiTSxKFOo39KYWTSaACsPD8q1swaTSjoYm9skyZvIkIFq5bHBCYEGFe6X/NY9l5tz3hSe+TJOerCHsg+dXVuQl+pIp5nw2as9TH9ox5Vgqc9Zh4GbTDQVvdAmUpmlsZ/SKoOMCkmkB1aRNFq/7RnERIJyAEGJbauHWmjtOM4cCxesl0L0b2Eab89zQpSn7pzE8JTiJgpzCUc22p653PTaqM=" 18 | 19 | git: 20 | quiet: true 21 | 22 | before_install: 23 | - rm -rf ~/.gnupg 24 | 25 | before_script: 26 | - sudo rm -rf /etc/mysql 27 | - sudo bash -c 'echo example.com > /etc/hostname' 28 | - unset LANG 29 | - sudo apt-get update -qq 30 | - sudo apt-get -qq purge mysql* graphviz* redis* php* 31 | - sudo apt-get -qq autoremove --purge 32 | 33 | 34 | after_script: 35 | - sudo -E python3 setup.py sdist bdist_wheel 36 | - sudo -E bash install --purge 37 | 38 | 39 | script: 40 | - lsb_release -a 41 | - sudo bash -c 'echo -e "[user]\n\tname = abc\n\temail = root@localhost.com" > /home/travis/.gitconfig' 42 | - sudo echo "Travis Banch = $TRAVIS_BRANCH" 43 | - sudo -E time bash install --travis -b "$TRAVIS_BRANCH" 44 | - sudo -E time bash tests/travis.sh 45 | - sudo -E wo update --travis -------------------------------------------------------------------------------- /wo/cli/templates/info_php.mustache: -------------------------------------------------------------------------------- 1 | 2 | PHP ({{version}}): 3 | 4 | user {{user}} 5 | expose_php {{expose_php}} 6 | memory_limit {{memory_limit}} 7 | post_max_size {{post_max_size}} 8 | upload_max_filesize {{upload_max_filesize}} 9 | max_execution_time {{max_execution_time}} 10 | 11 | Information about www.conf 12 | ping.path {{www_ping_path}} 13 | pm.status_path {{www_pm_status_path}} 14 | process_manager {{www_pm}} 15 | pm.max_requests {{www_pm_max_requests}} 16 | pm.max_children {{www_pm_max_children}} 17 | pm.start_servers {{www_pm_start_servers}} 18 | pm.min_spare_servers {{www_pm_min_spare_servers}} 19 | pm.max_spare_servers {{www_pm_max_spare_servers}} 20 | request_terminate_timeout {{www_request_terminate_timeout}} 21 | xdebug.profiler_enable_trigger {{www_xdebug_profiler_enable_trigger}} 22 | listen {{www_listen}} 23 | 24 | Information about debug.conf 25 | ping.path {{debug_ping_path}} 26 | pm.status_path {{debug_pm_status_path}} 27 | process_manager {{debug_pm}} 28 | pm.max_requests {{debug_pm_max_requests}} 29 | pm.max_children {{debug_pm_max_children}} 30 | pm.start_servers {{debug_pm_start_servers}} 31 | pm.min_spare_servers {{debug_pm_min_spare_servers}} 32 | pm.max_spare_servers {{debug_pm_max_spare_servers}} 33 | request_terminate_timeout {{debug_request_terminate_timeout}} 34 | xdebug.profiler_enable_trigger {{debug_xdebug_profiler_enable_trigger}} 35 | listen {{debug_listen}} 36 | -------------------------------------------------------------------------------- /wo/core/template.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from wo.core.logging import Log 4 | 5 | 6 | """ 7 | Render Templates 8 | """ 9 | 10 | 11 | class WOTemplate: 12 | """WordOps template utilities""" 13 | 14 | def deploy(self, fileconf, template, data, overwrite=True): 15 | """Deploy template with render()""" 16 | data = dict(data) 17 | if (not os.path.isfile('{0}.custom' 18 | .format(fileconf))): 19 | if (not overwrite): 20 | if not os.path.isfile('{0}'.format(fileconf)): 21 | Log.debug(self, 'Writting the configuration to ' 22 | 'file {0}'.format(fileconf)) 23 | wo_template = open('{0}'.format(fileconf), 24 | encoding='utf-8', mode='w') 25 | self.app.render((data), '{0}'.format(template), 26 | out=wo_template) 27 | wo_template.close() 28 | else: 29 | Log.debug(self, 'Writting the configuration to ' 30 | 'file {0}'.format(fileconf)) 31 | wo_template = open('{0}'.format(fileconf), 32 | encoding='utf-8', mode='w') 33 | self.app.render((data), '{0}'.format(template), 34 | out=wo_template) 35 | wo_template.close() 36 | else: 37 | Log.debug(self, 'Writting the configuration to ' 38 | 'file {0}.orig'.format(fileconf)) 39 | wo_template = open('{0}.orig'.format(fileconf), 40 | encoding='utf-8', mode='w') 41 | self.app.render((data), '{0}'.format(template), 42 | out=wo_template) 43 | wo_template.close() 44 | -------------------------------------------------------------------------------- /tests/cli/31_test_stack_purge.py: -------------------------------------------------------------------------------- 1 | from wo.utils import test 2 | from wo.cli.main import WOTestApp 3 | 4 | 5 | class CliTestCaseStackPurge(test.WOTestCase): 6 | 7 | def test_wo_cli_stack_purge_web(self): 8 | with WOTestApp( 9 | argv=['stack', 'purge', '--web', '--force']) as app: 10 | app.run() 11 | 12 | def test_wo_cli_stack_purge_admin(self): 13 | with WOTestApp( 14 | argv=['stack', 'purge', '--admin', '--force']) as app: 15 | app.run() 16 | 17 | def test_wo_cli_stack_purge_nginx(self): 18 | with WOTestApp( 19 | argv=['stack', 'purge', '--nginx', '--force']) as app: 20 | app.run() 21 | 22 | def test_wo_cli_stack_purge_php(self): 23 | with WOTestApp(argv=['stack', 'purge', 24 | '--php', '--force']) as app: 25 | app.run() 26 | 27 | def test_wo_cli_stack_purge_mysql(self): 28 | with WOTestApp(argv=['stack', 'purge', 29 | '--mysql', '--force']) as app: 30 | app.run() 31 | 32 | def test_wo_cli_stack_purge_wpcli(self): 33 | with WOTestApp(argv=['stack', 'purge', 34 | '--wpcli', '--force']) as app: 35 | app.run() 36 | 37 | def test_wo_cli_stack_purge_phpmyadmin(self): 38 | with WOTestApp( 39 | argv=['stack', 'purge', '--phpmyadmin', '--force']) as app: 40 | app.run() 41 | 42 | def test_wo_cli_stack_purge_adminer(self): 43 | with WOTestApp( 44 | argv=['stack', 'purge', '--adminer', '--force']) as app: 45 | app.run() 46 | 47 | def test_wo_cli_stack_purge_utils(self): 48 | with WOTestApp(argv=['stack', 'purge', 49 | '--utils', '--force']) as app: 50 | app.run() 51 | -------------------------------------------------------------------------------- /wo/core/stackconf.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from wo.core.logging import Log 4 | from wo.core.template import WOTemplate 5 | from wo.core.variables import WOVar 6 | 7 | 8 | class WOConf(): 9 | """wo stack configuration utilities""" 10 | def __init__(): 11 | pass 12 | 13 | def nginxcommon(self): 14 | """nginx common configuration deployment""" 15 | wo_php_version = list(WOVar.wo_php_versions.keys()) 16 | ngxcom = '/etc/nginx/common' 17 | if not os.path.exists(ngxcom): 18 | os.mkdir(ngxcom) 19 | for wo_php in wo_php_version: 20 | Log.debug(self, 'deploying templates for {0}'.format(wo_php)) 21 | data = dict(upstream="{0}".format(wo_php), 22 | release=WOVar.wo_version) 23 | WOTemplate.deploy(self, 24 | '{0}/{1}.conf' 25 | .format(ngxcom, wo_php), 26 | 'php.mustache', data) 27 | 28 | WOTemplate.deploy( 29 | self, '{0}/redis-{1}.conf'.format(ngxcom, wo_php), 30 | 'redis.mustache', data) 31 | 32 | WOTemplate.deploy( 33 | self, '{0}/wpcommon-{1}.conf'.format(ngxcom, wo_php), 34 | 'wpcommon.mustache', data) 35 | 36 | WOTemplate.deploy( 37 | self, '{0}/wpfc-{1}.conf'.format(ngxcom, wo_php), 38 | 'wpfc.mustache', data) 39 | 40 | WOTemplate.deploy( 41 | self, '{0}/wpsc-{1}.conf'.format(ngxcom, wo_php), 42 | 'wpsc.mustache', data) 43 | 44 | WOTemplate.deploy( 45 | self, '{0}/wprocket-{1}.conf'.format(ngxcom, wo_php), 46 | 'wprocket.mustache', data) 47 | 48 | WOTemplate.deploy( 49 | self, '{0}/wpce-{1}.conf'.format(ngxcom, wo_php), 50 | 'wpce.mustache', data) 51 | -------------------------------------------------------------------------------- /wo/cli/templates/22222.mustache: -------------------------------------------------------------------------------- 1 | # WordOps admin NGINX CONFIGURATION - WordOps {{release}} 2 | 3 | server { 4 | 5 | listen {{port}} default_server ssl http2; 6 | 7 | access_log /var/log/nginx/22222.access.log rt_cache; 8 | error_log /var/log/nginx/22222.error.log; 9 | 10 | # Force HTTP to HTTPS 11 | error_page 497 =200 https://$host:{{port}}$request_uri; 12 | 13 | root {{webroot}}22222/htdocs; 14 | index index.php index.htm index.html; 15 | 16 | # Turn on directory listing 17 | autoindex on; 18 | 19 | # HTTP Authentication on port 22222 20 | include common/acl.conf; 21 | 22 | # nginx-vts-status 23 | location /vts_status { 24 | vhost_traffic_status_bypass_limit on; 25 | vhost_traffic_status_bypass_stats on; 26 | vhost_traffic_status_display; 27 | vhost_traffic_status_display_format html; 28 | } 29 | 30 | location / { 31 | try_files $uri $uri/ /index.php$is_args$args; 32 | } 33 | 34 | # Display menu at location /fpm/status/ 35 | location = /fpm/status/ {} 36 | 37 | location ~ /fpm/status/(.*) { 38 | try_files $uri =404; 39 | include fastcgi_params; 40 | fastcgi_param SCRIPT_NAME /status; 41 | fastcgi_pass $1; 42 | } 43 | 44 | location ~ \.php$ { 45 | try_files $uri =404; 46 | include fastcgi_params; 47 | fastcgi_pass multiphp; 48 | } 49 | 50 | location /netdata { 51 | return 301 /netdata/; 52 | } 53 | 54 | location ~ /netdata/(?.*) { 55 | proxy_redirect off; 56 | proxy_set_header Host $host; 57 | 58 | proxy_set_header X-Forwarded-Host $host; 59 | proxy_set_header X-Forwarded-Server $host; 60 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 61 | proxy_http_version 1.1; 62 | proxy_pass_request_headers on; 63 | proxy_set_header Connection "keep-alive"; 64 | proxy_store off; 65 | proxy_pass http://netdata/$ndpath$is_args$args; 66 | 67 | } 68 | 69 | include {{webroot}}22222/conf/nginx/*.conf; 70 | 71 | } -------------------------------------------------------------------------------- /wo/cli/templates/ufw.mustache: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | wo_ufw_setup() { 4 | # get custom ssh port 5 | if [ -f /etc/ssh/sshd_config ]; then 6 | CURRENT_SSH_PORT=$(grep "Port" /etc/ssh/sshd_config | awk -F " " '{print $2}') 7 | fi 8 | # define firewall rules 9 | if ! grep -q "LOGLEVEL=low" /etc/ufw/ufw.conf; then 10 | ufw logging low 11 | fi 12 | if ! grep -q 'DEFAULT_OUTPUT_POLICY="ACCEPT"' /etc/default/ufw; then 13 | ufw default allow outgoing 14 | fi 15 | if ! grep -q 'DEFAULT_INPUT_POLICY="DROP"' /etc/default/ufw; then 16 | ufw default deny incoming 17 | fi 18 | if ! grep -q "\-\-dport 22 -j" /etc/ufw/user.rules; then 19 | # default ssh port 20 | ufw limit 22 21 | fi 22 | 23 | # custom ssh port 24 | if [ "$CURRENT_SSH_PORT" != "22" ]; then 25 | if ! grep -q "\-\-dport $CURRENT_SSH_PORT -j" /etc/ufw/user.rules; then 26 | ufw limit "$CURRENT_SSH_PORT" 27 | fi 28 | fi 29 | 30 | # nginx 31 | if ! grep -q "\-\-dport 80 -j" /etc/ufw/user.rules; then 32 | # http 33 | ufw allow http 34 | fi 35 | if ! grep -q "\-\-dport 443 -j" /etc/ufw/user.rules; then 36 | # https 37 | ufw allow https 38 | fi 39 | 40 | # ntp 41 | if ! grep -q "\-\-dport 123 -j" /etc/ufw/user.rules; then 42 | ufw allow 123 43 | fi 44 | 45 | if ! grep -q "\-\-dport 22222 -j" /etc/ufw/user.rules; then 46 | # wordops backend 47 | ufw limit 22222 48 | fi 49 | 50 | # allow proftpd port if installed 51 | if [ -f /etc/proftpd/proftpd.conf ]; then 52 | ufw limit 21 53 | ufw allow 49000:50000/tcp 54 | fi 55 | 56 | # enable ufw 57 | if [ -n "$CURRENT_SSH_PORT" ]; then 58 | ufw --force enable 59 | fi 60 | 61 | # remove ufw from syslog 62 | if [ -f /etc/rsyslog.d/20-ufw.conf ]; then 63 | sed -i 's/\#\& stop/\& stop/' /etc/rsyslog.d/20-ufw.conf 64 | service rsyslog restart 65 | fi 66 | } 67 | 68 | wo_ufw_setup -------------------------------------------------------------------------------- /wo/core/nginxhashbucket.py: -------------------------------------------------------------------------------- 1 | """WordOps Hash Bucket Calculator""" 2 | import fileinput 3 | import math 4 | import os 5 | import subprocess 6 | 7 | from wo.core.fileutils import WOFileUtils 8 | 9 | 10 | def hashbucket(self): 11 | # Check Nginx Hashbucket error 12 | sub = subprocess.Popen('nginx -t', stdout=subprocess.PIPE, 13 | stderr=subprocess.PIPE, shell=True) 14 | output, error_output = sub.communicate() 15 | if 'server_names_hash_bucket_size' not in str(error_output): 16 | return True 17 | 18 | count = 0 19 | # Get the list of sites-availble 20 | sites_list = os.listdir("/etc/nginx/sites-enabled/") 21 | 22 | # Count the number of characters in site names 23 | for site in sites_list: 24 | count = sum([count, len(site)]) 25 | 26 | # Calculate Nginx hash bucket size 27 | ngx_calc = math.trunc(sum([math.log(count, 2), 2])) 28 | ngx_hash = math.trunc(math.pow(2, ngx_calc)) 29 | 30 | # Replace hashbucket in Nginx.conf file 31 | if WOFileUtils.grepcheck(self, "/etc/nginx/nginx.conf", 32 | "# server_names_hash_bucket_size 64;"): 33 | ngxconf = open("/etc/nginx/conf.d/hashbucket.conf", 34 | encoding='utf-8', mode='w') 35 | ngxconf.write("\tserver_names_hash_bucket_size {0};".format(ngx_hash)) 36 | ngxconf.close() 37 | elif WOFileUtils.grepcheck(self, "/etc/nginx/nginx/conf", 38 | "server_names_hash_bucket_size"): 39 | for line in fileinput.FileInput("/etc/nginx/nginx.conf", inplace=1): 40 | if "server_names_hash_bucket_size" in line: 41 | print("\tserver_names_hash_bucket_size {0};".format(ngx_hash)) 42 | else: 43 | print(line, end='') 44 | 45 | else: 46 | ngxconf = open("/etc/nginx/conf.d/hashbucket.conf", 47 | encoding='utf-8', mode='w') 48 | ngxconf.write("\tserver_names_hash_bucket_size {0};".format(ngx_hash)) 49 | ngxconf.close() 50 | -------------------------------------------------------------------------------- /wo/cli/templates/wo-plus.mustache: -------------------------------------------------------------------------------- 1 | ## 2 | # WordOps Settings 3 | ## 4 | 5 | 6 | tcp_nopush on; 7 | tcp_nodelay on; 8 | types_hash_max_size 2048; 9 | 10 | server_tokens off; 11 | reset_timedout_connection on; 12 | add_header X-Powered-By "WordOps {{ version }}"; 13 | 14 | # Limit Request 15 | limit_req_status 403; 16 | limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s; 17 | 18 | # Proxy Settings 19 | # set_real_ip_from proxy-server-ip; 20 | # real_ip_header X-Forwarded-For; 21 | 22 | fastcgi_read_timeout 300; 23 | client_max_body_size 100m; 24 | 25 | ## 26 | # SSL Settings 27 | ## 28 | 29 | ssl_session_cache shared:SSL:5m; 30 | ssl_session_timeout 10m; 31 | ssl_prefer_server_ciphers on; 32 | ssl_ciphers "ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-CHACHA20-POLY1305:EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH"; 33 | ssl_protocols TLSv1.1 TLSv1.2; 34 | 35 | ## 36 | # Basic Settings 37 | ## 38 | server_names_hash_bucket_size 16384; 39 | # server_name_in_redirect off; 40 | 41 | 42 | ## 43 | # Logging Settings 44 | ## 45 | 46 | access_log /var/log/nginx/access.log; 47 | error_log /var/log/nginx/error.log; 48 | 49 | # Log format Settings 50 | log_format rt_cache '$remote_addr $upstream_response_time $upstream_cache_status [$time_local] ' 51 | '$http_host "$request" $status $body_bytes_sent ' 52 | '"$http_referer" "$http_user_agent" "$request_body"'; 53 | 54 | ## 55 | # Gzip Settings 56 | ## 57 | 58 | gzip on; 59 | gzip_disable "msie6"; 60 | 61 | gzip_vary on; 62 | gzip_proxied any; 63 | gzip_comp_level 6; 64 | gzip_buffers 16 8k; 65 | gzip_http_version 1.1; 66 | gzip_types 67 | application/atom+xml 68 | application/javascript 69 | application/json 70 | application/rss+xml 71 | application/vnd.ms-fontobject 72 | application/x-font-ttf 73 | application/x-web-app-manifest+json 74 | application/xhtml+xml 75 | application/xml 76 | font/opentype 77 | image/svg+xml 78 | image/x-icon 79 | text/css 80 | text/plain 81 | text/x-component 82 | text/xml 83 | text/javascript; 84 | -------------------------------------------------------------------------------- /wo/core/domainvalidate.py: -------------------------------------------------------------------------------- 1 | """WordOps domain validation module.""" 2 | import os 3 | 4 | 5 | class WODomain: 6 | """WordOps domain validation utilities""" 7 | 8 | def validate(self, url): 9 | """ 10 | This function returns domain name removing http:// and https:// 11 | returns domain name only with or without www as user provided. 12 | """ 13 | 14 | # Check if http:// or https:// present remove it if present 15 | domain_name = url.split('/') 16 | if 'http:' in domain_name or 'https:' in domain_name: 17 | domain_name = domain_name[2] 18 | else: 19 | domain_name = domain_name[0] 20 | www_domain_name = domain_name.split('.') 21 | final_domain = '' 22 | if www_domain_name[0] == 'www': 23 | final_domain = '.'.join(www_domain_name[1:]) 24 | return final_domain 25 | return domain_name 26 | 27 | def getlevel(self, domain): 28 | """ 29 | Returns the domain type : domain, subdomain and the root domain 30 | """ 31 | domain_name = domain.lower().strip().split('.') 32 | if domain_name[0] == 'www': 33 | domain_name = domain_name[1:] 34 | domain_type = '' 35 | if os.path.isfile("/var/lib/wo/public_suffix_list.dat"): 36 | # Read mode opens a file for reading only. 37 | suffix_file = open( 38 | "/var/lib/wo/public_suffix_list.dat", encoding='utf-8') 39 | # Read all the lines into a list. 40 | for domain_suffix in suffix_file: 41 | if (str(domain_suffix).strip()) == ('.'.join(domain_name[1:])): 42 | domain_type = 'domain' 43 | break 44 | else: 45 | domain_type = 'subdomain' 46 | suffix_file.close() 47 | if domain_type == 'domain': 48 | root_domain = ('.'.join(domain_name[0:])) 49 | else: 50 | root_domain = ('.'.join(domain_name[1:])) 51 | return (domain_type, root_domain) 52 | -------------------------------------------------------------------------------- /tests/cli/18_test_site_create.py: -------------------------------------------------------------------------------- 1 | from wo.utils import test 2 | from wo.cli.main import WOTestApp 3 | 4 | 5 | class CliTestCaseSiteCreate(test.WOTestCase): 6 | 7 | def test_wo_cli_site_create_html(self): 8 | with WOTestApp(argv=['site', 'create', 'html.com', 9 | '--html']) as app: 10 | app.config.set('wo', '', True) 11 | app.run() 12 | 13 | def test_wo_cli_site_create_php(self): 14 | with WOTestApp(argv=['site', 'create', 'php.com', 15 | '--php']) as app: 16 | app.run() 17 | 18 | def test_wo_cli_site_create_mysql(self): 19 | with WOTestApp(argv=['site', 'create', 'mysql.com', 20 | '--mysql']) as app: 21 | app.run() 22 | 23 | def test_wo_cli_site_create_wp(self): 24 | with WOTestApp(argv=['site', 'create', 'wp.com', 25 | '--wp']) as app: 26 | app.run() 27 | 28 | def test_wo_cli_site_create_wpsubdir(self): 29 | with WOTestApp(argv=['site', 'create', 'wpsubdir.com', 30 | '--wpsubdir']) as app: 31 | app.run() 32 | 33 | def test_wo_cli_site_create_wpsubdomain(self): 34 | with WOTestApp(argv=['site', 'create', 'wpsubdomain.com', 35 | '--wpsubdomain']) as app: 36 | app.run() 37 | 38 | def test_wo_cli_site_create_wpfc(self): 39 | with WOTestApp(argv=['site', 'create', 'wpfc.com', 40 | '--wpfc']) as app: 41 | app.run() 42 | 43 | def test_wo_cli_site_create_wpsc(self): 44 | with WOTestApp(argv=['site', 'create', 'wpsc.com', 45 | '--wpsc']) as app: 46 | app.run() 47 | 48 | def test_wo_cli_site_create_wpce(self): 49 | with WOTestApp(argv=['site', 'create', 'wpce.com', 50 | '--wpce']) as app: 51 | app.run() 52 | 53 | def test_wo_cli_site_create_wprocket(self): 54 | with WOTestApp(argv=['site', 'create', 'wprocket.com', 55 | '--wprocket']) as app: 56 | app.run() 57 | -------------------------------------------------------------------------------- /wo/cli/plugins/models.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Boolean, Column, DateTime, Integer, String, func 2 | 3 | from wo.core.database import Base 4 | 5 | 6 | class SiteDB(Base): 7 | """ 8 | Database model for site table 9 | """ 10 | __tablename__ = 'sites' 11 | __table_args__ = {'extend_existing': True} 12 | id = Column(Integer, primary_key=True) 13 | sitename = Column(String, unique=True) 14 | 15 | site_type = Column(String) 16 | cache_type = Column(String) 17 | site_path = Column(String) 18 | 19 | # Use default=func.now() to set the default created time 20 | # of a site to be the current time when a 21 | # Site record was created 22 | 23 | created_on = Column(DateTime, default=func.now()) 24 | is_enabled = Column(Boolean, unique=False, default=True, nullable=False) 25 | is_ssl = Column(Boolean, unique=False, default=False) 26 | storage_fs = Column(String) 27 | storage_db = Column(String) 28 | db_name = Column(String) 29 | db_user = Column(String) 30 | db_password = Column(String) 31 | db_host = Column(String) 32 | is_hhvm = Column(Boolean, unique=False, default=False) 33 | php_version = Column(String) 34 | 35 | def __init__(self, sitename=None, site_type=None, cache_type=None, 36 | site_path=None, site_enabled=None, 37 | is_ssl=None, storage_fs=None, storage_db=None, db_name=None, 38 | db_user=None, db_password=None, db_host='localhost', 39 | hhvm=None, php_version=None): 40 | self.sitename = sitename 41 | self.site_type = site_type 42 | self.cache_type = cache_type 43 | self.site_path = site_path 44 | self.is_enabled = site_enabled 45 | self.is_ssl = is_ssl 46 | self.storage_fs = storage_fs 47 | self.storage_db = storage_db 48 | self.db_name = db_name 49 | self.db_user = db_user 50 | self.db_password = db_password 51 | self.db_host = db_host 52 | self.is_hhvm = hhvm 53 | self.php_version = php_version 54 | # def __repr__(self): 55 | # return '' % (self.site_type) 56 | # 57 | # def getType(self): 58 | # return '%r>' % (self.site_type) 59 | -------------------------------------------------------------------------------- /wo/core/cron.py: -------------------------------------------------------------------------------- 1 | from wo.core.logging import Log 2 | from wo.core.shellexec import WOShellExec 3 | 4 | 5 | """ 6 | Set CRON on LINUX system. 7 | """ 8 | 9 | 10 | class WOCron(): 11 | def setcron_weekly(self, cmd, comment='Cron set by WordOps', user='root', 12 | min=0, hour=12): 13 | if not WOShellExec.cmd_exec(self, "crontab -l " 14 | "| grep -q \'{0}\'".format(cmd)): 15 | 16 | WOShellExec.cmd_exec(self, "/bin/bash -c \"crontab -l " 17 | "2> /dev/null | {{ cat; echo -e" 18 | " \\\"" 19 | "\\n0 0 * * 0 " 20 | "{0}".format(cmd) + 21 | " # {0}".format(comment) + 22 | "\\\"; } | crontab -\"") 23 | Log.debug(self, "Cron set") 24 | 25 | def setcron_daily(self, cmd, comment='Cron set by WordOps', user='root', 26 | min=0, hour=12): 27 | if not WOShellExec.cmd_exec(self, "crontab -l " 28 | "| grep -q \'{0}\'".format(cmd)): 29 | 30 | WOShellExec.cmd_exec(self, "/bin/bash -c \"crontab -l " 31 | "2> /dev/null | {{ cat; echo -e" 32 | " \\\"" 33 | "\\n@daily" 34 | "{0}".format(cmd) + 35 | " # {0}".format(comment) + 36 | "\\\"; } | crontab -\"") 37 | Log.debug(self, "Cron set") 38 | 39 | def remove_cron(self, cmd): 40 | if WOShellExec.cmd_exec(self, "crontab -l " 41 | "| grep -q \'{0}\'".format(cmd)): 42 | if not WOShellExec.cmd_exec(self, "/bin/bash -c " 43 | "\"crontab " 44 | "-l | sed '/{0}/d'" 45 | "| crontab -\"" 46 | .format(cmd)): 47 | Log.error(self, "Failed to remove crontab entry", False) 48 | else: 49 | Log.debug(self, "Cron not found") 50 | -------------------------------------------------------------------------------- /wo/cli/templates/sshd.mustache: -------------------------------------------------------------------------------- 1 | # Use a custom port in the following range : 1024-65536 2 | Port {{sshport}} 3 | 4 | #Prefer ed25519 & ECDSA keys rather than 2048 bit RSA 5 | HostKey /etc/ssh/ssh_host_rsa_key 6 | HostKey /etc/ssh/ssh_host_ecdsa_key 7 | HostKey /etc/ssh/ssh_host_ed25519_key 8 | 9 | # Allow root access with ssh keys 10 | PermitRootLogin without-password 11 | 12 | # Allow ssh access to some users only 13 | AllowUsers root ubuntu debian {{user}} 14 | 15 | # allow ssh key Authentication 16 | PubkeyAuthentication yes 17 | 18 | # ssh keys path in ~/.ssh/authorized_keys 19 | AuthorizedKeysFile %h/.ssh/authorized_keys 20 | 21 | # No password or empty passwords Authentication 22 | PasswordAuthentication {{allowpass}} 23 | PermitEmptyPasswords no 24 | 25 | # No challenge response Authentication 26 | ChallengeResponseAuthentication no 27 | 28 | UsePAM yes 29 | X11Forwarding yes 30 | 31 | PrintMotd yes 32 | 33 | # Allow client to pass locale environment variables 34 | AcceptEnv LANG LC_* 35 | 36 | # LogLevel VERBOSE logs user's key fingerprint on login. Needed to have a clear audit track of which key was using to log in. 37 | LogLevel VERBOSE 38 | 39 | # Log sftp level file access (read/write/etc.) that would not be easily logged otherwise. 40 | Subsystem sftp /usr/lib/openssh/sftp-server -f AUTHPRIV -l INFO 41 | 42 | # Host keys the client accepts - order here is honored by OpenSSH 43 | HostKeyAlgorithms ssh-ed25519-cert-v01@openssh.com,ssh-rsa-cert-v01@openssh.com,ssh-ed25519,ssh-rsa,ecdsa-sha2-nistp521-cert-v01@openssh.com,ecdsa-sha2-nistp384-cert-v01@openssh.com,ecdsa-sha2-nistp256-cert-v01@openssh.com,ecdsa-sha2-nistp521,ecdsa-sha2-nistp384,ecdsa-sha2-nistp256 44 | 45 | # use strong ciphers 46 | KexAlgorithms curve25519-sha256@libssh.org,ecdh-sha2-nistp521,ecdh-sha2-nistp384,ecdh-sha2-nistp256,diffie-hellman-group-exchange-sha256 47 | Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr 48 | MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-512,hmac-sha2-256,umac-128@openssh.com 49 | 50 | # Use kernel sandbox mechanisms where possible in unprivileged processes 51 | # Systrace on OpenBSD, Seccomp on Linux, seatbelt on MacOSX/Darwin, rlimit elsewhere. 52 | UsePrivilegeSeparation sandbox -------------------------------------------------------------------------------- /wo/core/addswap.py: -------------------------------------------------------------------------------- 1 | """WordOps Swap Creation""" 2 | import os 3 | 4 | import psutil 5 | 6 | from wo.core.aptget import WOAptGet 7 | from wo.core.fileutils import WOFileUtils 8 | from wo.core.logging import Log 9 | from wo.core.shellexec import WOShellExec 10 | 11 | 12 | class WOSwap(): 13 | """Manage Swap""" 14 | 15 | def __init__(): 16 | """Initialize """ 17 | pass 18 | 19 | def add(self): 20 | """Swap addition with WordOps""" 21 | # Get System RAM and SWAP details 22 | wo_ram = psutil.virtual_memory().total / (1024 * 1024) 23 | wo_swap = psutil.swap_memory().total / (1024 * 1024) 24 | if wo_ram < 512: 25 | if wo_swap < 1000: 26 | Log.info(self, "Adding SWAP file, please wait...") 27 | 28 | # Install dphys-swapfile 29 | WOAptGet.update(self) 30 | WOAptGet.install(self, ["dphys-swapfile"]) 31 | # Stop service 32 | WOShellExec.cmd_exec(self, "service dphys-swapfile stop") 33 | # Remove Default swap created 34 | WOShellExec.cmd_exec(self, "/sbin/dphys-swapfile uninstall") 35 | 36 | # Modify Swap configuration 37 | if os.path.isfile("/etc/dphys-swapfile"): 38 | WOFileUtils.searchreplace(self, "/etc/dphys-swapfile", 39 | "#CONF_SWAPFILE=/var/swap", 40 | "CONF_SWAPFILE=/wo-swapfile") 41 | WOFileUtils.searchreplace(self, "/etc/dphys-swapfile", 42 | "#CONF_MAXSWAP=2048", 43 | "CONF_MAXSWAP=1024") 44 | WOFileUtils.searchreplace(self, "/etc/dphys-swapfile", 45 | "#CONF_SWAPSIZE=", 46 | "CONF_SWAPSIZE=1024") 47 | else: 48 | with open("/etc/dphys-swapfile", 'w') as conffile: 49 | conffile.write("CONF_SWAPFILE=/wo-swapfile\n" 50 | "CONF_SWAPSIZE=1024\n" 51 | "CONF_MAXSWAP=1024\n") 52 | # Create swap file 53 | WOShellExec.cmd_exec(self, "service dphys-swapfile start") 54 | -------------------------------------------------------------------------------- /wo/cli/templates/virtualconf.mustache: -------------------------------------------------------------------------------- 1 | 2 | server { 3 | 4 | {{#multisite}} 5 | # Uncomment the following line for domain mapping 6 | # listen 80 default_server; 7 | {{/multisite}} 8 | 9 | server_name {{site_name}} {{#multisite}}*{{/multisite}}{{^multisite}}www{{/multisite}}.{{site_name}}; 10 | 11 | {{#multisite}} 12 | # Uncomment the following line for domain mapping 13 | #server_name_in_redirect off; 14 | {{/multisite}} 15 | 16 | access_log /var/log/nginx/{{site_name}}.access.log {{^wpredis}}{{^static}}rt_cache{{/static}}{{/wpredis}}{{#wpredis}}rt_cache_redis{{/wpredis}}; 17 | error_log /var/log/nginx/{{site_name}}.error.log; 18 | 19 | {{#alias}} 20 | location / { 21 | return 301 https://{{alias_name}}$request_uri; 22 | } 23 | {{/alias}} 24 | {{#proxy}} 25 | add_header X-Proxy-Cache $upstream_cache_status; 26 | location / { 27 | proxy_pass http://{{host}}:{{port}}; 28 | proxy_redirect off; 29 | include proxy_params; 30 | } 31 | {{#alias}} 32 | # Security settings for better privacy 33 | # Deny hidden files 34 | location ~ /\.(?!well-known\/) { 35 | deny all; 36 | } 37 | # letsencrypt validation 38 | location /.well-known/acme-challenge/ { 39 | alias /var/www/html/.well-known/acme-challenge/; 40 | allow all; 41 | auth_basic off; 42 | } 43 | {{/alias}} 44 | {{/proxy}} 45 | 46 | {{^proxy}} 47 | {{^alias}} 48 | 49 | {{^subsite}} 50 | root {{webroot}}/htdocs; 51 | {{/subsite}} 52 | 53 | {{#subsite}} 54 | root {{subsiteof_webroot}}/htdocs; 55 | {{/subsite}} 56 | 57 | index {{^static}}index.php{{/static}} index.html index.htm; 58 | 59 | {{#static}} 60 | location / { 61 | try_files $uri $uri/ =404; 62 | } 63 | {{/static}} 64 | 65 | {{^static}}include {{#basic}}common/{{wo_php}}.conf;{{/basic}}{{#wpfc}}common/wpfc-{{wo_php}}.conf;{{/wpfc}}{{#wpsc}}common/wpsc-{{wo_php}}.conf;{{/wpsc}}{{#wpredis}}common/redis-{{wo_php}}.conf;{{/wpredis}}{{#wprocket}}common/wprocket-{{wo_php}}.conf;{{/wprocket}}{{#wpce}}common/wpce-{{wo_php}}.conf;{{/wpce}} 66 | {{#wpsubdir}}include common/wpsubdir.conf;{{/wpsubdir}}{{/static}} 67 | {{#wp}}include common/wpcommon-{{wo_php}}.conf;{{/wp}} 68 | include common/locations-wo.conf;{{/alias}}{{/proxy}} 69 | include {{webroot}}/conf/nginx/*.conf; 70 | 71 | } 72 | -------------------------------------------------------------------------------- /config/wo.conf: -------------------------------------------------------------------------------- 1 | # WordOps Configuration 2 | # 3 | # All commented values are the application default 4 | # 5 | 6 | [wo] 7 | 8 | ### Toggle application level debug (does not toggle framework debugging) 9 | # debug = false 10 | 11 | ### Where external (third-party) plugins are loaded from 12 | # plugin_dir = /var/lib/wo/plugins/ 13 | 14 | ### Where all plugin configurations are loaded from 15 | # plugin_config_dir = /etc/wo/plugins.d/ 16 | 17 | ### Where external templates are loaded from 18 | # template_dir = /var/lib/wo/templates/ 19 | 20 | 21 | [log.colorlog] 22 | 23 | ### Where the log file lives (no log file by default) 24 | file = /var/log/wo/wordops.log 25 | 26 | ### The level for which to log. One of: info, warn, error, fatal, debug 27 | level = debug 28 | 29 | ### Whether or not to log to console 30 | to_console = false 31 | 32 | ### Whether or not to rotate the log file when it reaches `max_bytes` 33 | rotate = true 34 | 35 | ### Max size in bytes that a log file can grow until it is rotated. 36 | max_bytes = 1000000 37 | 38 | ### The maximun number of log files to maintain when rotating 39 | max_files = 7 40 | 41 | colorize_file_log = true 42 | 43 | colorize_console_log = true 44 | 45 | [stack] 46 | 47 | ### IP address that will be used in Nginx configurations while installing 48 | ip-address = 127.0.0.1 49 | 50 | [mysql] 51 | 52 | ### MySQL database grant host name 53 | grant-host = localhost 54 | 55 | ### Ask for MySQL db name while site creation 56 | db-name = False 57 | 58 | ### Ask for MySQL user name while site creation 59 | db-user = False 60 | 61 | [wordpress] 62 | 63 | ### Ask for WordPress prefix while site creation 64 | prefix = False 65 | 66 | ### User name for WordPress sites 67 | user = 68 | 69 | ### Password for WordPress sites 70 | password = 71 | 72 | ### EMail for WordPress sites 73 | email = 74 | 75 | [letsencrypt] 76 | 77 | keylength = "ec-384" 78 | 79 | [php] 80 | 81 | ### Default PHP version 82 | version = 8.2 83 | 84 | [mariadb] 85 | 86 | ### Default MariaDB release 87 | release = 10.11 88 | 89 | [update] 90 | 91 | ### If enabled, load a plugin named `update` either from the Python module 92 | ### `wo.cli.plugins.example` or from the file path 93 | ### `/var/lib/wo/plugins/example.py` 94 | enable_plugin = true 95 | 96 | [sync] 97 | ### If enabled, load a plugin named `update` either from the Python module 98 | ### `wo.cli.plugins.example` or from the file path 99 | ### `/var/lib/wo/plugins/example.py` 100 | enable_plugin = true 101 | -------------------------------------------------------------------------------- /wo/core/download.py: -------------------------------------------------------------------------------- 1 | """WordOps download core classes.""" 2 | import os 3 | import requests 4 | 5 | from wo.core.logging import Log 6 | 7 | 8 | class WODownload(): 9 | """Method to download using urllib""" 10 | def __init__(): 11 | pass 12 | 13 | def download(self, packages): 14 | """Download packages, packages must be list in format of 15 | [url, path, package name]""" 16 | for package in packages: 17 | url = package[0] 18 | filename = package[1] 19 | pkg_name = package[2] 20 | try: 21 | directory = os.path.dirname(filename) 22 | if not os.path.exists(directory): 23 | os.makedirs(directory) 24 | Log.info(self, "Downloading {0:20}".format(pkg_name), end=' ') 25 | with open(filename, "wb") as out_file: 26 | req = requests.get(url, timeout=(5, 30)) 27 | if req.encoding is None: 28 | req.encoding = 'utf-8' 29 | out_file.write(req.content) 30 | Log.info(self, "{0}".format("[" + Log.ENDC + "Done" + 31 | Log.OKBLUE + "]")) 32 | except requests.RequestException as e: 33 | Log.debug(self, "[{err}]".format(err=str(e.reason))) 34 | Log.error(self, "Unable to download file, {0}" 35 | .format(filename)) 36 | return False 37 | return 0 38 | 39 | def latest_release(self, repository, name=False): 40 | """Get the latest release number of a GitHub repository.\n 41 | repository format should be: \"user/repo\"""" 42 | try: 43 | req = requests.get( 44 | 'https://api.github.com/repos/{0}/releases/latest' 45 | .format(repository), 46 | timeout=(5, 30)) 47 | github_json = req.json() 48 | except requests.RequestException as e: 49 | Log.debug(self, str(e)) 50 | Log.error(self, "Unable to query GitHub API") 51 | if name: 52 | return github_json["name"] 53 | else: 54 | return github_json["tag_name"] 55 | 56 | def pma_release(self): 57 | """Get the latest phpmyadmin release number from a json file""" 58 | try: 59 | req = requests.get( 60 | 'https://www.phpmyadmin.net/home_page/version.json', 61 | timeout=(5, 30)) 62 | pma_json = req.json() 63 | except requests.RequestException as e: 64 | Log.debug(self, str(e)) 65 | Log.error(self, "Unable to query phpmyadmin API") 66 | return pma_json["version"] 67 | -------------------------------------------------------------------------------- /wo/cli/templates/cf-update.mustache: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # WordOps bash script to update cloudflare IP 3 | 4 | string::isIPv4Block(){ 5 | local -r ip="${1}" 6 | local stat=1 7 | 8 | if [[ "${ip}" =~ ^(([1-9]?[0-9]|1[0-9][0-9]|2([0-4][0-9]|5[0-5]))\.){3}([1-9]?[0-9]|1[0-9][0-9]|2([0-4][0-9]|5[0-5]))\/[0-9][0-9]?[0-9]?$ ]]; then 9 | stat=0 10 | fi 11 | return ${stat} 12 | } 13 | 14 | string::isIPv6Block(){ 15 | local -r ip="${1}" 16 | local stat=1 17 | 18 | 19 | if [[ "${ip}" =~ ^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))\/[0-9][0-9]?[0-9]?$ ]] ; then 20 | stat=0 21 | fi 22 | return "${stat}" 23 | } 24 | 25 | set -euo pipefail 26 | IFS=$'\n\t' 27 | # shellcheck disable=SC2154 28 | trap 's=$?; echo "$0: Error on line "$LINENO": $BASH_COMMAND"; tput cnorm ; exit $s' ERR 29 | 30 | declare -r CURL_BIN=$(command -v curl) 31 | declare -r cfIP="https://api.cloudflare.com/client/v4/ips" 32 | declare -r cfConf='/etc/nginx/conf.d/cloudflare.conf' 33 | declare allOK='true' 34 | declare ips4 ips6 ip 35 | 36 | ips4=$( ${CURL_BIN} -sL "${cfIP}" | jq -r '.result.ipv4_cidrs[]' ) 37 | ips6=$( ${CURL_BIN} -sL "${cfIP}" | jq -r '.result.ipv6_cidrs[]' ) 38 | 39 | 40 | if [ -d /etc/nginx/conf.d ]; then 41 | for ip in ${ips4}; do 42 | if ! string::isIPv4Block "${ip}"; then 43 | echo "Invalid IPv4 block: ${ip}" >&2 44 | allOK='false' 45 | fi 46 | done 47 | for ip in ${ips6}; do 48 | if ! string::isIPv6Block "${ip}"; then 49 | echo "Invalid IPv6 block: ${ip}" >&2 50 | allOK='false' 51 | fi 52 | done 53 | 54 | if [[ 'true' == "${allOK}" ]]; then 55 | echo '# WordOps (wo) set visitors real ip with Cloudflare' > "${cfConf}" 56 | 57 | for ip in ${ips4}; do 58 | echo "set_real_ip_from ${ip};" >> "${cfConf}" 59 | done 60 | for ip in ${ips6}; do 61 | echo "set_real_ip_from ${ip};" >> "${cfConf}" 62 | done 63 | 64 | echo 'real_ip_header CF-Connecting-IP;' >> "${cfConf}" 65 | 66 | nginx -t && service nginx reload 67 | else 68 | echo "There are some errors in the IP blocks" >&2 69 | exit 1 70 | fi 71 | else 72 | echo "Can't find /etc/nginx/conf.d directory" >&2 73 | exit 1 74 | fi 75 | echo "Cloudflare IPs updated" 76 | echo "" 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thank you for considering contributing to WordOps. 4 | 5 | We love to receive contributions. Maintaining a CLI tool for deploying WordPress with Nginx with a few key strokes require to have enough knowledge for each stack configuration and to work with non-interactive operations. We rely on community contributions and user feedback to continue providing the best CLI tool to deploy WordPress with Nginx, PHP-FPM, MariaDB and Redis. 6 | 7 | There are many ways to contribute, with varying requirements of skills, explained in detail in the following sections. 8 | 9 | ## All WordOps Users 10 | 11 | ### Give WordOps a GitHub star 12 | 13 | This is the minimum open-source users should contribute back to the projects they use. Github stars help the project gain visibility, stand out. So, if you use WordOps, consider pressing that button. **It really matters**. 14 | 15 | ### Spread the word 16 | 17 | Community growth allows the project to attract new talent willing to contribute. This talent is then developing new features and improves the project. These new features and improvements attract more users and so on. It is a loop. So, post about WordOps, present it to local meetups you attend, let your online social network or twitter, facebook, reddit, etc. know you are using it. **The more people involved, the faster the project evolves**. 18 | 19 | ### Provide feedback 20 | 21 | Is there anything that bothers you about WordOps? Did you experience an issue while installing it or using it? Would you like to see it evolve to you need? Let us know. [Open a github issue](https://github.com/WordOps/WordOps/issues) to discuss it or open a thread on our [Community Forum](https://community.wordops.net/). Feedback is very important for open-source projects. We can't commit we will do everything, but your feedback influences our road-map significantly. 22 | 23 | ## Experienced Users 24 | 25 | ### Help other users 26 | 27 | As the project grows, an increasing share of our time is spent on supporting this community of users in terms of answering questions, of helping users understand how WordOps works and find their way with it. Helping other users is crucial. It allows the developers and maintainers of the project to focus on improving it. 28 | 29 | ### Improve documentation 30 | 31 | All of our documentation is in markdown (.md) files inside the WordOps GitHub project. All of our [HTML documentation](https://docs.wordops.net) is generated from these files. At the top right of each documentation page you will see a pencil, that leads you directly to the markdown file that was used to generated it. Don't be afraid to click it and edit any of these documents and submit a GitHub Pull Request with your corrections/additions. 32 | 33 | ## Developers 34 | 35 | (available soon) 36 | 37 | ### Contributions Ground Rules 38 | 39 | #### Code of Conduct and CLA 40 | 41 | We expect all contributors to abide by the [Contributor Covenant Code of Conduct](CODE_OF_CONDUCT.md). 42 | -------------------------------------------------------------------------------- /wo/core/logging.py: -------------------------------------------------------------------------------- 1 | """WordOps log module""" 2 | 3 | 4 | class Log: 5 | """ 6 | Logs messages with colors for different messages 7 | according to functions 8 | """ 9 | HEADER = '\033[95m' 10 | OKBLUE = '\033[94m' 11 | OKGREEN = '\033[92m' 12 | WARNING = '\033[93m' 13 | FAIL = '\033[91m' 14 | ENDC = '\033[0m' 15 | BOLD = '\033[1m' 16 | UNDERLINE = '\033[4m' 17 | 18 | def error(self, msg, exit=True): 19 | """ 20 | Logs error into log file 21 | """ 22 | print(Log.FAIL + msg + Log.ENDC) 23 | self.app.log.error(Log.FAIL + msg + Log.ENDC) 24 | if exit: 25 | self.app.close(1) 26 | 27 | def info(self, msg, end='\n', log=True): 28 | """ 29 | Logs info messages into log file 30 | """ 31 | 32 | print(Log.OKBLUE + msg + Log.ENDC, end=end) 33 | if log: 34 | self.app.log.info(Log.OKBLUE + msg + Log.ENDC) 35 | 36 | def warn(self, msg): 37 | """ 38 | Logs warning into log file 39 | """ 40 | print(Log.WARNING + msg + Log.ENDC) 41 | self.app.log.warning(Log.BOLD + msg + Log.ENDC) 42 | 43 | def debug(self, msg): 44 | """ 45 | Logs debug messages into log file 46 | """ 47 | self.app.log.debug(Log.HEADER + msg + Log.ENDC, __name__) 48 | 49 | def wait(self, msg, end='\r', log=True): 50 | """ 51 | Logs info messages with validation step 52 | """ 53 | space_to_add = int(31 - len(msg[0:31])) 54 | space = " " 55 | print( 56 | Log.OKBLUE + "{0}".format(msg[0:31]) + 57 | "{0}".format(space[0:space_to_add]) + 58 | " [" + Log.ENDC + ".." + Log.OKBLUE + "]" + Log.ENDC, end=end) 59 | if log: 60 | self.app.log.info(Log.OKBLUE + msg + Log.ENDC) 61 | 62 | def valide(self, msg, end='\n', log=True): 63 | """ 64 | Logs info messages after validation step 65 | """ 66 | space_to_add = int(31 - len(msg[0:31])) 67 | space = " " 68 | print( 69 | Log.OKBLUE + "{0}".format(msg[0:31]) + 70 | "{0}".format(space[0:space_to_add]) + 71 | " [" + Log.ENDC + Log.OKGREEN + "OK" + 72 | Log.ENDC + Log.OKBLUE + "]" + Log.ENDC, end=end) 73 | if log: 74 | self.app.log.info(Log.OKGREEN + msg + Log.ENDC) 75 | 76 | def failed(self, msg, end='\n', log=True): 77 | """ 78 | Logs info messages after validation step 79 | """ 80 | space_to_add = int(31 - len(msg[0:31])) 81 | space = " " 82 | print( 83 | Log.OKBLUE + "{0}".format(msg[0:31]) + 84 | "{0}".format(space[0:space_to_add]) + 85 | " [" + Log.ENDC + Log.FAIL + "KO" + 86 | Log.ENDC + Log.OKBLUE + "]" + Log.ENDC, end=end) 87 | if log: 88 | self.app.log.info(Log.FAIL + msg + Log.ENDC) 89 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | 2 | import glob 3 | import os 4 | import sys 5 | 6 | from setuptools import find_packages, setup 7 | 8 | # read the contents of your README file 9 | this_directory = os.path.abspath(os.path.dirname(__file__)) 10 | with open(os.path.join(this_directory, 'README.md'), encoding='utf-8') as f: 11 | LONG = f.read() 12 | 13 | conf = [] 14 | templates = [] 15 | 16 | for name in glob.glob('config/plugins.d/*.conf'): 17 | conf.insert(1, name) 18 | 19 | for name in glob.glob('wo/cli/templates/*.mustache'): 20 | templates.insert(1, name) 21 | 22 | if os.geteuid() == 0: 23 | if not os.path.exists('/var/log/wo/'): 24 | os.makedirs('/var/log/wo/') 25 | 26 | if not os.path.exists('/var/lib/wo/tmp/'): 27 | os.makedirs('/var/lib/wo/tmp/') 28 | 29 | setup(name='wordops', 30 | version='3.19.1', 31 | description='An essential toolset that eases server administration', 32 | long_description=LONG, 33 | long_description_content_type='text/markdown', 34 | classifiers=[ 35 | "Programming Language :: Python :: 3", 36 | "License :: OSI Approved :: MIT License", 37 | "Operating System :: OS Independent", 38 | "Development Status :: 5 - Production/Stable", 39 | "Environment :: Console", 40 | "Natural Language :: English", 41 | "Topic :: System :: Systems Administration", 42 | ], 43 | keywords='nginx automation wordpress deployment CLI', 44 | author='WordOps', 45 | author_email='contact@wordops.io', 46 | url='https://github.com/WordOps/WordOps', 47 | license='MIT', 48 | project_urls={ 49 | 'Documentation': 'https://docs.wordops.net', 50 | 'Forum': 'https://community.wordops.net', 51 | 'Source': 'https://github.com/WordOps/WordOps', 52 | 'Tracker': 'https://github.com/WordOps/WordOps/issues', 53 | }, 54 | packages=find_packages(exclude=['ez_setup', 'examples', 'tests', 55 | 'templates']), 56 | include_package_data=True, 57 | zip_safe=False, 58 | test_suite='nose.collector', 59 | python_requires='>=3.4', 60 | install_requires=[ 61 | # Required to build documentation 62 | # "Sphinx >= 1.0", 63 | # Required to function 64 | 'cement == 2.10.12', 65 | 'pystache', 66 | 'pynginxconfig', 67 | 'PyMySQL >= 1.0.2', 68 | 'psutil', 69 | 'sh', 70 | 'SQLAlchemy == 2.0.25', 71 | 'requests', 72 | 'distro', 73 | 'argcomplete', 74 | 'colorlog', 75 | ], 76 | extras_require={ # Optional 77 | 'testing': ['nose', 'coverage'], 78 | }, 79 | data_files=[('/etc/wo', ['config/wo.conf']), 80 | ('/etc/wo/plugins.d', conf), 81 | ('/usr/lib/wo/templates', templates), 82 | ('/etc/bash_completion.d/', 83 | ['config/bash_completion.d/wo_auto.rc']), 84 | ('/usr/share/man/man8/', ['docs/wo.8'])], 85 | setup_requires=[], 86 | entry_points=""" 87 | [console_scripts] 88 | wo = wo.cli.main:main 89 | """, 90 | namespace_packages=[], 91 | ) 92 | -------------------------------------------------------------------------------- /wo/cli/templates/map-wp.mustache: -------------------------------------------------------------------------------- 1 | # NGINX CONFIGURATION FOR FASTCGI_CACHE EXCEPTION - WordOps {{release}} 2 | # DO NOT MODIFY, ALL CHANGES WILL BE LOST AFTER AN WordOps (wo) UPDATE 3 | 4 | # do not cache xhtml request 5 | map $http_x_requested_with $http_request_no_cache { 6 | default 0; 7 | XMLHttpRequest 1; 8 | } 9 | 10 | # do not cache requests with Authorization: header set 11 | map $http_authorization $http_auth_no_cache { 12 | default 1; 13 | "" 0; 14 | } 15 | 16 | # do not cache requests on cookies 17 | map $http_cookie $cookie_no_cache { 18 | default 0; 19 | "~*wordpress_[a-f0-9]+" 1; 20 | "~*wp-postpass" 1; 21 | "~*wordpress_logged_in" 1; 22 | "~*wordpress_no_cache" 1; 23 | "~*comment_author" 1; 24 | "~*woocommerce_items_in_cart" 1; 25 | "~*edd_items_in_cart" 1; 26 | "~*woocommerce_cart_hash" 1; 27 | "~*wptouch_switch_toogle" 1; 28 | "~*comment_author_email_" 1; 29 | "~*wptouch_switch_toggle" 1; 30 | "~*edd" 1; 31 | } 32 | 33 | # do not cache the following uri 34 | map $request_uri $uri_no_cache { 35 | default 0; 36 | "~*/wishlist/" 1; 37 | "~*/wp-admin/" 1; 38 | "~*/wp-[a-zA-Z0-9-]+\.php" 1; 39 | "~*/feed/" 1; 40 | "~*/index\.php" 1; 41 | "~*/[a-z0-9_-]+-sitemap([0-9]+)?\.xml" 1; 42 | "~*/sitemap(_index)?\.xml" 1; 43 | "~*/wp-comments-popup\.php" 1; 44 | "~*/wp-links-opml\.php" 1; 45 | "~*/xmlrpc\.php" 1; 46 | "~*/edd-sl/.*" 1; 47 | "~*/add_to_cart/" 1; 48 | "~*/cart/" 1; 49 | "~*/account/" 1; 50 | "~*/my-account/" 1; 51 | "~*/checkout/" 1; 52 | "~*/addons/" 1; 53 | "~*/wc-api/.*" 1; 54 | "~*/logout/" 1; 55 | "~*/lost-password/" 1; 56 | "~*/panier/" 1; 57 | "~*/mon-compte/" 1; 58 | "~*/embed" 1; 59 | "~*/commande/" 1; 60 | "~*/resetpass/" 1; 61 | "~*/wp.serviceworker" 1; 62 | } 63 | # mobile_prefix needed for WP-Rocket 64 | map $http_user_agent $mobile_prefix { 65 | default ""; 66 | "~*iphone" -mobile; 67 | "~*android" -mobile; 68 | } 69 | 70 | # do not cache requests with query strings 71 | map $is_args $is_args_no_cache { 72 | default 1; 73 | "" 0; 74 | } 75 | 76 | # cache requests with query string related to analytics 77 | map $args $args_to_cache { 78 | default 0; 79 | "~*utm_" 1; 80 | "~*fbclid" 1; 81 | } 82 | 83 | # do not cache requests with query strings excepted analytics related queries 84 | map $is_args_no_cache$args_to_cache $query_no_cache { 85 | defaut 1; 86 | 00 0; 87 | 11 0; 88 | } 89 | 90 | # if all previous check are passed, $skip_cache = 0 91 | map $http_request_no_cache$http_auth_no_cache$cookie_no_cache$uri_no_cache$query_no_cache $skip_cache { 92 | default 1; 93 | 00000 0; 94 | } 95 | 96 | # map $skip_cache with $cache_uri for --wpsc --wpce & --wprocket stack 97 | map $skip_cache $cache_uri { 98 | 0 $request_uri; 99 | default 'null cache'; 100 | } 101 | 102 | # http_prefix needed for WP-Rocket 103 | map $https $https_prefix { 104 | default ""; 105 | on "-https"; 106 | } 107 | 108 | # needed to proxy web-socket connections 109 | map $http_upgrade $connection_upgrade { 110 | default upgrade; 111 | '' close; 112 | } 113 | -------------------------------------------------------------------------------- /wo/cli/templates/nextcloud.mustache: -------------------------------------------------------------------------------- 1 | # WordOps nextcloud configuration 2 | add_header X-Robots-Tag none; 3 | add_header X-Permitted-Cross-Domain-Policies none; 4 | add_header Referrer-Policy no-referrer; 5 | location = /robots.txt { 6 | allow all; 7 | log_not_found off; 8 | access_log off; 9 | } 10 | location / { 11 | rewrite ^ /index.php; 12 | } 13 | location ~ ^\/(?:build|tests|config|lib|3rdparty|templates|data)\/ { 14 | deny all; 15 | } 16 | location ~ ^\/(?:\.|autotest|occ|issue|indie|db_|console) { 17 | deny all; 18 | } 19 | location ~ ^\/(?:index|remote|public|cron|core\/ajax\/update|status|ocs\/v[12]|updater\/.+|oc[ms]-provider\/.+)\.php(?:$|\/) { 20 | fastcgi_split_path_info ^(.+?\.php)(\/.*|)$; 21 | try_files $uri =404; 22 | include fastcgi_params; 23 | fastcgi_param PATH_INFO $fastcgi_path_info; 24 | # Avoid sending the security headers twice 25 | fastcgi_param modHeadersAvailable true; 26 | # Enable pretty urls 27 | fastcgi_param front_controller_active true; 28 | fastcgi_pass {{upstream}}; 29 | fastcgi_intercept_errors on; 30 | fastcgi_request_buffering off; 31 | } 32 | location ~ ^\/(?:updater|oc[ms]-provider)(?:$|\/) { 33 | try_files $uri/ =404; 34 | index index.php; 35 | } 36 | # Adding the cache control header for js, css and map files 37 | # Make sure it is BELOW the PHP block 38 | location ~ \.(?:css|js|woff2?|svg|gif|map)$ { 39 | try_files $uri /index.php$request_uri; 40 | add_header Cache-Control "public, max-age=15778463"; 41 | # Add headers to serve security related headers (It is intended to 42 | # have those duplicated to the ones above) 43 | # Before enabling Strict-Transport-Security headers please read into 44 | # this topic first. 45 | #add_header Strict-Transport-Security "max-age=15768000; includeSubDomains; preload;"; 46 | # 47 | # WARNING: Only add the preload option once you read about 48 | # the consequences in https://hstspreload.org/. This option 49 | # will add the domain to a hardcoded list that is shipped 50 | # in all major browsers and getting removed from this list 51 | # could take several months. 52 | add_header X-Content-Type-Options nosniff; 53 | add_header X-XSS-Protection "1; mode=block"; 54 | add_header X-Robots-Tag none; 55 | add_header X-Download-Options noopen; 56 | add_header X-Permitted-Cross-Domain-Policies none; 57 | add_header Referrer-Policy no-referrer; 58 | # Optional: Don't log access to assets 59 | access_log off; 60 | } 61 | location ~ \.(?:png|html|ttf|ico|jpg|jpeg|bcmap)$ { 62 | try_files $uri /index.php$request_uri; 63 | # Optional: Don't log access to other assets 64 | access_log off; 65 | } 66 | # Enable gzip but do not remove ETag headers 67 | gzip on; 68 | gzip_vary on; 69 | gzip_comp_level 4; 70 | gzip_min_length 256; 71 | gzip_proxied expired no-cache no-store private no_last_modified no_etag auth; 72 | gzip_types application/atom+xml application/javascript application/json application/ld+json application/manifest+json application/rss+xml application/vnd.geo+json application/vnd.ms-fontobject application/x-font-ttf application/x-web-app-manifest+json application/xhtml+xml application/xml font/opentype image/bmp image/svg+xml image/x-icon text/cache-manifest text/css text/plain text/vcard text/vnd.rim.location.xloc text/vtt text/x-component text/x-cross-domain-policy; 73 | -------------------------------------------------------------------------------- /wo/core/shellexec.py: -------------------------------------------------------------------------------- 1 | """WordOps Shell Functions""" 2 | import subprocess 3 | 4 | from wo.core.logging import Log 5 | 6 | 7 | class CommandExecutionError(Exception): 8 | """custom Exception for command execution""" 9 | pass 10 | 11 | 12 | class WOShellExec(): 13 | """Method to run shell commands""" 14 | def __init__(): 15 | pass 16 | 17 | def cmd_exec(self, command, errormsg='', log=True): 18 | """Run shell command from Python""" 19 | try: 20 | log and Log.debug(self, "Running command: {0}".format(command)) 21 | 22 | with subprocess.Popen([command], stdout=subprocess.PIPE, 23 | stderr=subprocess.PIPE, shell=True) as proc: 24 | (cmd_stdout_bytes, cmd_stderr_bytes) = proc.communicate() 25 | (cmd_stdout, cmd_stderr) = (cmd_stdout_bytes.decode('utf-8', 26 | "replace"), 27 | cmd_stderr_bytes.decode('utf-8', 28 | "replace")) 29 | 30 | Log.debug(self, "Command Output: {0}, \nCommand Error: {1}" 31 | .format(cmd_stdout, cmd_stderr)) 32 | return bool(proc.returncode == 0) 33 | except OSError as e: 34 | Log.debug(self, str(e)) 35 | raise CommandExecutionError 36 | except Exception as e: 37 | Log.debug(self, str(e)) 38 | raise CommandExecutionError 39 | 40 | def invoke_editor(self, filepath, errormsg=''): 41 | """ 42 | Open files using sensible editor 43 | """ 44 | try: 45 | subprocess.call(['sensible-editor', filepath]) 46 | except OSError as e: 47 | Log.debug(self, "{0}{1}".format(e.errno, e.strerror)) 48 | raise CommandExecutionError 49 | 50 | def cmd_exec_stdout(self, command, errormsg='', log=True): 51 | """Run shell command from Python""" 52 | try: 53 | log and Log.debug(self, "Running command: {0}".format(command)) 54 | with subprocess.Popen([command], stdout=subprocess.PIPE, 55 | stderr=subprocess.PIPE, shell=True) as proc: 56 | (cmd_stdout_bytes, cmd_stderr_bytes) = proc.communicate() 57 | (cmd_stdout, cmd_stderr) = (cmd_stdout_bytes.decode('utf-8', 58 | "replace"), 59 | cmd_stderr_bytes.decode('utf-8', 60 | "replace")) 61 | 62 | if proc.returncode == 0: 63 | Log.debug(self, "Command Output: {0}, \nCommand Error: {1}" 64 | .format(cmd_stdout, cmd_stderr)) 65 | return cmd_stdout 66 | else: 67 | Log.debug(self, "Command Output: {0}, \nCommand Error: {1}" 68 | .format(cmd_stdout, cmd_stderr)) 69 | return cmd_stdout 70 | except OSError as e: 71 | Log.debug(self, str(e)) 72 | raise CommandExecutionError 73 | except Exception as e: 74 | Log.debug(self, str(e)) 75 | raise CommandExecutionError 76 | -------------------------------------------------------------------------------- /wo/cli/templates/nginx-core.mustache: -------------------------------------------------------------------------------- 1 | user www-data; 2 | worker_processes auto; 3 | worker_cpu_affinity auto; 4 | worker_rlimit_nofile 100000; 5 | pid /run/nginx.pid; 6 | 7 | pcre_jit on; 8 | 9 | events { 10 | multi_accept on; 11 | worker_connections 50000; 12 | accept_mutex on; 13 | use epoll; 14 | } 15 | 16 | 17 | http { 18 | ## 19 | # WordOps Settings - WordOps {{release}} 20 | ## 21 | 22 | keepalive_timeout 8; 23 | 24 | # Nginx AIO : See - https://www.nginx.com/blog/thread-pools-boost-performance-9x/ 25 | # http://nginx.org/en/docs/http/ngx_http_core_module.html#aio 26 | aio threads; 27 | 28 | server_tokens off; 29 | reset_timedout_connection on; 30 | more_set_headers "X-Powered-By : WordOps"; 31 | 32 | # Limit Request 33 | limit_req_status 403; 34 | limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s; 35 | limit_req_zone $binary_remote_addr zone=two:10m rate=10r/s; 36 | 37 | # Proxy Settings 38 | # set_real_ip_from proxy-server-ip; 39 | # real_ip_header X-Forwarded-For; 40 | 41 | fastcgi_read_timeout 300; 42 | client_max_body_size 100m; 43 | 44 | # ngx_vts_module 45 | vhost_traffic_status_zone; 46 | 47 | # tls dynamic records patch directive 48 | ssl_dyn_rec_enable on; 49 | 50 | ## 51 | # SSL Settings 52 | ## 53 | 54 | # Enable 0-RTT support for TLS 1.3 55 | proxy_set_header Early-Data $ssl_early_data; 56 | ssl_early_data on; 57 | 58 | ssl_session_timeout 1d; 59 | ssl_session_cache shared:SSL:50m; 60 | ssl_session_tickets off; 61 | ssl_prefer_server_ciphers on; 62 | ssl_ciphers 'TLS13+AESGCM+AES256:TLS13+AESGCM+AES128:TLS13+CHACHA20:EECDH+AESGCM:EECDH+CHACHA20'; 63 | ssl_protocols TLSv1.2 TLSv1.3; 64 | ssl_ecdh_curve X25519:P-521:P-384:P-256; 65 | 66 | # Common security headers 67 | more_set_headers "X-Frame-Options : SAMEORIGIN"; 68 | more_set_headers "X-Content-Type-Options : nosniff"; 69 | more_set_headers "Referrer-Policy : strict-origin-when-cross-origin"; 70 | 71 | # oscp settings 72 | resolver 8.8.8.8 1.1.1.1 8.8.4.4 1.0.0.1 valid=300s; 73 | resolver_timeout 10; 74 | ssl_stapling on; 75 | 76 | ## 77 | # Basic Settings 78 | ## 79 | # server_names_hash_bucket_size 64; 80 | # server_name_in_redirect off; 81 | 82 | include /etc/nginx/mime.types; 83 | default_type application/octet-stream; 84 | 85 | ## 86 | # Logging Settings 87 | ## 88 | 89 | access_log off; 90 | error_log /var/log/nginx/error.log; 91 | 92 | # Log format Settings 93 | log_format rt_cache '$remote_addr $upstream_response_time $upstream_cache_status [$time_local] ' 94 | '$http_host "$request" $status $body_bytes_sent ' 95 | '"$http_referer" "$http_user_agent" "$server_protocol"'; 96 | 97 | ## 98 | # Virtual Host Configs 99 | ## 100 | 101 | include /etc/nginx/conf.d/*.conf; 102 | include /etc/nginx/sites-enabled/*; 103 | } 104 | 105 | 106 | #mail { 107 | # # See sample authentication script at: 108 | # # http://wiki.nginx.org/ImapAuthenticateWithApachePhpScript 109 | # 110 | # # auth_http localhost/auth.php; 111 | # # pop3_capabilities "TOP" "USER"; 112 | # # imap_capabilities "IMAP4rev1" "UIDPLUS"; 113 | # 114 | # server { 115 | # listen localhost:110; 116 | # protocol pop3; 117 | # proxy on; 118 | # } 119 | # 120 | # server { 121 | # listen localhost:143; 122 | # protocol imap; 123 | # proxy on; 124 | # } 125 | #} 126 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team lead at contact@wordops.io. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | -------------------------------------------------------------------------------- /wo/cli/templates/locations.mustache: -------------------------------------------------------------------------------- 1 | # NGINX CONFIGURATION FOR COMMON LOCATION - WordOps {{release}} 2 | # DO NOT MODIFY, ALL CHANGES WILL BE LOST AFTER AN WordOps (wo) UPDATE 3 | # Basic locations files 4 | location = /favicon.ico { 5 | try_files /wp-content/uploads/fbrfg/favicon.ico $uri $uri/ /index.php?$args @empty_gif; 6 | access_log off; 7 | log_not_found off; 8 | expires max; 9 | } 10 | location @empty_gif { 11 | empty_gif; 12 | } 13 | # Cache static files 14 | location ~* \.(ogg|ogv|svg|svgz|eot|otf|woff|woff2|ttf|m4a|mp4|ttf|rss|atom|jpe?g|gif|cur|heic|png|tiff|ico|webm|mp3|aac|tgz|doc|xls|exe|ppt|tar|mid|midi|wav|bmp|rtf|swf|webp|json|webmanifest|cast)$ { 15 | more_set_headers 'Access-Control-Allow-Origin : *'; 16 | more_set_headers "Cache-Control : public, no-transform"; 17 | access_log off; 18 | log_not_found off; 19 | expires max; 20 | } 21 | # Cache css & js files 22 | location ~* \.(?:css(\.map)?|js(\.map)?)$ { 23 | more_set_headers 'Access-Control-Allow-Origin : *'; 24 | more_set_headers "Cache-Control : public, no-transform"; 25 | access_log off; 26 | log_not_found off; 27 | expires 1y; 28 | } 29 | # Security settings for better privacy 30 | # Deny hidden files 31 | location ~ /\.(?!well-known\/) { 32 | deny all; 33 | } 34 | # letsencrypt validation 35 | location /.well-known/acme-challenge/ { 36 | alias /var/www/html/.well-known/acme-challenge/; 37 | allow all; 38 | auth_basic off; 39 | } 40 | # Private Prefetch Proxy 41 | # https://developer.chrome.com/blog/private-prefetch-proxy/ 42 | location /.well-known/traffic-advice { 43 | types { } default_type "application/trafficadvice+json; charset=utf-8"; 44 | return 200 "[{\n \"user_agent\": \"prefetch-proxy\",\n \"google_prefetch_proxy_eap\": {\n \"fraction\": 1.0\n }\n}]"; 45 | allow all; 46 | } 47 | # Return 403 forbidden for readme.(txt|html) or license.(txt|html) or example.(txt|html) or other common git repository files 48 | location ~* "/(^$|readme|license|example|README|LEGALNOTICE|INSTALLATION|CHANGELOG)\.(txt|html|md)" { 49 | deny all; 50 | } 51 | # Deny backup extensions & log files and return 403 forbidden 52 | location ~* "\.(old|orig|original|php#|php~|php_bak|save|swo|aspx?|tpl|sh|bash|bak?|cfg|cgi|dll|exe|git|hg|ini|jsp|log|mdb|out|sql|svn|swp|tar|rdf|gz|zip|bz2|7z|pem|asc|conf|dump)$" { 53 | deny all; 54 | } 55 | location ~* "/(=|\$&|_mm|(wp-)?config\.|cgi-|etc/passwd|muieblack)" { 56 | deny all; 57 | } 58 | 59 | # block base64_encoded content 60 | location ~* "(base64_encode)(.*)(\()" { 61 | deny all; 62 | } 63 | 64 | # block javascript eval() 65 | location ~* "(eval\()" { 66 | deny all; 67 | } 68 | 69 | # Additional security settings 70 | 71 | location ~* "(127\.0\.0\.1)" { 72 | deny all; 73 | } 74 | location ~* "([a-z0-9]{2000})" { 75 | deny all; 76 | } 77 | location ~* "(javascript\:)(.*)(\;)" { 78 | deny all; 79 | } 80 | location ~* "(GLOBALS|REQUEST)(=|\[|%)" { 81 | deny all; 82 | } 83 | location ~* "(<|%3C).*script.*(>|%3)" { 84 | deny all; 85 | } 86 | location ~ "(\\|\.\.\.|\.\./|~|`|<|>|\|)" { 87 | deny all; 88 | } 89 | location ~* "(boot\.ini|etc/passwd|self/environ)" { 90 | deny all; 91 | } 92 | location ~* "(thumbs?(_editor|open)?|tim(thumb)?)\.php" { 93 | deny all; 94 | } 95 | location ~* "(\'|\")(.*)(drop|insert|md5|select|union)" { 96 | deny all; 97 | } 98 | location ~* "(https?|ftp|php):/" { 99 | deny all; 100 | } 101 | location ~* "(=\\\'|=\\%27|/\\\'/?)\." { 102 | deny all; 103 | } 104 | location ~ "(\{0\}|\(/\(|\.\.\.|\+\+\+|\\\"\\\")" { 105 | deny all; 106 | } 107 | location ~ "(~|`|<|>|:|;|%|\\|\s|\{|\}|\[|\]|\|)" { 108 | deny all; 109 | } 110 | -------------------------------------------------------------------------------- /wo/cli/plugins/clean.py: -------------------------------------------------------------------------------- 1 | """Clean Plugin for WordOps.""" 2 | 3 | import os 4 | import requests 5 | 6 | from cement.core.controller import CementBaseController, expose 7 | 8 | from wo.core.aptget import WOAptGet 9 | from wo.core.logging import Log 10 | from wo.core.services import WOService 11 | from wo.core.shellexec import WOShellExec 12 | from wo.core.variables import WOVar 13 | 14 | 15 | def wo_clean_hook(app): 16 | pass 17 | 18 | 19 | class WOCleanController(CementBaseController): 20 | class Meta: 21 | label = 'clean' 22 | stacked_on = 'base' 23 | stacked_type = 'nested' 24 | description = ( 25 | 'Clean NGINX FastCGI cache, Opcache, Redis Cache') 26 | arguments = [ 27 | (['--all'], 28 | dict(help='Clean all cache', action='store_true')), 29 | (['--fastcgi'], 30 | dict(help='Clean FastCGI cache', action='store_true')), 31 | (['--opcache'], 32 | dict(help='Clean OpCache', action='store_true')), 33 | (['--redis'], 34 | dict(help='Clean Redis Cache', action='store_true')), 35 | ] 36 | usage = "wo clean [options]" 37 | 38 | @expose(hide=True) 39 | def default(self): 40 | pargs = self.app.pargs 41 | if ((not pargs.all) and (not pargs.fastcgi) and 42 | (not pargs.opcache) and (not pargs.redis)): 43 | self.clean_fastcgi() 44 | if pargs.all: 45 | self.clean_fastcgi() 46 | self.clean_opcache() 47 | self.clean_redis() 48 | if pargs.fastcgi: 49 | self.clean_fastcgi() 50 | if pargs.opcache: 51 | self.clean_opcache() 52 | if pargs.redis: 53 | self.clean_redis() 54 | 55 | @expose(hide=True) 56 | def clean_redis(self): 57 | """This function clears Redis cache""" 58 | if (WOAptGet.is_installed(self, "redis-server")): 59 | Log.info(self, "Cleaning Redis cache") 60 | WOShellExec.cmd_exec(self, "redis-cli flushall") 61 | else: 62 | Log.info(self, "Redis is not installed") 63 | 64 | @expose(hide=True) 65 | def clean_fastcgi(self): 66 | if (os.path.isdir("/var/run/nginx-cache") and 67 | os.path.exists('/usr/sbin/nginx')): 68 | Log.info(self, "Cleaning NGINX FastCGI cache") 69 | WOShellExec.cmd_exec(self, "rm -rf /var/run/nginx-cache/*") 70 | WOService.restart_service(self, 'nginx') 71 | else: 72 | Log.error(self, "Unable to clean FastCGI cache", False) 73 | 74 | @expose(hide=True) 75 | def clean_opcache(self): 76 | opcache_dir = '/var/www/22222/htdocs/cache/opcache/' 77 | if (os.path.exists('/usr/sbin/nginx') and 78 | os.path.exists( 79 | '/var/www/22222/htdocs/cache/opcache')): 80 | try: 81 | Log.info(self, "Cleaning opcache") 82 | wo_php_version = list(WOVar.wo_php_versions.keys()) 83 | for wo_php in wo_php_version: 84 | if os.path.exists('{0}{1}.php'.format(opcache_dir, wo_php)): 85 | requests.get( 86 | "http://127.0.0.1/cache/opcache/{0}.php".format(wo_php)) 87 | 88 | except requests.HTTPError as e: 89 | Log.debug(self, "{0}".format(e)) 90 | Log.debug(self, "Unable hit url, " 91 | " http://127.0.0.1/cache/opcache/" 92 | "php72.php," 93 | " please check you have admin tools installed") 94 | Log.debug(self, "please check you have admin tools installed," 95 | " or install them with `wo stack install --admin`") 96 | Log.error(self, "Unable to clean opcache", False) 97 | 98 | 99 | def load(app): 100 | # register the plugin class.. this only happens if the plugin is enabled 101 | app.handler.register(WOCleanController) 102 | # register a hook (function) to run after arguments are parsed. 103 | app.hook.register('post_argument_parsing', wo_clean_hook) 104 | -------------------------------------------------------------------------------- /wo/cli/main.py: -------------------------------------------------------------------------------- 1 | """WordOps main application entry point.""" 2 | import sys 3 | from os import geteuid 4 | 5 | from cement.core.exc import CaughtSignal, FrameworkError 6 | from cement.core.foundation import CementApp 7 | from cement.ext.ext_argparse import ArgParseArgumentHandler 8 | from cement.utils.misc import init_defaults 9 | 10 | from wo.core import exc 11 | 12 | # Application default. Should update config/wo.conf to reflect any 13 | # changes, or additions here. 14 | defaults = init_defaults('wo') 15 | 16 | # All internal/external plugin configurations are loaded from here 17 | defaults['wo']['plugin_config_dir'] = '/etc/wo/plugins.d' 18 | 19 | # External plugins (generally, do not ship with application code) 20 | defaults['wo']['plugin_dir'] = '/var/lib/wo/plugins' 21 | 22 | # External templates (generally, do not ship with application code) 23 | defaults['wo']['template_dir'] = '/var/lib/wo/templates' 24 | 25 | 26 | def encode_output(app, text): 27 | """ Encode the output to be suitable for the terminal 28 | 29 | :param app: The Cement App (unused) 30 | :param text: The rendered text 31 | :return: The encoded text 32 | """ 33 | 34 | return text.encode("utf-8") 35 | 36 | 37 | class WOArgHandler(ArgParseArgumentHandler): 38 | class Meta: 39 | label = 'wo_args_handler' 40 | 41 | def error(self, message): 42 | super(WOArgHandler, self).error("unknown args") 43 | 44 | 45 | class WOApp(CementApp): 46 | class Meta: 47 | label = 'wo' 48 | 49 | config_defaults = defaults 50 | 51 | # All built-in application bootstrapping (always run) 52 | bootstrap = 'wo.cli.bootstrap' 53 | 54 | # Internal plugins (ship with application code) 55 | plugin_bootstrap = 'wo.cli.plugins' 56 | 57 | # Internal templates (ship with application code) 58 | template_module = 'wo.cli.templates' 59 | 60 | extensions = ['mustache', 'argcomplete', 'colorlog'] 61 | 62 | hooks = [ 63 | ("post_render", encode_output) 64 | ] 65 | 66 | output_handler = 'mustache' 67 | 68 | log_handler = 'colorlog' 69 | 70 | arg_handler = WOArgHandler 71 | exit_on_close = True 72 | 73 | 74 | class WOTestApp(WOApp): 75 | """A test app that is better suited for testing.""" 76 | class Meta: 77 | # default argv to empty (don't use sys.argv) 78 | argv = [] 79 | 80 | # don't look for config files (could break tests) 81 | config_files = [] 82 | 83 | # don't call sys.exit() when app.close() is called in tests 84 | exit_on_close = False 85 | 86 | 87 | # Define the applicaiton object outside of main, as some libraries might wish 88 | # to import it as a global (rather than passing it into another class/func) 89 | app = WOApp() 90 | 91 | 92 | def main(): 93 | with app: 94 | try: 95 | global sys 96 | 97 | # if not root...kick out 98 | if not geteuid() == 0: 99 | print("\nNon-privileged users cant use WordOps. " 100 | "Switch to root or invoke sudo.\n") 101 | app.close(1) 102 | app.run() 103 | except AssertionError as e: 104 | print("AssertionError => %s" % e.args[0]) 105 | app.exit_code = 1 106 | except exc.WOError as e: 107 | # Catch our application errors and exit 1 (error) 108 | print('WOError > %s' % e) 109 | app.exit_code = 1 110 | except CaughtSignal as e: 111 | # Default Cement signals are SIGINT and SIGTERM, exit 0 (non-error) 112 | print('CaughtSignal > %s' % e) 113 | app.exit_code = 0 114 | except FrameworkError as e: 115 | # Catch framework errors and exit 1 (error) 116 | print('FrameworkError > %s' % e) 117 | app.exit_code = 1 118 | finally: 119 | # Maybe we want to see a full-stack trace for the above 120 | # exceptions, but only if --debug was passed? 121 | if app.debug: 122 | import traceback 123 | traceback.print_exc() 124 | 125 | 126 | if __name__ == '__main__': 127 | main() 128 | -------------------------------------------------------------------------------- /wo/core/git.py: -------------------------------------------------------------------------------- 1 | """WordOps GIT module""" 2 | import os 3 | 4 | from sh import ErrorReturnCode, git 5 | from wo.core.logging import Log 6 | 7 | 8 | class WOGit: 9 | """Intialization of core variables""" 10 | def ___init__(): 11 | # TODO method for core variables 12 | pass 13 | 14 | def add(self, paths, msg="Intializating"): 15 | """ 16 | Initializes Directory as repository if not already git repo. 17 | and adds uncommited changes automatically 18 | """ 19 | for path in paths: 20 | global git 21 | wogit = git.bake("-C", "{0}".format(path)) 22 | if os.path.isdir(path): 23 | if not os.path.isdir(path + "/.git"): 24 | try: 25 | Log.debug(self, "WOGit: git init at {0}" 26 | .format(path)) 27 | wogit.init(path) 28 | except ErrorReturnCode as e: 29 | Log.debug(self, "{0}".format(e)) 30 | Log.error(self, "Unable to git init at {0}" 31 | .format(path)) 32 | 33 | status = wogit.status("-s") 34 | if len(status.splitlines()) > 0: 35 | try: 36 | Log.debug(self, "WOGit: git commit at {0}" 37 | .format(path)) 38 | wogit.add("--all") 39 | wogit.commit("-am {0}".format(msg)) 40 | except ErrorReturnCode as e: 41 | Log.debug(self, "{0}".format(e)) 42 | Log.error(self, "Unable to git commit at {0} " 43 | .format(path)) 44 | else: 45 | Log.debug(self, "WOGit: Path {0} not present".format(path)) 46 | 47 | def checkfilestatus(self, repo, filepath): 48 | """ 49 | Checks status of file, If its tracked or untracked. 50 | """ 51 | global git 52 | wogit = git.bake("-C", "{0}".format(repo)) 53 | status = wogit.status("-s", "{0}".format(filepath)) 54 | if len(status.splitlines()) > 0: 55 | return True 56 | else: 57 | return False 58 | 59 | def rollback(self, paths, msg="Rolling-Back"): 60 | """ 61 | Rollback last commit to restore previous. 62 | configuration and commit changes automatically 63 | """ 64 | for path in paths: 65 | global git 66 | wogit = git.bake("-C", "{0}".format(path)) 67 | if os.path.isdir(path): 68 | if not os.path.isdir(path + "/.git"): 69 | Log.error( 70 | self, "Unable to find a git repository at {0}" 71 | .format(path)) 72 | try: 73 | Log.debug( 74 | self, "WOGit: git stash --include-untracked at {0}" 75 | .format(path)) 76 | wogit.stash("push", "--include-untracked", "-m {0}" 77 | .format(msg)) 78 | except ErrorReturnCode as e: 79 | Log.debug(self, "{0}".format(e)) 80 | Log.error(self, "Unable to git reset at {0} " 81 | .format(path)) 82 | else: 83 | Log.debug(self, "WOGit: Path {0} not present".format(path)) 84 | 85 | def clone(self, repo, path, branch='master'): 86 | """Equivalent to git clone """ 87 | if not os.path.exists('{0}'.format(path)): 88 | global git 89 | try: 90 | git.clone( 91 | '{0}'.format(repo), 92 | '{0}'.format(path), 93 | '--branch={0}'.format(branch), 94 | '--depth=1') 95 | except ErrorReturnCode as e: 96 | Log.debug(self, "{0}".format(e)) 97 | Log.error(self, "Unable to git clone at {0} " 98 | .format(path)) 99 | else: 100 | Log.debug(self, "WOGit: Path {0} already exist".format(path)) 101 | -------------------------------------------------------------------------------- /wo/cli/templates/wpcommon.mustache: -------------------------------------------------------------------------------- 1 | # WordPress COMMON SETTINGS - WordOps {{release}} 2 | # DO NOT MODIFY, ALL CHANGES WILL BE LOST AFTER AN WordOps (wo) UPDATE 3 | # Limit access to avoid brute force attack 4 | location = /wp-login.php { 5 | limit_req zone=one burst=1 nodelay; 6 | include fastcgi_params; 7 | fastcgi_pass {{upstream}}; 8 | } 9 | # Prevent DoS attacks on wp-cron 10 | location = /wp-cron.php { 11 | limit_req zone=two burst=1 nodelay; 12 | include fastcgi_params; 13 | fastcgi_pass {{upstream}}; 14 | } 15 | # Prevent DoS attacks with xmlrpc.php 16 | location = /xmlrpc.php { 17 | # Whitelist Jetpack IP ranges, Allow all Communications Between Jetpack and WordPress.com 18 | allow 122.248.245.244/32; 19 | allow 54.217.201.243/32; 20 | allow 54.232.116.4/32; 21 | allow 192.0.80.0/20; 22 | allow 192.0.96.0/20; 23 | allow 192.0.112.0/20; 24 | allow 195.234.108.0/22; 25 | 26 | # Deny all other requests 27 | deny all; 28 | 29 | # Disable access and error logging 30 | access_log off; 31 | log_not_found off; 32 | 33 | # Limit the rate of requests to prevent DoS attacks 34 | limit_req zone=two burst=1 nodelay; 35 | 36 | # Pass the request to PHP-FPM backend 37 | include fastcgi_params; 38 | fastcgi_pass {{upstream}}; 39 | } 40 | # Disable wp-config.txt 41 | location = /wp-config.txt { 42 | deny all; 43 | access_log off; 44 | log_not_found off; 45 | } 46 | location = /robots.txt { 47 | # Some WordPress plugin gererate robots.txt file 48 | # Refer #340 issue 49 | try_files $uri $uri/ /index.php?$args @robots; 50 | access_log off; 51 | log_not_found off; 52 | } 53 | # fallback for robots.txt with default wordpress rules 54 | location @robots { 55 | return 200 "User-agent: *\nDisallow: /wp-admin/\nAllow: /wp-admin/admin-ajax.php\n"; 56 | } 57 | # webp rewrite rules for jpg and png images 58 | # try to load alternative image.png.webp before image.png 59 | location /wp-content/uploads { 60 | location ~ \.(png|jpe?g)$ { 61 | add_header Vary "Accept-Encoding"; 62 | more_set_headers 'Access-Control-Allow-Origin : *'; 63 | more_set_headers "Cache-Control : public, no-transform"; 64 | access_log off; 65 | log_not_found off; 66 | expires max; 67 | try_files $uri$avif_suffix $uri$webp_suffix $uri =404; 68 | } 69 | location ~* \.(php|gz|log|zip|tar|rar|xz)$ { 70 | #Prevent Direct Access Of PHP Files & Backups from Web Browsers 71 | deny all; 72 | } 73 | } 74 | # webp rewrite rules for EWWW testing image 75 | location /wp-content/plugins/ewww-image-optimizer/images { 76 | location ~ \.(png|jpe?g)$ { 77 | add_header Vary "Accept-Encoding"; 78 | more_set_headers 'Access-Control-Allow-Origin : *'; 79 | more_set_headers "Cache-Control : public, no-transform"; 80 | access_log off; 81 | log_not_found off; 82 | expires max; 83 | try_files $uri$avif_suffix $uri$webp_suffix $uri =404; 84 | } 85 | location ~ \.php$ { 86 | #Prevent Direct Access Of PHP Files From Web Browsers 87 | deny all; 88 | } 89 | } 90 | # enable gzip on static assets - php files are forbidden 91 | location /wp-content/cache { 92 | # Cache css & js files 93 | location ~* \.(?:css(\.map)?|js(\.map)?|.html)$ { 94 | more_set_headers 'Access-Control-Allow-Origin : *'; 95 | access_log off; 96 | log_not_found off; 97 | expires 1y; 98 | } 99 | location ~ \.php$ { 100 | #Prevent Direct Access Of PHP Files From Web Browsers 101 | deny all; 102 | } 103 | } 104 | # Deny access to any files with a .php extension in the uploads directory 105 | # Works in sub-directory installs and also in multisite network 106 | # Keep logging the requests to parse later (or to pass to firewall utilities such as fail2ban) 107 | location ~* /(?:uploads|files)/.*\.php$ { 108 | deny all; 109 | } 110 | # mitigate DoS attack CVE with WordPress script concatenation 111 | # add the following line to wp-config.php 112 | # define( 'CONCATENATE_SCRIPTS', false ); 113 | location ~ \/wp-admin\/load-(scripts|styles).php { 114 | deny all; 115 | } 116 | # Protect Easy Digital Download files from being accessed directly. 117 | location ~ ^/wp-content/uploads/edd/(.*?)\.zip$ { 118 | rewrite / permanent; 119 | } 120 | -------------------------------------------------------------------------------- /wo/cli/plugins/sitedb.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import (Boolean, Column, DateTime, ForeignKey, Integer, String, 2 | func) 3 | from sqlalchemy.ext.declarative import declarative_base 4 | from sqlalchemy.orm import backref, relationship 5 | 6 | from wo.cli.plugins.models import SiteDB 7 | from wo.core.database import db_session 8 | from wo.core.logging import Log 9 | 10 | 11 | def addNewSite(self, site, stype, cache, path, 12 | enabled=True, ssl=False, fs='ext4', db='mysql', 13 | db_name=None, db_user=None, db_password=None, 14 | db_host='localhost', hhvm=0, php_version='8.1'): 15 | """ 16 | Add New Site record information into the wo database. 17 | """ 18 | try: 19 | newRec = SiteDB(site, stype, cache, path, enabled, ssl, fs, db, 20 | db_name, db_user, db_password, db_host, hhvm, 21 | php_version) 22 | db_session.add(newRec) 23 | db_session.commit() 24 | except Exception as e: 25 | Log.debug(self, "{0}".format(e)) 26 | Log.error(self, "Unable to add site to database") 27 | 28 | 29 | def getSiteInfo(self, site): 30 | """ 31 | Retrieves site record from ee databse 32 | """ 33 | try: 34 | q = SiteDB.query.filter(SiteDB.sitename == site).first() 35 | return q 36 | except Exception as e: 37 | Log.debug(self, "{0}".format(e)) 38 | Log.error(self, "Unable to query database for site info") 39 | 40 | 41 | def updateSiteInfo(self, site, stype='', cache='', webroot='', 42 | enabled=True, ssl=False, fs='', db='', db_name=None, 43 | db_user=None, db_password=None, db_host=None, hhvm=None, 44 | php_version=''): 45 | """updates site record in database""" 46 | try: 47 | q = SiteDB.query.filter(SiteDB.sitename == site).first() 48 | except Exception as e: 49 | Log.debug(self, "{0}".format(e)) 50 | Log.error(self, "Unable to query database for site info") 51 | 52 | if not q: 53 | Log.error(self, "{0} does not exist in database".format(site)) 54 | 55 | # Check if new record matches old if not then only update database 56 | if stype and q.site_type != stype: 57 | q.site_type = stype 58 | 59 | if cache and q.cache_type != cache: 60 | q.cache_type = cache 61 | 62 | if q.is_enabled != enabled: 63 | q.is_enabled = enabled 64 | 65 | if q.is_ssl != ssl: 66 | q.is_ssl = ssl 67 | 68 | if db_name and q.db_name != db_name: 69 | q.db_name = db_name 70 | 71 | if db_user and q.db_user != db_user: 72 | q.db_user = db_user 73 | 74 | if db_user and q.db_password != db_password: 75 | q.db_password = db_password 76 | 77 | if db_host and q.db_host != db_host: 78 | q.db_host = db_host 79 | 80 | if webroot and q.site_path != webroot: 81 | q.site_path = webroot 82 | 83 | if (hhvm is not None) and (q.is_hhvm is not hhvm): 84 | q.is_hhvm = hhvm 85 | 86 | if php_version and q.php_version != php_version: 87 | q.php_version = php_version 88 | 89 | try: 90 | q.created_on = func.now() 91 | db_session.commit() 92 | except Exception as e: 93 | Log.debug(self, "{0}".format(e)) 94 | Log.error(self, "Unable to update site info in application database.") 95 | 96 | 97 | def deleteSiteInfo(self, site): 98 | """Delete site record in database""" 99 | try: 100 | q = SiteDB.query.filter(SiteDB.sitename == site).first() 101 | except Exception as e: 102 | Log.debug(self, "{0}".format(e)) 103 | Log.error(self, "Unable to query database") 104 | 105 | if not q: 106 | Log.error(self, "{0} does not exist in database".format(site)) 107 | 108 | try: 109 | db_session.delete(q) 110 | db_session.commit() 111 | except Exception as e: 112 | Log.debug(self, "{0}".format(e)) 113 | Log.error(self, "Unable to delete site from application database.") 114 | 115 | 116 | def getAllsites(self): 117 | """ 118 | 1. returns all records from ee database 119 | """ 120 | try: 121 | q = SiteDB.query.all() 122 | return q 123 | except Exception as e: 124 | Log.debug(self, "{0}".format(e)) 125 | Log.error(self, "Unable to query database") 126 | -------------------------------------------------------------------------------- /wo/cli/templates/proftpd.mustache: -------------------------------------------------------------------------------- 1 | # 2 | # /etc/proftpd/proftpd.conf -- This is a basic ProFTPD configuration file. 3 | # To really apply changes, reload proftpd after modifications, if 4 | # it runs in daemon mode. It is not required in inetd/xinetd mode. 5 | # 6 | 7 | # Includes DSO modules 8 | Include /etc/proftpd/modules.conf 9 | 10 | # Set off to disable IPv6 support which is annoying on IPv4 only boxes. 11 | UseIPv6 off 12 | # If set on you can experience a longer connection delay in many cases. 13 | 14 | IdentLookups off 15 | 16 | ServerName "Debian" 17 | # Set to inetd only if you would run proftpd by inetd/xinetd. 18 | # Read README.Debian for more information on proper configuration. 19 | ServerType standalone 20 | DeferWelcome off 21 | 22 | MultilineRFC2228 on 23 | DefaultServer on 24 | ShowSymlinks on 25 | 26 | TimeoutNoTransfer 600 27 | TimeoutStalled 600 28 | TimeoutIdle 1200 29 | 30 | DisplayLogin welcome.msg 31 | DisplayChdir .message true 32 | ListOptions "-l" 33 | 34 | DenyFilter \*.*/ 35 | 36 | # Use this to jail all users in their homes 37 | DefaultRoot ~ 38 | 39 | # Users require a valid shell listed in /etc/shells to login. 40 | # Use this directive to release that constrain. 41 | RequireValidShell off 42 | 43 | # Port 21 is the standard FTP port. 44 | Port 21 45 | 46 | # In some cases you have to specify passive ports range to by-pass 47 | # firewall limitations. Ephemeral ports can be used for that, but 48 | # feel free to use a more narrow range. 49 | PassivePorts 49000 50000 50 | 51 | # If your host was NATted, this option is useful in order to 52 | # allow passive tranfers to work. You have to use your public 53 | # address and opening the passive ports used on your firewall as well. 54 | # MasqueradeAddress 1.2.3.4 55 | 56 | # This is useful for masquerading address with dynamic IPs: 57 | # refresh any configured MasqueradeAddress directives every 8 hours 58 | 59 | # DynMasqRefresh 28800 60 | 61 | 62 | # To prevent DoS attacks, set the maximum number of child processes 63 | # to 30. If you need to allow more than 30 concurrent connections 64 | # at once, simply increase this value. Note that this ONLY works 65 | # in standalone mode, in inetd mode you should use an inetd server 66 | # that allows you to limit maximum number of processes per service 67 | # (such as xinetd) 68 | MaxInstances 30 69 | 70 | # Set the user and group that the server normally runs at. 71 | User proftpd 72 | Group nogroup 73 | 74 | # Umask 022 is a good standard umask to prevent new files and dirs 75 | # (second parm) from being group and world writable. 76 | Umask 002 002 77 | # Normally, we want files to be overwriteable. 78 | AllowOverwrite on 79 | 80 | # Uncomment this if you are using NIS or LDAP via NSS to retrieve passwords: 81 | # PersistentPasswd off 82 | 83 | # This is required to use both PAM-based authentication and local passwords 84 | # AuthOrder mod_auth_pam.c* mod_auth_unix.c 85 | 86 | # Be warned: use of this directive impacts CPU average load! 87 | # Uncomment this if you like to see progress and transfer rate with ftpwho 88 | # in downloads. That is not needed for uploads rates. 89 | # 90 | UseSendFile off 91 | 92 | TransferLog /var/log/proftpd/xferlog 93 | SystemLog /var/log/proftpd/proftpd.log 94 | 95 | # Logging onto /var/log/lastlog is enabled but set to off by default 96 | #UseLastlog on 97 | 98 | # In order to keep log file dates consistent after chroot, use timezone info 99 | # from /etc/localtime. If this is not set, and proftpd is configured to 100 | # chroot (e.g. DefaultRoot or ), it will use the non-daylight 101 | # savings timezone regardless of whether DST is in effect. 102 | SetEnv TZ :/etc/localtime 103 | 104 | 105 | QuotaEngine off 106 | 107 | 108 | 109 | Ratios off 110 | 111 | 112 | 113 | # Delay engine reduces impact of the so-called Timing Attack described in 114 | # http://www.securityfocus.com/bid/11430/discuss 115 | # It is on by default. 116 | 117 | DelayEngine on 118 | 119 | 120 | 121 | ControlsEngine off 122 | ControlsMaxClients 2 123 | ControlsLog /var/log/proftpd/controls.log 124 | ControlsInterval 5 125 | ControlsSocket /var/run/proftpd/proftpd.sock 126 | 127 | 128 | 129 | AdminControlsEngine off 130 | 131 | 132 | 133 | # This is used for FTPS connections 134 | # 135 | Include /etc/proftpd/tls.conf 136 | 137 | # Include other custom configuration files 138 | Include /etc/proftpd/conf.d/ 139 | -------------------------------------------------------------------------------- /wo/cli/templates/upstream.mustache: -------------------------------------------------------------------------------- 1 | # NGINX UPSTREAM CONFIGURATION - WordOps {{release}} 2 | # DO NOT MODIFY, ALL CHANGES WILL BE LOST AFTER AN WordOps (wo) UPDATE 3 | #------------------------------- 4 | # PHP 5.6 5 | #------------------------------- 6 | upstream php { 7 | server 127.0.0.1:{{php}}; 8 | } 9 | 10 | upstream debug { 11 | server 127.0.0.1:{{debug}}; 12 | } 13 | 14 | 15 | #------------------------------- 16 | # PHP 7.0 17 | #------------------------------- 18 | 19 | upstream php7 { 20 | server 127.0.0.1:{{php7}}; 21 | } 22 | upstream debug7 { 23 | # Debug Pool 24 | server 127.0.0.1:{{debug7}}; 25 | } 26 | 27 | 28 | #------------------------------- 29 | # PHP 7.2 30 | #------------------------------- 31 | 32 | # PHP 7.2 upstream with load-balancing on two unix sockets 33 | upstream php72 { 34 | least_conn; 35 | 36 | server unix:/var/run/php/php72-fpm.sock; 37 | server unix:/var/run/php/php72-two-fpm.sock; 38 | 39 | keepalive 5; 40 | } 41 | 42 | # PHP 7.2 debug 43 | upstream debug72 { 44 | # Debug Pool 45 | server 127.0.0.1:9172; 46 | } 47 | 48 | #------------------------------- 49 | # PHP 7.3 50 | #------------------------------- 51 | 52 | # PHP 7.3 upstream with load-balancing on two unix sockets 53 | upstream php73 { 54 | least_conn; 55 | 56 | server unix:/var/run/php/php73-fpm.sock; 57 | server unix:/var/run/php/php73-two-fpm.sock; 58 | 59 | keepalive 5; 60 | } 61 | 62 | # PHP 7.3 debug 63 | upstream debug73 { 64 | # Debug Pool 65 | server 127.0.0.1:9173; 66 | } 67 | 68 | #------------------------------- 69 | # PHP 7.4 70 | #------------------------------- 71 | 72 | # PHP 7.4 upstream with load-balancing on two unix sockets 73 | upstream php74 { 74 | least_conn; 75 | 76 | server unix:/var/run/php/php74-fpm.sock; 77 | server unix:/var/run/php/php74-two-fpm.sock; 78 | 79 | keepalive 5; 80 | } 81 | 82 | # PHP 7.4 debug 83 | upstream debug74 { 84 | # Debug Pool 85 | server 127.0.0.1:9174; 86 | } 87 | 88 | #------------------------------- 89 | # PHP 8.0 90 | #------------------------------- 91 | 92 | # PHP 8.0 upstream with load-balancing on two unix sockets 93 | upstream php80 { 94 | least_conn; 95 | 96 | server unix:/var/run/php/php80-fpm.sock; 97 | server unix:/var/run/php/php80-two-fpm.sock; 98 | 99 | keepalive 5; 100 | } 101 | 102 | # PHP 8.0 debug 103 | upstream debug80 { 104 | # Debug Pool 105 | server 127.0.0.1:9175; 106 | } 107 | 108 | #------------------------------- 109 | # PHP 8.1 110 | #------------------------------- 111 | 112 | # PHP 8.1 upstream with load-balancing on two unix sockets 113 | upstream php81 { 114 | least_conn; 115 | 116 | server unix:/var/run/php/php81-fpm.sock; 117 | server unix:/var/run/php/php81-two-fpm.sock; 118 | 119 | keepalive 5; 120 | } 121 | 122 | # PHP 8.1 debug 123 | upstream debug81 { 124 | # Debug Pool 125 | server 127.0.0.1:9176; 126 | } 127 | 128 | #------------------------------- 129 | # PHP 8.2 130 | #------------------------------- 131 | 132 | # PHP 8.2 upstream with load-balancing on two unix sockets 133 | upstream php82 { 134 | least_conn; 135 | 136 | server unix:/var/run/php/php82-fpm.sock; 137 | server unix:/var/run/php/php82-two-fpm.sock; 138 | 139 | keepalive 5; 140 | } 141 | 142 | # PHP 8.2 debug 143 | upstream debug82 { 144 | # Debug Pool 145 | server 127.0.0.1:9177; 146 | } 147 | 148 | #------------------------------- 149 | # PHP 8.3 150 | #------------------------------- 151 | 152 | # PHP 8.3 upstream with load-balancing on two unix sockets 153 | upstream php83 { 154 | least_conn; 155 | 156 | server unix:/var/run/php/php83-fpm.sock; 157 | server unix:/var/run/php/php83-two-fpm.sock; 158 | 159 | keepalive 5; 160 | } 161 | 162 | # PHP 8.3 debug 163 | upstream debug83 { 164 | # Debug Pool 165 | server 127.0.0.1:9178; 166 | } 167 | 168 | #------------------------------- 169 | # Netdata 170 | #------------------------------- 171 | 172 | # Netdata Monitoring Upstream 173 | upstream netdata { 174 | server 127.0.0.1:19999; 175 | keepalive 64; 176 | } 177 | 178 | #------------------------------- 179 | # Redis 180 | #------------------------------- 181 | 182 | # Redis cache upstream 183 | upstream redis { 184 | server 127.0.0.1:6379; 185 | keepalive 10; 186 | } 187 | 188 | #------------------------------- 189 | # Multi PHP 190 | #------------------------------- 191 | 192 | # Multi PHP upstream for WordOps backend 193 | upstream multiphp { 194 | server unix:/var/run/php/php73-fpm.sock; 195 | server unix:/var/run/php/php74-fpm.sock; 196 | server unix:/var/run/php/php72-fpm.sock; 197 | server unix:/var/run/php/php80-fpm.sock; 198 | server unix:/var/run/php/php81-fpm.sock; 199 | server unix:/var/run/php/php82-fpm.sock; 200 | server unix:/var/run/php/php83-fpm.sock; 201 | } 202 | -------------------------------------------------------------------------------- /wo/cli/plugins/update.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | 4 | 5 | from cement.core.controller import CementBaseController, expose 6 | from wo.core.download import WODownload 7 | from wo.core.logging import Log 8 | from wo.core.variables import WOVar 9 | 10 | 11 | def wo_update_hook(app): 12 | pass 13 | 14 | 15 | class WOUpdateController(CementBaseController): 16 | class Meta: 17 | label = 'wo_update' 18 | stacked_on = 'base' 19 | aliases = ['update'] 20 | aliases_only = True 21 | stacked_type = 'nested' 22 | description = ('update WordOps to latest version') 23 | arguments = [ 24 | (['--force'], 25 | dict(help='Force WordOps update', action='store_true')), 26 | (['--beta'], 27 | dict(help='Update WordOps to latest mainline release ' 28 | '(same than --mainline)', 29 | action='store_true')), 30 | (['--mainline'], 31 | dict(help='Update WordOps to latest mainline release', 32 | action='store_true')), 33 | (['--branch'], 34 | dict(help="Update WordOps from a specific repository branch ", 35 | action='store' or 'store_const', 36 | const='develop', nargs='?')), 37 | (['--travis'], 38 | dict(help='Argument used only for WordOps development', 39 | action='store_true')), 40 | ] 41 | usage = "wo update [options]" 42 | 43 | @expose(hide=True) 44 | def default(self): 45 | pargs = self.app.pargs 46 | filename = "woupdate" + time.strftime("%Y%m%d-%H%M%S") 47 | 48 | install_args = "" 49 | wo_branch = "master" 50 | if pargs.mainline or pargs.beta: 51 | wo_branch = "mainline" 52 | install_args = install_args + "--mainline " 53 | elif pargs.branch: 54 | wo_branch = pargs.branch 55 | install_args = install_args + "-b {0} ".format(wo_branch) 56 | if pargs.force: 57 | install_args = install_args + "--force " 58 | if pargs.travis: 59 | install_args = install_args + "--travis " 60 | wo_branch = "updating-configuration" 61 | 62 | # check if WordOps already up-to-date 63 | if ((not pargs.force) and (not pargs.travis) and 64 | (not pargs.mainline) and (not pargs.beta) and 65 | (not pargs.branch)): 66 | wo_current = ("v{0}".format(WOVar.wo_version)) 67 | wo_latest = WODownload.latest_release(self, "WordOps/WordOps") 68 | if wo_current == wo_latest: 69 | Log.info( 70 | self, "WordOps {0} is already installed" 71 | .format(wo_latest)) 72 | self.app.close(0) 73 | 74 | # prompt user before starting upgrade 75 | if not pargs.force: 76 | Log.info( 77 | self, "WordOps changelog available on " 78 | "https://github.com/WordOps/WordOps/releases/tag/{0}" 79 | .format(wo_latest)) 80 | start_upgrade = input("Do you want to continue:[y/N]") 81 | if start_upgrade not in ("Y", "y"): 82 | Log.error(self, "Not starting WordOps update") 83 | 84 | # download the install/update script 85 | if not os.path.isdir('/var/lib/wo/tmp'): 86 | os.makedirs('/var/lib/wo/tmp') 87 | WODownload.download(self, [["https://raw.githubusercontent.com/" 88 | "WordOps/WordOps/{0}/install" 89 | .format(wo_branch), 90 | "/var/lib/wo/tmp/{0}".format(filename), 91 | "update script"]]) 92 | 93 | # launch install script 94 | if os.path.isfile('install'): 95 | Log.info(self, "updating WordOps from local install\n") 96 | try: 97 | Log.info(self, "updating WordOps, please wait...") 98 | os.system("/bin/bash install --travis") 99 | except OSError as e: 100 | Log.debug(self, str(e)) 101 | Log.error(self, "WordOps update failed !") 102 | else: 103 | try: 104 | Log.info(self, "updating WordOps, please wait...") 105 | os.system("/bin/bash /var/lib/wo/tmp/{0} " 106 | "{1}".format(filename, install_args)) 107 | except OSError as e: 108 | Log.debug(self, str(e)) 109 | Log.error(self, "WordOps update failed !") 110 | 111 | os.remove("/var/lib/wo/tmp/{0}".format(filename)) 112 | 113 | 114 | def load(app): 115 | # register the plugin class.. this only happens if the plugin is enabled 116 | app.handler.register(WOUpdateController) 117 | # register a hook (function) to run after arguments are parsed. 118 | app.hook.register('post_argument_parsing', wo_update_hook) 119 | -------------------------------------------------------------------------------- /wo/core/apt_repo.py: -------------------------------------------------------------------------------- 1 | """WordOps packages repository operations""" 2 | import os 3 | 4 | from wo.core.logging import Log 5 | from wo.core.shellexec import WOShellExec 6 | from wo.core.variables import WOVar 7 | 8 | 9 | class WORepo(): 10 | """Manage Repositories""" 11 | 12 | def __init__(self): 13 | """Initialize """ 14 | pass 15 | 16 | def add(self, repo_url=None, ppa=None): 17 | """ 18 | This function used to add apt repositories and or ppa's 19 | If repo_url is provided adds repo file to 20 | /etc/apt/sources.list.d/ 21 | If ppa is provided add apt-repository using 22 | add-apt-repository 23 | command. 24 | """ 25 | 26 | if repo_url is not None: 27 | repo_file_path = ("/etc/apt/sources.list.d/" + 28 | WOVar().wo_repo_file) 29 | try: 30 | if not os.path.isfile(repo_file_path): 31 | with open(repo_file_path, 32 | encoding='utf-8', mode='a') as repofile: 33 | repofile.write(repo_url) 34 | repofile.write('\n') 35 | repofile.close() 36 | elif repo_url not in open(repo_file_path, 37 | encoding='utf-8').read(): 38 | with open(repo_file_path, 39 | encoding='utf-8', mode='a') as repofile: 40 | repofile.write(repo_url) 41 | repofile.write('\n') 42 | repofile.close() 43 | return True 44 | except IOError as e: 45 | Log.debug(self, "{0}".format(e)) 46 | Log.error(self, "File I/O error.") 47 | except Exception as e: 48 | Log.debug(self, "{0}".format(e)) 49 | Log.error(self, "Unable to add repo") 50 | if ppa is not None: 51 | ppa_split = ppa.split(':')[1] 52 | ppa_author = ppa_split.split('/')[0] 53 | Log.debug(self, "ppa_author = {0}".format(ppa_author)) 54 | ppa_package = ppa_split.split('/')[1] 55 | Log.debug(self, "ppa_package = {0}".format(ppa_package)) 56 | if os.path.exists( 57 | '/etc/apt/sources.list.d/{0}-ubuntu-{1}-{2}.list' 58 | .format(ppa_author, 59 | ppa_package, WOVar.wo_platform_codename)): 60 | Log.debug(self, "ppa already added") 61 | return True 62 | if WOShellExec.cmd_exec( 63 | self, "LC_ALL=C.UTF-8 add-apt-repository -y '{ppa_name}'" 64 | .format(ppa_name=ppa)): 65 | Log.debug(self, "Added PPA {0}".format(ppa)) 66 | return True 67 | return False 68 | 69 | def remove(self, ppa=None, repo_url=None): 70 | """ 71 | This function used to remove ppa's 72 | If ppa is provided adds repo file to 73 | /etc/apt/sources.list.d/ 74 | command. 75 | """ 76 | if ppa: 77 | WOShellExec.cmd_exec(self, "add-apt-repository -y " 78 | "--remove '{ppa_name}'" 79 | .format(ppa_name=ppa)) 80 | elif repo_url: 81 | repo_file_path = ("/etc/apt/sources.list.d/" + 82 | WOVar().wo_repo_file) 83 | 84 | try: 85 | repofile = open(repo_file_path, "w+", encoding='utf-8') 86 | repofile.write(repofile.read().replace(repo_url, "")) 87 | repofile.close() 88 | except IOError as e: 89 | Log.debug(self, "{0}".format(e)) 90 | Log.error(self, "File I/O error.") 91 | except Exception as e: 92 | Log.debug(self, "{0}".format(e)) 93 | Log.error(self, "Unable to remove repo") 94 | 95 | def add_key(self, keyid, keyserver=None): 96 | """ 97 | This function adds imports repository keys from keyserver. 98 | default keyserver is hkp://keyserver.ubuntu.com 99 | user can provide other keyserver with keyserver="hkp://xyz" 100 | """ 101 | try: 102 | WOShellExec.cmd_exec( 103 | self, "apt-key adv --keyserver {serv}" 104 | .format(serv=(keyserver or 105 | "hkp://keyserver.ubuntu.com")) + 106 | " --recv-keys {key}".format(key=keyid)) 107 | except Exception as e: 108 | Log.debug(self, "{0}".format(e)) 109 | Log.error(self, "Unable to import repo key") 110 | 111 | def download_key(self, key_url): 112 | """ 113 | This function download gpg keys and add import them with apt-key add" 114 | """ 115 | try: 116 | WOShellExec.cmd_exec( 117 | self, "curl -sL {0} ".format(key_url) + 118 | "| apt-key add -") 119 | except Exception as e: 120 | Log.debug(self, "{0}".format(e)) 121 | Log.error(self, "Unable to import repo keys") 122 | -------------------------------------------------------------------------------- /wo/cli/plugins/sync.py: -------------------------------------------------------------------------------- 1 | import glob 2 | import os 3 | 4 | from cement.core.controller import CementBaseController, expose 5 | 6 | from wo.cli.plugins.sitedb import getAllsites, updateSiteInfo 7 | from wo.core.fileutils import WOFileUtils 8 | from wo.core.logging import Log 9 | from wo.core.mysql import StatementExcecutionError, WOMysql 10 | 11 | 12 | def wo_sync_hook(app): 13 | pass 14 | 15 | 16 | class WOSyncController(CementBaseController): 17 | class Meta: 18 | label = 'sync' 19 | stacked_on = 'base' 20 | stacked_type = 'nested' 21 | description = 'synchronize the WordOps database' 22 | 23 | @expose(hide=True) 24 | def default(self): 25 | self.sync() 26 | 27 | @expose(hide=True) 28 | def sync(self): 29 | """ 30 | 1. reads database information from wp/wo-config.php 31 | 2. updates records into wo database accordingly. 32 | """ 33 | Log.info(self, "Synchronizing wo database, please wait...") 34 | sites = getAllsites(self) 35 | if not sites: 36 | pass 37 | for site in sites: 38 | if site.site_type in ['mysql', 'wp', 'wpsubdir', 'wpsubdomain']: 39 | wo_site_webroot = site.site_path 40 | # Read config files 41 | configfiles = glob.glob(wo_site_webroot + '/*-config.php') 42 | 43 | if (os.path.exists( 44 | '{0}/ee-config.php'.format(wo_site_webroot)) and 45 | os.path.exists( 46 | '{0}/wo-config.php'.format(wo_site_webroot))): 47 | configfiles = glob.glob( 48 | wo_site_webroot + 'wo-config.php') 49 | 50 | # search for wp-config.php inside htdocs/ 51 | if not configfiles: 52 | Log.debug(self, "Config files not found in {0}/ " 53 | .format(wo_site_webroot)) 54 | if site.site_type != 'mysql': 55 | Log.debug(self, 56 | "Searching wp-config.php in {0}/htdocs/" 57 | .format(wo_site_webroot)) 58 | configfiles = glob.glob( 59 | wo_site_webroot + '/htdocs/wp-config.php') 60 | 61 | if configfiles: 62 | if WOFileUtils.isexist(self, configfiles[0]): 63 | wo_db_name = ( 64 | WOFileUtils.grep(self, configfiles[0], 65 | 'DB_NAME').split(',')[1] 66 | .split(')')[0].strip().replace('\'', '')) 67 | wo_db_user = ( 68 | WOFileUtils.grep(self, configfiles[0], 69 | 'DB_USER').split(',')[1] 70 | .split(')')[0].strip().replace('\'', '')) 71 | wo_db_pass = ( 72 | WOFileUtils.grep(self, configfiles[0], 73 | 'DB_PASSWORD').split(',')[1] 74 | .split(')')[0].strip().replace('\'', '')) 75 | wo_db_host = ( 76 | WOFileUtils.grep(self, configfiles[0], 77 | 'DB_HOST').split(',')[1] 78 | .split(')')[0].strip().replace('\'', '')) 79 | 80 | # Check if database really exist 81 | try: 82 | if not WOMysql.check_db_exists(self, wo_db_name): 83 | # Mark it as deleted if not exist 84 | wo_db_name = 'deleted' 85 | wo_db_user = 'deleted' 86 | wo_db_pass = 'deleted' 87 | except StatementExcecutionError as e: 88 | Log.debug(self, str(e)) 89 | except Exception as e: 90 | Log.debug(self, str(e)) 91 | 92 | if site.db_name != wo_db_name: 93 | # update records if any mismatch found 94 | Log.debug(self, "Updating wo db record for {0}" 95 | .format(site.sitename)) 96 | updateSiteInfo(self, site.sitename, 97 | db_name=wo_db_name, 98 | db_user=wo_db_user, 99 | db_password=wo_db_pass, 100 | db_host=wo_db_host) 101 | else: 102 | Log.debug(self, "Config files not found for {0} " 103 | .format(site.sitename)) 104 | 105 | 106 | def load(app): 107 | # register the plugin class.. this only happens if the plugin is enabled 108 | app.handler.register(WOSyncController) 109 | # register a hook (function) to run after arguments are parsed. 110 | app.hook.register('post_argument_parsing', wo_sync_hook) 111 | -------------------------------------------------------------------------------- /wo/cli/templates/mime.mustache: -------------------------------------------------------------------------------- 1 | 2 | types { 3 | text/html html htm shtml; 4 | text/css css; 5 | text/xml xml; 6 | image/gif gif; 7 | image/jpeg jpeg jpg; 8 | application/javascript js; 9 | application/atom+xml atom; 10 | application/rss+xml rss; 11 | 12 | text/mathml mml; 13 | text/plain txt; 14 | text/vnd.sun.j2me.app-descriptor jad; 15 | text/vnd.wap.wml wml; 16 | text/x-component htc; 17 | 18 | image/png png; 19 | image/svg+xml svg svgz; 20 | image/tiff tif tiff; 21 | image/vnd.wap.wbmp wbmp; 22 | image/webp webp; 23 | image/x-icon ico; 24 | image/x-jng jng; 25 | image/x-ms-bmp bmp; 26 | 27 | font/woff woff; 28 | font/woff2 woff2; 29 | font/ttf ttf; 30 | 31 | application/java-archive jar war ear; 32 | application/json json; 33 | application/mac-binhex40 hqx; 34 | application/msword doc; 35 | application/pdf pdf; 36 | application/postscript ps eps ai; 37 | application/rtf rtf; 38 | application/vnd.apple.mpegurl m3u8; 39 | application/vnd.google-earth.kml+xml kml; 40 | application/vnd.google-earth.kmz kmz; 41 | application/vnd.ms-excel xls; 42 | application/vnd.ms-fontobject eot; 43 | application/vnd.ms-powerpoint ppt; 44 | application/vnd.oasis.opendocument.graphics odg; 45 | application/vnd.oasis.opendocument.presentation odp; 46 | application/vnd.oasis.opendocument.spreadsheet ods; 47 | application/vnd.oasis.opendocument.text odt; 48 | application/vnd.openxmlformats-officedocument.presentationml.presentation 49 | pptx; 50 | application/vnd.openxmlformats-officedocument.spreadsheetml.sheet 51 | xlsx; 52 | application/vnd.openxmlformats-officedocument.wordprocessingml.document 53 | docx; 54 | application/vnd.wap.wmlc wmlc; 55 | application/x-7z-compressed 7z; 56 | application/x-cocoa cco; 57 | application/x-java-archive-diff jardiff; 58 | application/x-java-jnlp-file jnlp; 59 | application/x-makeself run; 60 | application/x-perl pl pm; 61 | application/x-pilot prc pdb; 62 | application/x-rar-compressed rar; 63 | application/x-redhat-package-manager rpm; 64 | application/x-sea sea; 65 | application/x-shockwave-flash swf; 66 | application/x-stuffit sit; 67 | application/x-tcl tcl tk; 68 | application/x-x509-ca-cert der pem crt; 69 | application/x-xpinstall xpi; 70 | application/xhtml+xml xhtml; 71 | application/xspf+xml xspf; 72 | application/zip zip; 73 | 74 | application/octet-stream bin exe dll; 75 | application/octet-stream deb; 76 | application/octet-stream dmg; 77 | application/octet-stream iso img; 78 | application/octet-stream msi msp msm; 79 | 80 | audio/midi mid midi kar; 81 | audio/mpeg mp3; 82 | audio/ogg ogg; 83 | audio/x-m4a m4a; 84 | audio/x-realaudio ra; 85 | 86 | video/3gpp 3gpp 3gp; 87 | video/mp2t ts; 88 | video/mp4 mp4; 89 | video/mpeg mpeg mpg; 90 | video/quicktime mov; 91 | video/webm webm; 92 | video/x-flv flv; 93 | video/x-m4v m4v; 94 | video/x-mng mng; 95 | video/x-ms-asf asx asf; 96 | video/x-ms-wmv wmv; 97 | video/x-msvideo avi; 98 | } 99 | -------------------------------------------------------------------------------- /wo/cli/plugins/stack_migrate.py: -------------------------------------------------------------------------------- 1 | from cement.core.controller import CementBaseController, expose 2 | 3 | from wo.cli.plugins.stack_pref import post_pref, pre_pref 4 | from wo.core.aptget import WOAptGet 5 | from wo.core.fileutils import WOFileUtils 6 | from wo.core.logging import Log 7 | from wo.core.mysql import WOMysql 8 | from wo.core.shellexec import WOShellExec 9 | from wo.core.variables import WOVar 10 | from wo.core.apt_repo import WORepo 11 | 12 | 13 | class WOStackMigrateController(CementBaseController): 14 | class Meta: 15 | label = 'migrate' 16 | stacked_on = 'stack' 17 | stacked_type = 'nested' 18 | description = ('Migrate stack safely') 19 | arguments = [ 20 | (['--mariadb'], 21 | dict(help="Migrate/Upgrade database to MariaDB", 22 | action='store_true')), 23 | (['--force'], 24 | dict(help="Force Packages upgrade without any prompt", 25 | action='store_true')), 26 | (['--ci'], 27 | dict(help="Argument used for testing, " 28 | "do not use it on your server", 29 | action='store_true')), 30 | ] 31 | 32 | @expose(hide=True) 33 | def migrate_mariadb(self, ci=False): 34 | 35 | if WOShellExec.cmd_exec(self, 'mysqladmin ping'): 36 | # Backup all database 37 | WOMysql.backupAll(self, fulldump=True) 38 | else: 39 | Log.error(self, "Unable to connect to MariaDB") 40 | 41 | # Check current MariaDB version 42 | wo_mysql_current_repo = WOFileUtils.grep( 43 | self, '/etc/apt/sources.list.d/wo-repo.list', 'mariadb') 44 | if wo_mysql_current_repo: 45 | current_mysql_version = wo_mysql_current_repo.split('/') 46 | else: 47 | Log.error(self, "MariaDB is not installed from repository yet") 48 | if 'repo' in current_mysql_version: 49 | current_mysql_version = current_mysql_version[5] 50 | 51 | if self.app.config.has_section('mariadb'): 52 | mariadb_release = self.app.config.get( 53 | 'mariadb', 'release') 54 | if mariadb_release < WOVar.mariadb_ver: 55 | mariadb_release = WOVar.mariadb_ver 56 | else: 57 | mariadb_release = WOVar.mariadb_ver 58 | if mariadb_release == current_mysql_version: 59 | Log.info(self, "You already have the latest " 60 | "MariaDB version available") 61 | return 0 62 | 63 | wo_old_mysql_repo = ("deb [arch=amd64,arm64,ppc64el] " 64 | "http://mariadb.mirrors.ovh.net/MariaDB/repo/" 65 | "{version}/{distro} {codename} main" 66 | .format(version=current_mysql_version, 67 | distro=WOVar.wo_distro, 68 | codename=WOVar.wo_platform_codename)) 69 | 70 | if WOFileUtils.grepcheck( 71 | self, '/etc/apt/sources.list.d/wo-repo.list', 72 | wo_old_mysql_repo): 73 | WORepo.remove(self, repo_url=wo_old_mysql_repo) 74 | # Add MariaDB repo 75 | pre_pref(self, WOVar.wo_mysql) 76 | 77 | # Install MariaDB 78 | 79 | Log.wait(self, "Updating apt-cache ") 80 | WOAptGet.update(self) 81 | Log.valide(self, "Updating apt-cache ") 82 | Log.wait(self, "Upgrading MariaDB ") 83 | WOAptGet.remove(self, ["mariadb-server"]) 84 | WOAptGet.auto_remove(self) 85 | WOAptGet.install(self, WOVar.wo_mysql) 86 | if not ci: 87 | WOAptGet.dist_upgrade(self) 88 | WOAptGet.auto_remove(self) 89 | WOAptGet.auto_clean(self) 90 | Log.valide(self, "Upgrading MariaDB ") 91 | WOFileUtils.mvfile( 92 | self, '/etc/mysql/my.cnf', '/etc/mysql/my.cnf.old') 93 | WOFileUtils.create_symlink( 94 | self, ['/etc/mysql/mariadb.cnf', '/etc/mysql/my.cnf']) 95 | WOShellExec.cmd_exec(self, 'systemctl daemon-reload') 96 | WOShellExec.cmd_exec(self, 'systemctl enable mariadb') 97 | post_pref(self, WOVar.wo_mysql, []) 98 | 99 | @expose(hide=True) 100 | def default(self): 101 | pargs = self.app.pargs 102 | if not pargs.mariadb: 103 | self.app.args.print_help() 104 | if pargs.mariadb: 105 | if WOVar.wo_distro == 'raspbian': 106 | Log.error(self, "MariaDB upgrade is not available on Raspbian") 107 | if WOVar.wo_mysql_host != "localhost": 108 | Log.error( 109 | self, "Remote MySQL server in use, skipping local install") 110 | 111 | if WOShellExec.cmd_exec(self, "mysqladmin ping"): 112 | 113 | Log.info(self, "If your database size is big, " 114 | "migration may take some time.") 115 | Log.info(self, "During migration non nginx-cached parts of " 116 | "your site may remain down") 117 | if not pargs.force: 118 | start_upgrade = input("Do you want to continue:[y/N]") 119 | if start_upgrade != "Y" and start_upgrade != "y": 120 | Log.error(self, "Not starting package update") 121 | if not pargs.ci: 122 | self.migrate_mariadb() 123 | else: 124 | self.migrate_mariadb(ci=True) 125 | else: 126 | Log.error(self, "Your current MySQL is not alive or " 127 | "you allready installed MariaDB") 128 | -------------------------------------------------------------------------------- /wo/cli/templates/my.mustache: -------------------------------------------------------------------------------- 1 | # MariaDB database server configuration file. 2 | # Optimized by WordOps {{release}} 3 | # 4 | # You can copy this file to one of: 5 | # - "/etc/mysql/my.cnf" to set global options, 6 | # - "~/.my.cnf" to set user-specific options. 7 | # 8 | # One can use all long options that the program supports. 9 | # Run program with --help to get a list of available options and with 10 | # --print-defaults to see which it would actually understand and use. 11 | # 12 | # For explanations see 13 | # http://dev.mysql.com/doc/mysql/en/server-system-variables.html 14 | 15 | # This will be passed to all mysql clients 16 | # It has been reported that passwords should be enclosed with ticks/quotes 17 | # escpecially if they contain "#" chars... 18 | # Remember to edit /etc/mysql/debian.cnf when changing the socket location. 19 | [client] 20 | port = 3306 21 | socket = /var/run/mysqld/mysqld.sock 22 | 23 | # Here is entries for some specific programs 24 | # The following values assume you have at least 32M ram 25 | 26 | # This was formally known as [safe_mysqld]. Both versions are currently parsed. 27 | [mysqld_safe] 28 | socket = /var/run/mysqld/mysqld.sock 29 | nice = 0 30 | 31 | [mysqld] 32 | # 33 | # * Basic Settings 34 | # 35 | user = mysql 36 | pid-file = /var/run/mysqld/mysqld.pid 37 | socket = /var/run/mysqld/mysqld.sock 38 | port = 3306 39 | basedir = /usr 40 | datadir = /var/lib/mysql 41 | tmpdir = /tmp 42 | lc_messages_dir = /usr/share/mysql 43 | lc_messages = en_US 44 | skip-external-locking 45 | # 46 | # Instead of skip-networking the default is now to listen only on 47 | # localhost which is more compatible and is not less secure. 48 | bind-address = localhost 49 | # 50 | # * Fine Tuning 51 | # 52 | max_connections = 100 53 | connect_timeout = 5 54 | wait_timeout = 60 55 | max_allowed_packet = 64M 56 | thread_cache_size = 128 57 | sort_buffer_size = 4M 58 | bulk_insert_buffer_size = 16M 59 | tmp_table_size = {{tmp_table_size}}M 60 | max_heap_table_size = {{tmp_table_size}}M 61 | # 62 | # * MyISAM 63 | # 64 | # This replaces the startup script and checks MyISAM tables if needed 65 | # the first time they are touched. On error, make copy and try a repair. 66 | myisam_recover_options = BACKUP 67 | key_buffer_size = 16M 68 | open-files-limit = 500000 69 | table_open_cache = 16000 70 | myisam_sort_buffer_size = 128M 71 | concurrent_insert = 2 72 | read_buffer_size = 2M 73 | read_rnd_buffer_size = 1M 74 | # 75 | # * Query Cache Configuration 76 | # 77 | # Cache only tiny result sets, so we can fit more in the query cache. 78 | query_cache_limit = 128K 79 | query_cache_size = 0 80 | # for more write intensive setups, set to DEMAND or OFF 81 | query_cache_type = 0 82 | # 83 | # * Logging and Replication 84 | # 85 | # Both location gets rotated by the cronjob. 86 | # Be aware that this log type is a performance killer. 87 | # As of 5.1 you can enable the log at runtime! 88 | #general_log_file = /var/log/mysql/mysql.log 89 | #general_log = 1 90 | # 91 | # Error logging goes to syslog due to /etc/mysql/conf.d/mysqld_safe_syslog.cnf. 92 | # 93 | # we do want to know about network errors and such 94 | log_warnings = 2 95 | # 96 | # Enable the slow query log to see queries with especially long duration 97 | slow_query_log = 1 98 | slow_query_log_file = /var/log/mysql/mariadb-slow.log 99 | long_query_time = 10 100 | #log_slow_rate_limit = 1000 101 | log_slow_verbosity = query_plan 102 | 103 | #log-queries-not-using-indexes 104 | #log_slow_admin_statements 105 | # 106 | # The following can be used as easy to replay backup logs or for replication. 107 | # note: if you are setting up a replication slave, see README.Debian about 108 | # other settings you may need to change. 109 | #server-id = 1 110 | #report_host = master1 111 | #auto_increment_increment = 2 112 | #auto_increment_offset = 1 113 | #log_bin = /var/log/mysql/mariadb-bin 114 | #log_bin_index = /var/log/mysql/mariadb-bin.index 115 | # not fab for performance, but safer 116 | #sync_binlog = 1 117 | #expire_logs_days = 10 118 | #max_binlog_size = 100M 119 | transaction_isolation = READ-COMMITTED 120 | binlog_format = ROW 121 | 122 | # slaves 123 | #relay_log = /var/log/mysql/relay-bin 124 | #relay_log_index = /var/log/mysql/relay-bin.index 125 | #relay_log_info_file = /var/log/mysql/relay-bin.info 126 | #log_slave_updates 127 | #read_only 128 | # 129 | # If applications support it, this stricter sql_mode prevents some 130 | # mistakes like inserting invalid dates etc. 131 | #sql_mode = NO_ENGINE_SUBSTITUTION,TRADITIONAL 132 | # 133 | # * InnoDB 134 | # 135 | # InnoDB is enabled by default with a 10MB datafile in /var/lib/mysql/. 136 | # Read the manual for more InnoDB related options. There are many! 137 | default_storage_engine = InnoDB 138 | # you can't just change log file size, requires special procedure 139 | innodb_log_file_size = {{inno_log}}M 140 | innodb_buffer_pool_size = {{inno_buffer}}M 141 | innodb_log_buffer_size = {{inno_log_buffer}}M 142 | innodb_file_per_table = 1 143 | innodb_open_files = 500000 144 | innodb_io_capacity = 500000 145 | innodb_flush_method = O_DIRECT 146 | # 147 | # * Security Features 148 | # 149 | # Read the manual, too, if you want chroot! 150 | # chroot = /var/lib/mysql/ 151 | # 152 | # For generating SSL certificates I recommend the OpenSSL GUI "tinyca". 153 | # 154 | # ssl-ca=/etc/mysql/cacert.pem 155 | # ssl-cert=/etc/mysql/server-cert.pem 156 | # ssl-key=/etc/mysql/server-key.pem 157 | 158 | # 159 | # * Galera-related settings 160 | # 161 | [galera] 162 | # Mandatory settings 163 | #wsrep_on=ON 164 | #wsrep_provider= 165 | #wsrep_cluster_address= 166 | #binlog_format=row 167 | #default_storage_engine=InnoDB 168 | #innodb_autoinc_lock_mode=2 169 | # 170 | # Allow server to accept connections on all interfaces. 171 | # 172 | #bind-address=0.0.0.0 173 | # 174 | # Optional setting 175 | #wsrep_slave_threads=1 176 | #innodb_flush_log_at_trx_commit=0 177 | 178 | [mysqldump] 179 | quick 180 | quote-names 181 | max_allowed_packet = 64M 182 | 183 | [mysql] 184 | #no-auto-rehash # faster start of mysql but no tab completion 185 | 186 | [isamchk] 187 | key_buffer = 16M 188 | 189 | # 190 | # * IMPORTANT: Additional settings that can override those from this file! 191 | # The files must end with '.cnf', otherwise they'll be ignored. 192 | # 193 | {{#newmariadb}} 194 | !include /etc/mysql/mariadb.cnf 195 | !includedir /etc/mysql/conf.d/{{/newmariadb}} 196 | -------------------------------------------------------------------------------- /wo/core/mysql.py: -------------------------------------------------------------------------------- 1 | """WordOps MySQL core classes.""" 2 | import os 3 | from os.path import expanduser 4 | 5 | import pymysql 6 | from pymysql import DatabaseError, Error, connections 7 | 8 | from wo.core.logging import Log 9 | from wo.core.variables import WOVar 10 | 11 | 12 | class MySQLConnectionError(Exception): 13 | """Custom Exception when MySQL server Not Connected""" 14 | pass 15 | 16 | 17 | class StatementExcecutionError(Exception): 18 | """Custom Exception when any Query Fails to execute""" 19 | pass 20 | 21 | 22 | class DatabaseNotExistsError(Exception): 23 | """Custom Exception when Database not Exist""" 24 | pass 25 | 26 | 27 | class WOMysql(): 28 | """Method for MySQL connection""" 29 | 30 | def connect(self): 31 | # Makes connection with MySQL server 32 | try: 33 | if os.path.exists('/etc/mysql/conf.d/my.cnf'): 34 | connection = pymysql.connect( 35 | read_default_file='/etc/mysql/conf.d/my.cnf') 36 | else: 37 | connection = pymysql.connect(read_default_file='~/.my.cnf') 38 | return connection 39 | except ValueError as e: 40 | Log.debug(self, str(e)) 41 | raise MySQLConnectionError 42 | except pymysql.err.InternalError as e: 43 | Log.debug(self, str(e)) 44 | raise MySQLConnectionError 45 | 46 | def dbConnection(self, db_name): 47 | try: 48 | if os.path.exists('/etc/mysql/conf.d/my.cnf'): 49 | connection = pymysql.connect( 50 | db=db_name, read_default_file='/etc/mysql/conf.d/my.cnf') 51 | else: 52 | connection = pymysql.connect( 53 | db=db_name, read_default_file='~/.my.cnf') 54 | 55 | return connection 56 | except pymysql.err.InternalError as e: 57 | Log.debug(self, str(e)) 58 | raise MySQLConnectionError 59 | except DatabaseError as e: 60 | if e.args[1] == '#42000Unknown database \'{0}\''.format(db_name): 61 | raise DatabaseNotExistsError 62 | else: 63 | raise MySQLConnectionError 64 | except Exception as e: 65 | Log.debug(self, "[Error]Setting up database: \'" + str(e) + "\'") 66 | raise MySQLConnectionError 67 | 68 | def execute(self, statement, errormsg='', log=True): 69 | # Get login details from /etc/mysql/conf.d/my.cnf 70 | # & Execute MySQL query 71 | connection = WOMysql.connect(self) 72 | log and Log.debug(self, "Executing MySQL Statement : {0}" 73 | .format(statement)) 74 | try: 75 | cursor = connection.cursor() 76 | sql = statement 77 | cursor.execute(sql) 78 | 79 | # connection is not autocommit by default. 80 | # So you must commit to save your changes. 81 | connection.commit() 82 | except AttributeError as e: 83 | Log.debug(self, str(e)) 84 | raise StatementExcecutionError 85 | except Error as e: 86 | Log.debug(self, str(e)) 87 | raise StatementExcecutionError 88 | finally: 89 | connection.close() 90 | 91 | def backupAll(self, fulldump=False): 92 | import subprocess 93 | try: 94 | Log.info(self, "Backing up database at location: " 95 | "/var/lib/wo-backup/mysql") 96 | # Setup Nginx common directory 97 | if not os.path.exists('/var/lib/wo-backup/mysql'): 98 | Log.debug(self, 'Creating directory' 99 | '/var/lib/wo-backup/mysql') 100 | os.makedirs('/var/lib/wo-backup/mysql') 101 | if not fulldump: 102 | db = subprocess.check_output( 103 | ["/usr/bin/mysql " 104 | "-Bse \'show databases\'"], 105 | universal_newlines=True, 106 | shell=True).split('\n') 107 | for dbs in db: 108 | if dbs == "": 109 | continue 110 | Log.info(self, "Backing up {0} database".format(dbs)) 111 | p1 = subprocess.Popen( 112 | "/usr/bin/mysqldump {0} --max_allowed_packet=1024M " 113 | "--single-transaction --hex-blob".format(dbs), 114 | stdout=subprocess.PIPE, 115 | stderr=subprocess.PIPE, shell=True) 116 | p2 = subprocess.Popen( 117 | "/usr/bin/zstd -c > " 118 | "/var/lib/wo-backup/mysql/{0}{1}.sql.zst" 119 | .format(dbs, WOVar.wo_date), 120 | stdin=p1.stdout, shell=True) 121 | # Allow p1 to receive a SIGPIPE if p2 exits 122 | p1.stdout.close() 123 | output = p1.stderr.read() 124 | p1.wait() 125 | if p1.returncode == 0: 126 | Log.debug(self, "done") 127 | else: 128 | Log.error(self, output.decode("utf-8")) 129 | else: 130 | Log.info(self, "Backing up all databases") 131 | p1 = subprocess.Popen( 132 | "/usr/bin/mysqldump --all-databases " 133 | "--max_allowed_packet=1024M --hex-blob " 134 | "--single-transaction --events", 135 | stdout=subprocess.PIPE, 136 | stderr=subprocess.PIPE, shell=True) 137 | p2 = subprocess.Popen( 138 | "/usr/bin/zstd -c > " 139 | "/var/lib/wo-backup/mysql/fulldump-{0}.sql.zst" 140 | .format(WOVar.wo_date), 141 | stdin=p1.stdout, shell=True) 142 | p1.stdout.close() 143 | output = p1.stderr.read() 144 | p1.wait() 145 | if p1.returncode == 0: 146 | Log.debug(self, "done") 147 | else: 148 | Log.error(self, output.decode("utf-8")) 149 | 150 | except Exception as e: 151 | Log.error(self, "Error: process exited with status %s" 152 | % e) 153 | 154 | def check_db_exists(self, db_name): 155 | try: 156 | if WOMysql.dbConnection(self, db_name): 157 | return True 158 | except DatabaseNotExistsError as e: 159 | Log.debug(self, str(e)) 160 | return False 161 | except MySQLConnectionError as e: 162 | Log.debug(self, str(e)) 163 | return False 164 | -------------------------------------------------------------------------------- /wo/core/logwatch.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | Real time log files watcher supporting log rotation. 4 | """ 5 | 6 | import errno 7 | import os 8 | import stat 9 | import time 10 | 11 | from wo.core.logging import Log 12 | 13 | 14 | class LogWatcher(object): 15 | """Looks for changes in all files of a directory. 16 | This is useful for watching log file changes in real-time. 17 | It also supports files rotation. 18 | 19 | Example: 20 | 21 | >>> def callback(filename, lines): 22 | ... print filename, lines 23 | ... 24 | >>> l = LogWatcher("/var/www/example.com/logs", callback) 25 | >>> l.loop() 26 | """ 27 | 28 | def __init__(self, filelist, callback, extensions=["log"], tail_lines=0): 29 | """Arguments: 30 | 31 | (str) @folder: 32 | the folder to watch 33 | 34 | (callable) @callback: 35 | a function which is called every time a new line in a 36 | file being watched is found; 37 | this is called with "filename" and "lines" arguments. 38 | 39 | (list) @extensions: 40 | only watch files with these extensions 41 | 42 | (int) @tail_lines: 43 | read last N lines from files being watched before starting 44 | """ 45 | self.files_map = {} 46 | self.filelist = filelist 47 | self.callback = callback 48 | # self.folder = os.path.realpath(folder) 49 | self.extensions = extensions 50 | # assert (os.path.isdir(self.folder), "%s does not exists" 51 | # % self.folder) 52 | for file in self.filelist: 53 | if not os.path.isfile(file): 54 | if not callable(callback): 55 | self.update_files() 56 | # The first time we run the script we move all file markers at EOF. 57 | # In case of files created afterwards we don't do this. 58 | for id, file in list(iter(self.files_map.items())): 59 | file.seek(os.path.getsize(file.name)) # EOF 60 | if tail_lines: 61 | lines = self.tail(file.name, tail_lines) 62 | if lines: 63 | self.callback(file.name, lines) 64 | 65 | def __del__(self): 66 | self.close() 67 | 68 | def loop(self, interval=0.1, req_async=False): 69 | """Start the loop. 70 | If async is True make one loop then return. 71 | """ 72 | while 1: 73 | self.update_files() 74 | for fid, file in list(iter(self.files_map.items())): 75 | self.readfile(file) 76 | if req_async: 77 | return 78 | time.sleep(interval) 79 | 80 | def log(self, line): 81 | """Log when a file is un/watched""" 82 | print(line) 83 | 84 | # def listdir(self): 85 | # """List directory and filter files by extension. 86 | # You may want to override this to add extra logic or 87 | # globbling support. 88 | # """ 89 | # ls = os.listdir(self.folder) 90 | # if self.extensions: 91 | # return ([x for x in ls if os.path.splitext(x)[1][1:] 92 | # in self.extensions]) 93 | # else: 94 | # return ls 95 | 96 | @staticmethod 97 | def tail(fname, window): 98 | """Read last N lines from file fname.""" 99 | try: 100 | f = open(fname, encoding='utf-8', mode='r') 101 | except IOError as err: 102 | if err.errno == errno.ENOENT: 103 | return [] 104 | else: 105 | raise 106 | else: 107 | BUFSIZ = 1024 108 | f.seek(0, os.SEEK_END) 109 | fsize = f.tell() 110 | block = -1 111 | data = "" 112 | exit = False 113 | while not exit: 114 | step = (block * BUFSIZ) 115 | if abs(step) >= fsize: 116 | f.seek(0) 117 | exit = True 118 | else: 119 | f.seek(step, os.SEEK_END) 120 | data = f.read().strip() 121 | if data.count('\n') >= window: 122 | break 123 | else: 124 | block -= 1 125 | return data.splitlines()[-window:] 126 | 127 | def update_files(self): 128 | ls = [] 129 | for name in self.filelist: 130 | absname = os.path.realpath(os.path.join(name)) 131 | try: 132 | st = os.stat(absname) 133 | except EnvironmentError as err: 134 | if err.errno != errno.ENOENT: 135 | raise 136 | else: 137 | if not stat.S_ISREG(st.st_mode): 138 | continue 139 | fid = self.get_file_id(st) 140 | ls.append((fid, absname)) 141 | 142 | # check existent files 143 | for fid, file in list(iter(self.files_map.items())): 144 | # next(iter(graph.items())) 145 | try: 146 | st = os.stat(file.name) 147 | except EnvironmentError as err: 148 | if err.errno == errno.ENOENT: 149 | self.unwatch(file, fid) 150 | else: 151 | raise 152 | else: 153 | if fid != self.get_file_id(st): 154 | # same name but different file (rotation); reload it. 155 | self.unwatch(file, fid) 156 | self.watch(file.name) 157 | 158 | # add new ones 159 | for fid, fname in ls: 160 | if fid not in self.files_map: 161 | self.watch(fname) 162 | 163 | def readfile(self, file): 164 | lines = file.readlines() 165 | if lines: 166 | self.callback(file.name, lines) 167 | 168 | def watch(self, fname): 169 | try: 170 | file = open(fname, encoding='utf-8', mode='r') 171 | fid = self.get_file_id(os.stat(fname)) 172 | except EnvironmentError as err: 173 | if err.errno != errno.ENOENT: 174 | raise 175 | else: 176 | self.log("watching logfile %s" % fname) 177 | self.files_map[fid] = file 178 | 179 | def unwatch(self, file, fid): 180 | # file no longer exists; if it has been renamed 181 | # try to read it for the last time in case the 182 | # log rotator has written something in it. 183 | lines = self.readfile(file) 184 | self.log("un-watching logfile %s" % file.name) 185 | del self.files_map[fid] 186 | if lines: 187 | self.callback(file.name, lines) 188 | 189 | @staticmethod 190 | def get_file_id(st): 191 | return "%xg%x" % (st.st_dev, st.st_ino) 192 | 193 | def close(self): 194 | for id, file in list(iter(self.files_map.items())): 195 | file.close() 196 | self.files_map.clear() 197 | --------------------------------------------------------------------------------