django-orchestra/orchestra/management/commands/setupnginx.py

341 lines
13 KiB
Python
Raw Permalink Normal View History

2015-05-04 12:57:41 +00:00
import os
2015-04-27 14:54:17 +00:00
import textwrap
2014-05-08 16:59:35 +00:00
from os.path import expanduser
from django.conf import settings
2015-05-05 20:11:03 +00:00
from django.core.management.base import BaseCommand, CommandError
2014-05-08 16:59:35 +00:00
2015-04-27 14:54:17 +00:00
from orchestra.utils import paths
2015-08-31 11:58:59 +00:00
from orchestra.utils.sys import run, check_root, confirm
2014-05-08 16:59:35 +00:00
class Command(BaseCommand):
2021-01-30 13:17:18 +00:00
help = 'Configures nginx + uwsgi to run with your Orchestra instance.'
2014-05-08 16:59:35 +00:00
def __init__(self, *args, **kwargs):
super(Command, self).__init__(*args, **kwargs)
2021-01-30 13:17:18 +00:00
def add_arguments(self, parser):
parser.add_argument(
'--cert',
dest='cert',
default='',
help='Nginx SSL certificate, one will be created by default.',
)
parser.add_argument(
'--cert-key',
dest='cert_key',
default='',
help='Nginx SSL certificate key.'
)
parser.add_argument(
'--cert-path',
dest='cert_path',
default=os.path.join(paths.get_site_dir(), 'ssl', 'orchestra.crt'),
help='Nginx SSL certificate, one will be created by default.'
)
parser.add_argument(
'--cert-key-path',
dest='cert_key_path',
default=os.path.join(paths.get_site_dir(), 'ssl', 'orchestra.key'),
help='Nginx SSL certificate key.'
)
parser.add_argument(
'--cert-override',
dest='cert_override',
action='store_true',
default=False, help='Force override cert and keys if exists.'
)
parser.add_argument(
'--cert-country',
dest='cert_country',
default='ES',
help='Certificate Distinguished Name Country.'
)
parser.add_argument(
'--cert-state',
dest='cert_state',
default='Spain',
help='Certificate Distinguished Name STATE.'
)
parser.add_argument(
'--cert-locality',
dest='cert_locality',
default='Barcelona',
help='Certificate Distinguished Name Country.'
)
parser.add_argument(
'--cert-org_name',
dest='cert_org_name',
default='Orchestra',
help='Certificate Distinguished Name Organization Name.'
)
parser.add_argument(
'--cert-org_unit',
dest='cert_org_unit',
default='DevOps',
help='Certificate Distinguished Name Organization Unity.'
)
parser.add_argument(
'--cert-email',
dest='cert_email',
default='orchestra@orchestra.lan',
help='Certificate Distinguished Name Email Address.'
)
parser.add_argument(
'--cert-common_name',
dest='cert_common_name',
default=None,
help='Certificate Distinguished Name Common Name.'
)
parser.add_argument(
'--server-name',
dest='server_name',
default='',
help='Nginx SSL certificate key.'
)
parser.add_argument(
'--user',
dest='user',
default='',
help='uWSGI daemon user.'
)
parser.add_argument(
'--group',
dest='group',
default='',
help='uWSGI daemon group.'
)
parser.add_argument(
'--processes',
dest='processes',
default=4,
help='uWSGI number of processes.'
)
parser.add_argument(
'--noinput',
action='store_false',
dest='interactive',
default=True,
help='''Tells Django to NOT prompt the user for input of any kind.
You must use --username with --noinput, and must contain the
cleeryd process owner, which is the user how will perform tincd updates'''
)
2014-05-08 16:59:35 +00:00
2015-05-04 12:57:41 +00:00
def generate_certificate(self, **options):
override = options.get('cert_override')
2014-05-08 16:59:35 +00:00
interactive = options.get('interactive')
2015-05-04 10:48:09 +00:00
cert = options.get('cert')
2015-05-04 12:57:41 +00:00
key = options.get('cert_key')
if bool(cert) != bool(key):
2015-05-04 10:48:09 +00:00
raise CommandError("--cert and --cert-key go in tandem")
2015-05-04 12:57:41 +00:00
cert_path = options.get('cert_path')
key_path = options.get('cert_key_path')
2015-05-04 10:48:09 +00:00
2015-05-05 20:44:00 +00:00
run('mkdir -p %s' % os.path.dirname(cert_path))
2015-05-04 12:57:41 +00:00
exists = os.path.isfile(cert_path)
if not override and exists:
self.stdout.write('Your cert and keys are already in place.')
self.stdout.write('Use --override in order to regenerate them.')
return cert_path, key_path
common_name = options.get('cert_common_name') or options.get('server_name') or 'orchestra.lan'
country = options.get('cert_country')
state = options.get('cert_state')
locality = options.get('cert_locality')
org_name = options.get('cert_org_name')
org_unit = options.get('cert_org_unit')
email = options.get('cert_email')
if interactive:
msg = ('-----\n'
'You are about to be asked to enter information that\n'
'will be incorporated\n'
'into your certificate request.\n'
'What you are about to enter is what is called a\n'
'Distinguished Name or a DN.\n'
'There are quite a few fields but you can leave some blank\n'
'-----\n')
self.stdout.write(msg)
msg = 'Country Name (2 letter code) [%s]: ' % country
country = input(msg) or country
msg = 'State or Province Name (full name) [%s]: ' % state
state = input(msg) or state
msg = 'Locality Name (eg, city) [%s]: ' % locality
locality = input(msg) or locality
msg = 'Organization Name (eg, company) [%s]: ' % org_name
org_name = input(msg) or org_name
msg = 'Organizational Unit Name (eg, section) [%s]: ' % org_unit
org_unit = input(msg) or org_unit
msg = 'Email Address [%s]: ' % email
email = input(msg) or email
self.stdout.write('Common Name: %s' % common_name)
subject = {
'C': country,
'S': state,
'L': locality,
'O': org_name,
'OU': org_unit,
'Email': email,
'CN': common_name,
}
2014-05-08 16:59:35 +00:00
context = {
2015-05-04 12:57:41 +00:00
'subject': ''.join(('/%s=%s' % (k,v) for k,v in subject.items())),
'key_path': key_path,
'cert_path': cert_path,
2015-10-01 19:02:39 +00:00
'cert_root': os.path.dirname(cert_path),
2015-05-04 12:57:41 +00:00
}
self.stdout.write('writing new cert to \'%s\'' % cert_path)
self.stdout.write('writing new cert key to \'%s\'' % key_path)
2015-10-01 19:02:39 +00:00
run(textwrap.dedent("""\
openssl req -x509 -nodes -days 365 -newkey rsa:4096 -keyout %(key_path)s -out %(cert_path)s -subj "%(subject)s"
chown --reference=%(cert_root)s %(cert_path)s %(key_path)s\
""") % context, display=True
)
2015-05-04 12:57:41 +00:00
return cert_path, key_path
@check_root
def handle(self, *args, **options):
user = options.get('user')
if not user:
raise CommandError("System user for running uwsgi must be provided.")
cert_path, key_path = self.generate_certificate(**options)
server_name = options.get('server_name')
context = {
'cert_path': cert_path,
'key_path': key_path,
2015-04-27 14:54:17 +00:00
'project_name': paths.get_project_name(),
'project_dir': paths.get_project_dir(),
'site_dir': paths.get_site_dir(),
2014-05-08 16:59:35 +00:00
'static_root': settings.STATIC_ROOT,
'static_url': (settings.STATIC_URL or '/static').rstrip('/'),
2015-05-04 12:57:41 +00:00
'user': user,
'group': options.get('group') or user,
2014-05-08 16:59:35 +00:00
'home': expanduser("~%s" % options.get('user')),
2015-05-04 12:57:41 +00:00
'processes': int(options.get('processes')),
'server_name': 'server_name %s' % server_name if server_name else ''
}
2014-05-08 16:59:35 +00:00
2015-04-27 14:54:17 +00:00
nginx_conf = textwrap.dedent("""\
server {
listen 80;
listen [::]:80 ipv6only=on;
2015-05-04 12:57:41 +00:00
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
# listen [::]:443 ssl; # add SSL support to IPv6 address
%(server_name)s
ssl_certificate %(cert_path)s;
ssl_certificate_key %(key_path)s;
2015-04-27 14:54:17 +00:00
rewrite ^/$ /admin/;
2015-05-04 12:57:41 +00:00
client_max_body_size 16m;
2015-04-27 14:54:17 +00:00
location / {
uwsgi_pass unix:///var/run/uwsgi/app/%(project_name)s/socket;
include uwsgi_params;
}
location %(static_url)s {
2015-04-27 14:54:17 +00:00
alias %(static_root)s;
expires 30d;
}
}
"""
) % context
2014-05-08 16:59:35 +00:00
2015-04-27 14:54:17 +00:00
uwsgi_conf = textwrap.dedent("""\
[uwsgi]
2016-01-11 15:18:25 +00:00
plugins = python3
2015-05-04 12:57:41 +00:00
chdir = %(site_dir)s
module = %(project_name)s.wsgi
master = true
2016-01-11 15:18:25 +00:00
workers = %(processes)d
2015-05-04 12:57:41 +00:00
chmod-socket = 664
stats = /run/uwsgi/%%(deb-confnamespace)/%%(deb-confname)/statsocket
uid = %(user)s
gid = %(group)s
env = HOME=%(home)s
touch-reload = %(project_dir)s/wsgi.py
2015-05-09 17:08:45 +00:00
vacuum = true # Remove socket stop
enable-threads = true # Initializes the GIL
max-requests = 500 # Mitigates memory leaks
lazy-apps = true # Don't share database connections
2015-04-27 14:54:17 +00:00
"""
) % context
2014-05-08 16:59:35 +00:00
2015-05-05 20:55:54 +00:00
nginx_file = '/etc/nginx/sites-available/%(project_name)s.conf' % context
2015-05-04 12:57:41 +00:00
if server_name:
context['server_name'] = server_name
nginx_file = '/etc/nginx/sites-available/%(server_name)s.conf' % context
2014-05-08 16:59:35 +00:00
nginx = {
2015-05-04 12:57:41 +00:00
'file': nginx_file,
'conf': nginx_conf
}
2014-05-08 16:59:35 +00:00
uwsgi = {
'file': '/etc/uwsgi/apps-available/%(project_name)s.ini' % context,
2015-05-04 12:57:41 +00:00
'conf': uwsgi_conf
}
2014-05-08 16:59:35 +00:00
2015-06-12 13:01:56 +00:00
interactive = options.get('interactive')
2014-05-08 16:59:35 +00:00
for extra_context in (nginx, uwsgi):
context.update(extra_context)
2015-06-12 13:01:56 +00:00
diff = run("cat << 'EOF' | diff - %(file)s\n%(conf)s\nEOF" % context, valid_codes=(0,1,2))
2015-05-09 17:08:45 +00:00
if diff.exit_code == 2:
2014-05-08 16:59:35 +00:00
# File does not exist
2015-06-12 13:01:56 +00:00
run("cat << 'EOF' > %(file)s\n%(conf)s\nEOF" % context, display=True)
2015-05-09 17:08:45 +00:00
elif diff.exit_code == 1:
2014-05-08 16:59:35 +00:00
# File is different, save the old one
if interactive:
2015-08-31 11:58:59 +00:00
if not confirm("\n\nFile %(file)s be updated, do you like to overide "
"it? (yes/no): " % context):
2015-08-31 11:58:59 +00:00
return
2015-10-01 19:02:39 +00:00
run(textwrap.dedent("""\
cp %(file)s %(file)s.save
cat << 'EOF' > %(file)s
%(conf)s
EOF""") % context, display=True
)
2014-05-08 16:59:35 +00:00
self.stdout.write("\033[1;31mA new version of %(file)s has been installed.\n "
"The old version has been placed at %(file)s.save\033[m" % context)
2015-05-04 12:57:41 +00:00
if server_name:
2015-05-05 20:55:54 +00:00
run('ln -s /etc/nginx/sites-available/%(server_name)s.conf /etc/nginx/sites-enabled/' % context,
2015-05-09 17:08:45 +00:00
valid_codes=[0,1], display=True)
2015-05-05 20:55:54 +00:00
else:
2015-10-01 18:08:00 +00:00
run('rm -f /etc/nginx/sites-enabled/default')
2015-05-05 20:55:54 +00:00
run('ln -s /etc/nginx/sites-available/%(project_name)s.conf /etc/nginx/sites-enabled/' % context,
2015-05-09 17:08:45 +00:00
valid_codes=[0,1], display=True)
2015-05-05 20:55:54 +00:00
run('ln -s /etc/uwsgi/apps-available/%(project_name)s.ini /etc/uwsgi/apps-enabled/' % context,
2015-05-09 17:08:45 +00:00
valid_codes=[0,1], display=True)
2014-05-08 16:59:35 +00:00
2015-04-27 14:54:17 +00:00
rotate = textwrap.dedent("""\
/var/log/nginx/*.log {
daily
missingok
rotate 30
compress
delaycompress
notifempty
create 640 root adm
sharedscripts
postrotate
[ ! -f /var/run/nginx.pid ] || kill -USR1 `cat /var/run/nginx.pid`
endscript
}"""
)
2015-05-04 12:57:41 +00:00
run("echo '%s' > /etc/logrotate.d/nginx" % rotate, display=True)
2014-05-08 16:59:35 +00:00
# Allow nginx to write to uwsgi socket
2015-05-04 12:57:41 +00:00
run('adduser www-data %(group)s' % context, display=True)