commit e188096fe7a809ad8a0360e9e7096950d51eda46 Author: Lance Edgar Date: Wed Dec 2 22:18:22 2020 -0600 Initial commit, as generated by Rattail Demo diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d2534e4 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +Corporal.egg-info/ diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..bd74bcf --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,9 @@ +# -*- mode: conf; -*- + +include *.md +include *.rst + + +recursive-include corporal/web/static *.css +recursive-include corporal/web/static *.js +recursive-include corporal/web/templates *.mako diff --git a/README.md b/README.md new file mode 100644 index 0000000..8cc6219 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ + + +# Corporal + +This is a starter Rattail project. See the +[Rattail website](https://rattailproject.org/) +for more info. diff --git a/corporal/__init__.py b/corporal/__init__.py new file mode 100644 index 0000000..1f0c4b8 --- /dev/null +++ b/corporal/__init__.py @@ -0,0 +1,5 @@ +""" +Corporal package root +""" + +from ._version import __version__ diff --git a/corporal/_version.py b/corporal/_version.py new file mode 100644 index 0000000..e41b669 --- /dev/null +++ b/corporal/_version.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8; -*- + +__version__ = '0.1.0' diff --git a/corporal/commands.py b/corporal/commands.py new file mode 100644 index 0000000..367d21c --- /dev/null +++ b/corporal/commands.py @@ -0,0 +1,39 @@ +""" +Corporal commands +""" + +import sys + +from rattail import commands + +from corporal import __version__ + + +def main(*args): + """ + Main entry point for Corporal command system + """ + args = list(args or sys.argv[1:]) + cmd = Command() + cmd.run(*args) + + +class Command(commands.Command): + """ + Main command for Corporal + """ + name = 'corporal' + version = __version__ + description = "Corporal (custom Rattail system)" + long_description = '' + + +class HelloWorld(commands.Subcommand): + """ + The requisite 'hello world' example + """ + name = 'hello' + description = __doc__.strip() + + def run(self, args): + self.stdout.write("hello world!\n") diff --git a/corporal/config.py b/corporal/config.py new file mode 100644 index 0000000..9156578 --- /dev/null +++ b/corporal/config.py @@ -0,0 +1,20 @@ +""" +Custom config +""" + +from rattail.config import ConfigExtension + + +class CorporalConfig(ConfigExtension): + """ + Rattail config extension for Corporal + """ + key = 'corporal' + + def configure(self, config): + + # set some default config values + config.setdefault('rattail.mail', 'emails', 'corporal.emails') + config.setdefault('rattail', 'settings', 'corporal.settings') + config.setdefault('tailbone', 'menus', 'corporal.web.menus') + diff --git a/corporal/data/config/corporal-rattail.conf b/corporal/data/config/corporal-rattail.conf new file mode 100644 index 0000000..f9723ee --- /dev/null +++ b/corporal/data/config/corporal-rattail.conf @@ -0,0 +1,131 @@ + +############################################################ +# +# base config for Corporal +# +############################################################ + + + +############################## +# rattail +############################## + +[rattail] +# TODO: this will of course depend on your location +timezone.default = America/Chicago +# TODO: you should change these to absolute path +batch.files = app/batch +workdir = app/work + +[rattail.config] +# include = /etc/rattail/rattail.conf +configure_logging = true +usedb = true +preferdb = true + +[rattail.db] +default.url = postgresql://rattail:rattailpass@localhost/corporal +# TODO: disable if you do not want data versioning +versioning.enabled = true + +[rattail.mail] +# TODO: enable this if you want emails to send +send_emails = false +smtp.server = localhost +templates = rattail:templates/mail +default.prefix = [Corporal] +default.from = rattail@localhost +default.to = root@localhost +# default.enabled = false + + +############################## +# alembic +############################## + +[alembic] +script_location = rattail.db:alembic +version_locations = rattail.db:alembic/versions + + +############################## +# logging +############################## + +[loggers] +keys = root, exc_logger, beaker, txn, sqlalchemy, django_db, flufl_bounce, requests + +[handlers] +keys = file, console, email + +[formatters] +keys = generic, console + +[logger_root] +handlers = file, console +level = DEBUG + +[logger_exc_logger] +qualname = exc_logger +handlers = email +level = ERROR + +[logger_beaker] +qualname = beaker +handlers = +level = INFO + +[logger_txn] +qualname = txn +handlers = +level = INFO + +[logger_sqlalchemy] +qualname = sqlalchemy.engine +handlers = +# handlers = file +# level = INFO + +[logger_django_db] +qualname = django.db.backends +handlers = +level = INFO +# level = DEBUG + +[logger_flufl_bounce] +qualname = flufl.bounce +handlers = +level = WARNING + +[logger_requests] +qualname = requests +handlers = +# level = WARNING + +[handler_file] +class = handlers.RotatingFileHandler +# TODO: you probably should change this to absolute path +args = ('app/log/rattail.log', 'a', 1000000, 100, 'utf_8') +formatter = generic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +formatter = console +# formatter = generic +# level = INFO +# level = WARNING + +[handler_email] +class = handlers.SMTPHandler +args = ('localhost', 'rattail@localhost', ['root@localhost'], "[Corporal] Logging") +formatter = generic +level = ERROR + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(funcName)s: %(message)s +datefmt = %Y-%m-%d %H:%M:%S + +[formatter_console] +format = %(levelname)-5.5s [%(name)s][%(threadName)s] %(funcName)s: %(message)s diff --git a/corporal/data/config/corporal-web.conf b/corporal/data/config/corporal-web.conf new file mode 100644 index 0000000..830986c --- /dev/null +++ b/corporal/data/config/corporal-web.conf @@ -0,0 +1,71 @@ + +############################################################ +# +# config for Corporal web app +# +############################################################ + + +############################## +# rattail +############################## + +[rattail.config] +include = %(here)s/rattail.conf + +[tailbone] +products.print_labels = false + + +############################## +# pyramid +############################## + +[app:main] +use = egg:Corporal + +# TODO: you should probably disable these first two for production +pyramid.reload_templates = true +pyramid.debug_all = true +pyramid.default_locale_name = en +# pyramid.includes = pyramid_debugtoolbar + +beaker.session.type = file +beaker.session.data_dir = %(here)s/sessions/data +beaker.session.lock_dir = %(here)s/sessions/lock +# TODO: you should change this +beaker.session.secret = XXXXXXXXXXXXXXXXXXXX +beaker.session.key = corporal + +exclog.extra_info = true + +# required for tailbone +rattail.config = %(__file__)s + +[server:main] +use = egg:waitress#main +host = 0.0.0.0 +port = 9761 + +# NOTE: this is needed for local reverse proxy stuff to work with HTTPS +# https://docs.pylonsproject.org/projects/waitress/en/latest/reverse-proxy.html +# https://docs.pylonsproject.org/projects/waitress/en/latest/arguments.html +# trusted_proxy = 127.0.0.1 + +# TODO: leave this empty if proxy serves as root site, e.g. http://rattail.example.com/ +# url_prefix = + +# TODO: or, if proxy serves as subpath of root site, e.g. http://rattail.example.com/backend/ +# url_prefix = /backend + + +############################## +# logging +############################## + +[handler_console] +level = INFO + +[handler_file] +# TODO: you probably should change this to absolute path +args = ('app/log/web.log', 'a', 1000000, 100, 'utf_8') diff --git a/corporal/emails.py b/corporal/emails.py new file mode 100644 index 0000000..3214582 --- /dev/null +++ b/corporal/emails.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8; -*- +""" +Custom email profiles +""" + +from rattail.mail import Email + +# bring in some common config from rattail +from rattail.emails import (ImporterEmail, + # ProblemReportEmail, + upgrade_failure, + upgrade_success, + user_feedback) + + diff --git a/corporal/fablib/__init__.py b/corporal/fablib/__init__.py new file mode 100644 index 0000000..d183a95 --- /dev/null +++ b/corporal/fablib/__init__.py @@ -0,0 +1,8 @@ +""" +Fabric library for Corporal +""" + +from rattail_fabric2 import make_deploy + + +deploy_common = make_deploy(__file__) diff --git a/corporal/fablib/deploy/python/premkvirtualenv.mako b/corporal/fablib/deploy/python/premkvirtualenv.mako new file mode 100644 index 0000000..bc728da --- /dev/null +++ b/corporal/fablib/deploy/python/premkvirtualenv.mako @@ -0,0 +1,18 @@ +#!/bin/bash +# This hook is run after a new virtualenv is created and before it is activated. + +cat >$1/pip.conf <$1/bin/postactivate <$1/bin/postdeactivate < + +<%def name="favicon()"> + ## + + + +<%def name="header_logo()"> + ${h.image(request.static_url('tailbone:static/img/rattail.ico'), "Header Logo", style="height: 49px;")} + + +<%def name="footer()"> +

+ ${h.link_to("Corporal {}{}".format(corporal.__version__, '' if request.rattail_config.production() else '+dev'), url('about'))} +

+ \ No newline at end of file diff --git a/corporal/web/views/__init__.py b/corporal/web/views/__init__.py new file mode 100644 index 0000000..3907e63 --- /dev/null +++ b/corporal/web/views/__init__.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8; -*- +""" +Corporal Views +""" + + +def includeme(config): + + # core views + config.include('corporal.web.views.common') + config.include('tailbone.views.auth') + config.include('tailbone.views.tables') + config.include('tailbone.views.upgrades') + config.include('tailbone.views.progress') + + # main table views + config.include('tailbone.views.customergroups') + config.include('tailbone.views.datasync') + config.include('tailbone.views.email') + config.include('tailbone.views.families') + config.include('tailbone.views.members') + config.include('tailbone.views.messages') + config.include('tailbone.views.people') + config.include('tailbone.views.reportcodes') + config.include('tailbone.views.roles') + config.include('tailbone.views.settings') + config.include('tailbone.views.subdepartments') + config.include('tailbone.views.shifts') + config.include('tailbone.views.users') + + config.include('tailbone.views.stores') + config.include('tailbone.views.customers') + config.include('tailbone.views.employees') + config.include('tailbone.views.taxes') + config.include('tailbone.views.departments') + config.include('tailbone.views.brands') + config.include('tailbone.views.vendors') + config.include('tailbone.views.products') + + # purchasing / receiving + config.include('tailbone.views.purchases') + config.include('tailbone.views.purchasing') + + # batch views + config.include('tailbone.views.handheld') + config.include('tailbone.views.batch.inventory') diff --git a/corporal/web/views/common.py b/corporal/web/views/common.py new file mode 100644 index 0000000..5d457e4 --- /dev/null +++ b/corporal/web/views/common.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8; -*- +""" +Common views +""" + +from tailbone.views import common as base + +import corporal + + +class CommonView(base.CommonView): + + project_title = "Corporal" + project_version = corporal.__version__ + '+dev' + + +def includeme(config): + CommonView.defaults(config) diff --git a/machines/server/README.md b/machines/server/README.md new file mode 100644 index 0000000..8d50d65 --- /dev/null +++ b/machines/server/README.md @@ -0,0 +1,41 @@ + +# server + +This is for a generic "server" which runs Corporal. + + +## Updating Live Server + +Note that this assumes you have defined `server` within your SSH config, +e.g. at `~/.ssh/config`. Also you should configure the root password within +`./fabric.yaml`. + +Install everything with: + + fab2 -e -H server bootstrap-all + + +## Testing with Vagrant + +You should be able to get a VM going with a simple: + + vagrant up + +You can then SSH directly into the VM with: + + vagrant ssh + +You can confirm SSH credentials needed for connecting to it, with: + + vagrant ssh-config + +Now you can "bootstrap" the machine with Fabric. Please double-check your +`fabenv.py` file and make sure it contains: + + env.machine_is_live = False + +After all this machine is *not* live, it's just for testing. Finally, here is +the bootstrap command. Note that it's possible you may need to modify some +parameters based on the output of `vagrant ssh-config` above. + + fab2 -e -H vagrant@localhost:2222 -i .vagrant/machines/default/virtualbox/private_key bootstrap-all diff --git a/machines/server/Vagrantfile b/machines/server/Vagrantfile new file mode 100644 index 0000000..03c0b58 --- /dev/null +++ b/machines/server/Vagrantfile @@ -0,0 +1,24 @@ +# -*- mode: ruby; -*- + +Vagrant.configure("2") do |config| + + # live machine runs Debian 10 Buster + config.vm.box = "debian/buster64" + + # # live machine runs Ubuntu 20.04 Focal Fossa + # config.vm.box = "ubuntu/focal64" + + # this may be necessary for big data import tasks. you can raise or lower + # it, or comment out if you find that you don't need it + config.vm.provider "virtualbox" do |v| + v.memory = 4096 + end + + # Corporal web app + config.vm.network "forwarded_port", guest: 9761, host: 9761 + + # apache + config.vm.network "forwarded_port", guest: 80, host: 8080 + config.vm.network "forwarded_port", guest: 443, host: 8443 + +end diff --git a/machines/server/deploy/corporal/cron.conf b/machines/server/deploy/corporal/cron.conf new file mode 100644 index 0000000..ef23083 --- /dev/null +++ b/machines/server/deploy/corporal/cron.conf @@ -0,0 +1,34 @@ + +############################################################ +# +# cron config for Corporal +# +############################################################ + + +############################## +# rattail +############################## + +[rattail.config] +include = %(here)s/rattail.conf + + +############################## +# alembic +############################## + +[alembic] +script_location = rattail.db:alembic +version_locations = rattail.db:alembic/versions + + +############################## +# logging +############################## + +[handler_console] +level = WARNING + +[handler_file] +args = ('/srv/envs/corporal/app/log/cron.log', 'a', 'utf_8') diff --git a/machines/server/deploy/corporal/crontab.mako b/machines/server/deploy/corporal/crontab.mako new file mode 100644 index 0000000..f87686f --- /dev/null +++ b/machines/server/deploy/corporal/crontab.mako @@ -0,0 +1,11 @@ + +${'#'}########################################################### +# +# crontab for Corporal +# +${'#'}########################################################### + +MAILTO="root@localhost,fred@mailinator.com" + +# overnight automation starts at 1:00am +${'' if env.machine_is_live else '#'}00 01 * * * rattail /srv/envs/corporal/app/overnight-wrapper.sh diff --git a/machines/server/deploy/corporal/logrotate.conf b/machines/server/deploy/corporal/logrotate.conf new file mode 100644 index 0000000..0954fd7 --- /dev/null +++ b/machines/server/deploy/corporal/logrotate.conf @@ -0,0 +1,20 @@ + +/srv/envs/corporal/pip.log { + daily + missingok + rotate 30 + compress + delaycompress + notifempty + create 640 rattail rattail +} + +/srv/envs/corporal/app/log/*.log { + daily + missingok + rotate 30 + compress + delaycompress + notifempty + create 640 rattail rattail +} diff --git a/machines/server/deploy/corporal/overnight-wrapper.sh b/machines/server/deploy/corporal/overnight-wrapper.sh new file mode 100644 index 0000000..5a3a62f --- /dev/null +++ b/machines/server/deploy/corporal/overnight-wrapper.sh @@ -0,0 +1,21 @@ +#!/bin/sh -e +############################################################ +# +# wrapper script for Corporal overnight automation +# +############################################################ + + +if [ "$1" = "--verbose" ]; then + VERBOSE='--verbose' + PROGRESS='--progress' +else + VERBOSE= + PROGRESS= +fi + +cd /srv/envs/corporal + +RATTAIL="bin/rattail --config=app/cron.conf $PROGRESS" + +$RATTAIL run-n-mail --no-versioning --skip-if-empty --subject 'Overnight automation' /srv/envs/corporal/app/overnight.sh diff --git a/machines/server/deploy/corporal/overnight.sh b/machines/server/deploy/corporal/overnight.sh new file mode 100644 index 0000000..5704902 --- /dev/null +++ b/machines/server/deploy/corporal/overnight.sh @@ -0,0 +1,36 @@ +#!/bin/sh -e +############################################################ +# +# overnight automation for Corporal +# +############################################################ + +if [ "$1" = "--verbose" ]; then + VERBOSE='--verbose' + PROGRESS='--progress' +else + VERBOSE= + PROGRESS= +fi + +cd /srv/envs/corporal + +RATTAIL="bin/rattail --config=app/cron.conf $PROGRESS" + + +############################## +# data sync +############################## + +# import latest data from Catapult +$RATTAIL --runas catapult import-catapult --delete --warnings + +# make sure version data is correct +$RATTAIL import-versions --delete --dry-run --warnings + + +############################## +# problem reports +############################## + +$RATTAIL problems diff --git a/machines/server/deploy/corporal/rattail.conf.mako b/machines/server/deploy/corporal/rattail.conf.mako new file mode 100644 index 0000000..86b05e3 --- /dev/null +++ b/machines/server/deploy/corporal/rattail.conf.mako @@ -0,0 +1,148 @@ +## -*- mode: conf; -*- + +${'#'}########################################################### +# +# base config for Corporal +# +${'#'}########################################################### + + + + +${'#'}############################# +# rattail +${'#'}############################# + +[rattail] +production = ${'true' if env.machine_is_live else 'false'} + +# TODO: this will of course depend on your location +timezone.default = America/Chicago + +# TODO: set this to a valid user within your DB +#runas.default = corporal + +appdir = /srv/envs/corporal/app +datadir = /srv/envs/corporal/app/data +workdir = /srv/envs/corporal/app/work +batch.files = /srv/envs/corporal/app/batch +export.files = /srv/envs/corporal/app/data/exports + +[rattail.config] +#include = /etc/rattail/rattail.conf +configure_logging = true +usedb = true +preferdb = true + +[rattail.db] +default.url = postgresql://rattail:${env.password_postgresql_rattail}@localhost/corporal +versioning.enabled = true + +[rattail.mail] +# this is the master switch, *no* emails are sent if false +send_emails = true + +smtp.server = localhost + +templates = + corporal:templates/mail + rattail:templates/mail + +default.prefix = [Corporal] +default.from = ${env.email_default_sender} +default.to = ${', '.join(env.email_default_recipients)} +#default.enabled = false + +[rattail.upgrades] +command = sudo /srv/envs/corporal/app/upgrade-wrapper.sh --verbose +files = /srv/envs/corporal/app/data/upgrades + + +${'#'}############################# +# alembic +${'#'}############################# + +[alembic] +script_location = rattail.db:alembic +version_locations = rattail.db:alembic/versions + + +${'#'}############################# +# logging +${'#'}############################# + +[loggers] +keys = root, exc_logger, beaker, txn, sqlalchemy, django_db, flufl_bounce, requests + +[handlers] +keys = file, console, email + +[formatters] +keys = generic, console + +[logger_root] +handlers = file, console, email +level = DEBUG + +[logger_exc_logger] +qualname = exc_logger +handlers = email +level = ERROR + +[logger_beaker] +qualname = beaker +handlers = +level = INFO + +[logger_txn] +qualname = txn +handlers = +level = INFO + +[logger_sqlalchemy] +qualname = sqlalchemy.engine +handlers = +# handlers = file +# level = INFO + +[logger_django_db] +qualname = django.db.backends +handlers = +level = INFO +# level = DEBUG + +[logger_flufl_bounce] +qualname = flufl.bounce +handlers = +level = WARNING + +[logger_requests] +qualname = requests +handlers = +# level = WARNING + +[handler_file] +class = handlers.WatchedFileHandler +args = ('/srv/envs/corporal/app/log/rattail.log', 'a', 'utf_8') +formatter = generic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +formatter = console +# formatter = generic +# level = INFO +# level = WARNING + +[handler_email] +class = handlers.SMTPHandler +args = ('localhost', '${env.email_default_sender}', ${env.email_default_recipients}, "[Corporal] Logging") +formatter = generic +level = ERROR + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(funcName)s: %(message)s +datefmt = %Y-%m-%d %H:%M:%S + +[formatter_console] +format = %(levelname)-5.5s [%(name)s] %(funcName)s: %(message)s diff --git a/machines/server/deploy/corporal/sudoers b/machines/server/deploy/corporal/sudoers new file mode 100644 index 0000000..8be0be4 --- /dev/null +++ b/machines/server/deploy/corporal/sudoers @@ -0,0 +1,5 @@ +# -*- mode: conf; -*- + +# let rattail upgrade the app +rattail ALL = NOPASSWD: /srv/envs/corporal/app/upgrade-wrapper.sh +rattail ALL = NOPASSWD: /srv/envs/corporal/app/upgrade-wrapper.sh --verbose diff --git a/machines/server/deploy/corporal/supervisor.conf b/machines/server/deploy/corporal/supervisor.conf new file mode 100644 index 0000000..e9c1bd8 --- /dev/null +++ b/machines/server/deploy/corporal/supervisor.conf @@ -0,0 +1,8 @@ + +[group:corporal] +programs=corporal_webmain + +[program:corporal_webmain] +command=/srv/envs/corporal/bin/pserve pastedeploy+ini:/srv/envs/corporal/app/web.conf +directory=/srv/envs/corporal/app/work +user=rattail diff --git a/machines/server/deploy/corporal/tasks.py b/machines/server/deploy/corporal/tasks.py new file mode 100644 index 0000000..769a03e --- /dev/null +++ b/machines/server/deploy/corporal/tasks.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8; -*- + +from invoke import task + + +@task +def upgrade(ctx): + ctx.run('/srv/envs/corporal/app/upgrade.sh --verbose') diff --git a/machines/server/deploy/corporal/upgrade-wrapper.sh b/machines/server/deploy/corporal/upgrade-wrapper.sh new file mode 100644 index 0000000..76f8180 --- /dev/null +++ b/machines/server/deploy/corporal/upgrade-wrapper.sh @@ -0,0 +1,19 @@ +#!/bin/sh -e + +if [ "$1" = "--verbose" ]; then + VERBOSE='--verbose' + INVOKE_ARGS='--echo' +else + VERBOSE= + INVOKE_ARGS= +fi + +cd /srv/envs/corporal + +INVOKE="sudo -H -u rattail bin/invoke --collection=app/tasks $INVOKE_ARGS" + +# run upgrade task, as rattail user +$INVOKE upgrade + +# restart web app +sh -c 'sleep 10; supervisorctl restart corporal:corporal_webmain' & diff --git a/machines/server/deploy/corporal/upgrade.sh b/machines/server/deploy/corporal/upgrade.sh new file mode 100644 index 0000000..9b50870 --- /dev/null +++ b/machines/server/deploy/corporal/upgrade.sh @@ -0,0 +1,38 @@ +#!/bin/sh -e + +# NOTE: this script is meant be ran by the 'rattail' user! + +if [ "$1" = "--verbose" ]; then + VERBOSE='--verbose' + QUIET= +else + VERBOSE= + QUIET='--quiet' +fi + +SRC=/srv/envs/corporal/src +PIP=/srv/envs/corporal/bin/pip +export PIP_CONFIG_FILE=/srv/envs/corporal/pip.conf + +# upgrade pip +$PIP install $QUIET --disable-pip-version-check --upgrade pip +# $PIP install $QUIET --upgrade --upgrade-strategy eager setuptools wheel ndg-httpsclient + +# upgrade app software... + +# how you need to upgrade your app will depend on whether you are running any +# packages "from source" as opposed to only using built/released packages + +# if running Corporal from source, you should first fetch/install latest code: +#cd $SRC/corporal +#git pull $QUIET +#find . -name '*.pyc' -delete +#$PIP install $QUIET --editable . + +# in any case the last step is always the same. note that this will ensure the +# "latest" Corporal is used, but also will upgrade any dependencies +$PIP install $QUIET --upgrade --upgrade-strategy eager 'Corporal' + +# migrate database schema +cd /srv/envs/corporal +bin/alembic --config app/rattail.conf upgrade heads diff --git a/machines/server/deploy/corporal/web.conf.mako b/machines/server/deploy/corporal/web.conf.mako new file mode 100644 index 0000000..1185af5 --- /dev/null +++ b/machines/server/deploy/corporal/web.conf.mako @@ -0,0 +1,72 @@ +## -*- mode: conf; -*- + +${'#'}########################################################### +# +# config for Corporal web app +# +${'#'}########################################################### + + +${'#'}############################# +# rattail +${'#'}############################# + +[rattail.config] +include = %(here)s/rattail.conf + + +${'#'}############################# +# pyramid +${'#'}############################# + +[app:main] +use = egg:Corporal + +pyramid.reload_templates = false +pyramid.debug_all = false +pyramid.default_locale_name = en +pyramid.includes = pyramid_exclog + +beaker.session.type = file +beaker.session.data_dir = %(here)s/sessions/data +beaker.session.lock_dir = %(here)s/sessions/lock +beaker.session.secret = ${env.tailbone_beaker_secret} +beaker.session.key = corporal + +pyramid_deform.tempdir = %(here)s/data/uploads + +exclog.extra_info = true + +# required for tailbone +rattail.config = %(__file__)s + +[server:main] +use = egg:waitress#main +# TODO: should ideally use 127.0.0.1 as host address, e.g. with reverse proxy +host = 0.0.0.0 +port = 9761 + +# NOTE: this is needed for local reverse proxy stuff to work with HTTPS +# https://docs.pylonsproject.org/projects/waitress/en/latest/reverse-proxy.html +# https://docs.pylonsproject.org/projects/waitress/en/latest/arguments.html +# trusted_proxy = 127.0.0.1 + +# TODO: leave this empty if proxy serves as root site, e.g. http://rattail.example.com/ +# url_prefix = + +# TODO: or, if proxy serves as subpath of root site, e.g. http://rattail.example.com/backend/ +# url_prefix = /backend + + +${'#'}############################# +# logging +${'#'}############################# + +[logger_root] +handlers = file, console + +[handler_console] +level = INFO + +[handler_file] +args = ('/srv/envs/corporal/app/log/web.log', 'a', 'utf_8') diff --git a/machines/server/fabenv.py.dist b/machines/server/fabenv.py.dist new file mode 100644 index 0000000..173138e --- /dev/null +++ b/machines/server/fabenv.py.dist @@ -0,0 +1,44 @@ +# -*- coding: utf-8; mode: python; -*- +""" +Fabric environment tweaks +""" + +from fabfile import env + + +############################## +# volatile +############################## + +# this should be True only when targeting the truly *live* machine, but False +# otherwise, e.g. when building a new "live" machine, or using Vagrant +env.machine_is_live = False + + +############################## +# stable +############################## + +# default sender and recipients for all emails +env.email_default_sender = 'rattail@example.com' +env.email_default_recipients = ['root@example.com'] + +# this is for the 'rattail' user within PostgreSQL, running on the server. +# this rattail user owns the 'corporal' database and is used by the +# Corporal app to access the db +env.password_postgresql_rattail = 'password' + +# this is the hostname for your Catapult WebOffice +env.catapult_host = 'INSTANCE.catapultweboffice.com' + +# these credentials are used to access the ODBC DSN for ECRS Catapult +env.catapult_odbc_username = 'username' +env.catapult_odbc_password = 'password' + +# this is used for protecting user session data for the web app, which lives in +# the server's file system. should probably be at least 20 random characters +env.tailbone_beaker_secret = 'password' + +# these credentials are used to access the "restricted" Rattail Project PyPI +env.restricted_pypi_username = 'username' +env.restricted_pypi_password = 'password' diff --git a/machines/server/fabfile.py b/machines/server/fabfile.py new file mode 100644 index 0000000..10b9be0 --- /dev/null +++ b/machines/server/fabfile.py @@ -0,0 +1,156 @@ +# -*- coding: utf-8; -*- +""" +Fabric script for 'server' machine + +Please see the accompanying README for full instructions. +""" + +from fabric2 import task + +from rattail.core import Object +from rattail_fabric2 import apt, postfix, postgresql, python, exists, make_system_user, mkdir + +from corporal.fablib import make_deploy +from corporal.fablib.python import bootstrap_python + + +env = Object() +deploy = make_deploy(__file__) + + +############################## +# bootstrap +############################## + +@task +def bootstrap_all(c): + """ + Bootstrap all aspects of the machine + """ + bootstrap_base(c) + bootstrap_corporal(c) + + +@task +def bootstrap_base(c): + """ + Bootstrap the base system + """ + apt.dist_upgrade(c) + + # postfix + postfix.install(c) + + # rattail user + make_system_user(c, 'rattail', home='/var/lib/rattail', shell='/bin/bash') + postfix.alias(c, 'rattail', 'root') + + # python + bootstrap_python(c, user='rattail', env=env, + virtualenvwrapper_from_apt=True, + python3=True) + + # postgres + apt.install(c, 'postgresql') + postgresql.create_user(c, 'rattail', password=env.password_postgresql_rattail) + + + # misc. + apt.install( + c, + 'git', + 'libpq-dev', + 'supervisor', + ) + + # uncomment this if you prefer to use emacs + #apt.install_emacs(c) + + +@task +def bootstrap_corporal(c): + """ + Bootstrap the Corporal app + """ + user = 'rattail' + + c.sudo('supervisorctl stop corporal:', warn=True) + + # virtualenv + if not exists(c, '/srv/envs/corporal'): + python.mkvirtualenv(c, 'corporal', python='/usr/bin/python3', runas_user=user) + c.sudo('chmod 0600 /srv/envs/corporal/pip.conf') + mkdir(c, '/srv/envs/corporal/src', owner=user, use_sudo=True) + + # Corporal + if env.machine_is_live: + c.sudo("bash -lc 'workon corporal && pip install Corporal'", + user=user) + else: + # TODO: this really only works for vagrant + c.sudo("bash -lc 'workon corporal && pip install /vagrant/Corporal-*.tar.gz'", + user=user) + + # app dir + if not exists(c, '/srv/envs/corporal/app'): + c.sudo("bash -lc 'workon corporal && cdvirtualenv && rattail make-appdir'", user=user) + c.sudo('chmod 0750 /srv/envs/corporal/app/log') + mkdir(c, '/srv/envs/corporal/app/data', use_sudo=True, owner=user) + + # config / scripts + deploy(c, 'corporal/rattail.conf.mako', '/srv/envs/corporal/app/rattail.conf', + owner=user, mode='0600', use_sudo=True, context={'env': env}) + if not exists(c, '/srv/envs/corporal/app/quiet.conf'): + c.sudo("bash -lc 'workon corporal && cdvirtualenv app && rattail make-config -T quiet'", + user=user) + deploy(c, 'corporal/cron.conf', '/srv/envs/corporal/app/cron.conf', + owner=user, use_sudo=True) + deploy(c, 'corporal/web.conf.mako', '/srv/envs/corporal/app/web.conf', + owner=user, mode='0600', use_sudo=True, context={'env': env}) + deploy(c, 'corporal/upgrade.sh', '/srv/envs/corporal/app/upgrade.sh', + owner=user, mode='0755', use_sudo=True) + deploy(c, 'corporal/tasks.py', '/srv/envs/corporal/app/tasks.py', + owner=user, use_sudo=True) + deploy(c, 'corporal/upgrade-wrapper.sh', '/srv/envs/corporal/app/upgrade-wrapper.sh', + owner=user, mode='0755', use_sudo=True) + deploy(c, 'corporal/overnight.sh', '/srv/envs/corporal/app/overnight.sh', + owner=user, mode='0755', use_sudo=True) + deploy(c, 'corporal/overnight-wrapper.sh', '/srv/envs/corporal/app/overnight-wrapper.sh', + owner=user, mode='0755', use_sudo=True) + + # database + if not postgresql.db_exists(c, 'corporal'): + postgresql.create_db(c, 'corporal', owner='rattail', checkfirst=False) + c.sudo("bash -lc 'workon corporal && cdvirtualenv && bin/alembic --config app/rattail.conf upgrade heads'", + user=user) + postgresql.sql(c, "insert into setting values ('rattail.app_title', 'Corporal')", + database='corporal') + postgresql.sql(c, "insert into setting values ('tailbone.theme', 'falafel')", + database='corporal') + postgresql.sql(c, "insert into setting values ('tailbone.themes.expose_picker', 'false')", + database='corporal') + + # supervisor + deploy(c, 'corporal/supervisor.conf', '/etc/supervisor/conf.d/corporal.conf', + use_sudo=True) + c.sudo('supervisorctl update') + c.sudo('supervisorctl start corporal:') + + # cron etc. + deploy.sudoers(c, 'corporal/sudoers', '/etc/sudoers.d/corporal') + deploy(c, 'corporal/crontab.mako', '/etc/cron.d/corporal', + use_sudo=True, context={'env': env}) + deploy(c, 'corporal/logrotate.conf', '/etc/logrotate.d/corporal', + use_sudo=True) + + +############################## +# fabenv +############################## + +try: + import fabenv +except ImportError as error: + print("\ncouldn't import fabenv: {}".format(error)) + +env.setdefault('machine_is_live', False) diff --git a/machines/server/fabric.yaml.dist b/machines/server/fabric.yaml.dist new file mode 100644 index 0000000..62aa848 --- /dev/null +++ b/machines/server/fabric.yaml.dist @@ -0,0 +1,3 @@ +# -*- mode: yaml; -*- +sudo: + password: "XXXXXXXX" diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..53e619d --- /dev/null +++ b/setup.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8; -*- +""" +Corporal setup script +""" + +import os +from setuptools import setup, find_packages + + +here = os.path.abspath(os.path.dirname(__file__)) +exec(open(os.path.join(here, 'corporal', '_version.py')).read()) +README = open(os.path.join(here, 'README.md')).read() + + +requires = [ + # + # Version numbers within comments below have specific meanings. + # Basically the 'low' value is a "soft low," and 'high' a "soft high." + # In other words: + # + # If either a 'low' or 'high' value exists, the primary point to be + # made about the value is that it represents the most current (stable) + # version available for the package (assuming typical public access + # methods) whenever this project was started and/or documented. + # Therefore: + # + # If a 'low' version is present, you should know that attempts to use + # versions of the package significantly older than the 'low' version + # may not yield happy results. (A "hard" high limit may or may not be + # indicated by a true version requirement.) + # + # Similarly, if a 'high' version is present, and especially if this + # project has laid dormant for a while, you may need to refactor a bit + # when attempting to support a more recent version of the package. (A + # "hard" low limit should be indicated by a true version requirement + # when a 'high' version is present.) + # + # In any case, developers and other users are encouraged to play + # outside the lines with regard to these soft limits. If bugs are + # encountered then they should be filed as such. + # + # package # low high + + 'invoke', # 1.4.1 + 'rattail[auth,db,bouncer]', # 0.9.141 + 'rattail-fabric2', # 0.2.1 + + 'psycopg2', # 2.8.4 + + 'Tailbone', # 0.8.72 + +] + + +setup( + name = "Corporal", + version = __version__, + author = "Your Name", + author_email = "you@example.com", + # url = "", + description = "Custom Rattail project for Lance Edgar", + long_description = README, + + classifiers = [ + # TODO: remove this if you intend to publish your project! + # (it's here by default, to prevent accidental publishing) + 'Private :: Do Not Upload', + 'Development Status :: 3 - Alpha', + 'Environment :: Console', + 'Environment :: Web Environment', + 'Framework :: Pyramid', + 'Intended Audience :: Developers', + 'Natural Language :: English', + 'Operating System :: POSIX :: Linux', + 'Programming Language :: Python', + 'Programming Language :: Python :: 3', + 'Topic :: Office/Business', + ], + + install_requires = requires, + packages = find_packages(), + include_package_data = True, + # zip_safe = False, + + entry_points = { + + 'rattail.config.extensions': [ + 'corporal = corporal.config:CorporalConfig', + ], + + 'console_scripts': [ + 'corporal = corporal.commands:main', + ], + + 'corporal.commands': [ + 'hello = corporal.commands:HelloWorld', + ], + + 'paste.app_factory': [ + 'main = corporal.web.app:main', + ], + }, +) diff --git a/tasks.py b/tasks.py new file mode 100644 index 0000000..b26919e --- /dev/null +++ b/tasks.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8; -*- +""" +Tasks for Corporal +""" + +import os +import shutil + +from invoke import task + + +here = os.path.abspath(os.path.dirname(__file__)) +exec(open(os.path.join(here, 'corporal', '_version.py')).read()) + + +@task +def release(ctx): + """ + Release a new version of Corporal + """ + # rebuild local tar.gz file for distribution + shutil.rmtree('Corporal.egg-info') + ctx.run('python setup.py sdist --formats=gztar') + + # TODO: uncomment and update these details, to upload to private PyPI + #filename = 'Corporal-{}.tar.gz'.format(__version__) + #ctx.run('scp dist/{} rattail@pypi.example.com:/srv/pypi/corporal/'.format(filename))