Merge pull request #1398 from SchrodingersGat/django-q

Django q
This commit is contained in:
Oliver 2021-04-11 15:38:20 +10:00 committed by GitHub
commit a2ff3e3474
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 989 additions and 169 deletions

View File

@ -16,6 +16,8 @@ jobs:
INVENTREE_DB_NAME: './test_db.sqlite' INVENTREE_DB_NAME: './test_db.sqlite'
INVENTREE_DB_ENGINE: django.db.backends.sqlite3 INVENTREE_DB_ENGINE: django.db.backends.sqlite3
INVENTREE_DEBUG: info INVENTREE_DEBUG: info
INVENTREE_MEDIA_ROOT: ./media
INVENTREE_STATIC_ROOT: ./static
steps: steps:
- name: Checkout Code - name: Checkout Code

18
.github/workflows/docker_build.yaml vendored Normal file
View File

@ -0,0 +1,18 @@
# Test that the docker file builds correctly
name: Docker
on: ["push", "pull_request"]
jobs:
docker:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Build Server Image
run: cd docker/inventree && docker build . --tag inventree:$(date +%s)
- name: Build nginx Image
run: cd docker/nginx && docker build . --tag nxinx:$(date +%s)

38
.github/workflows/docker_publish.yaml vendored Normal file
View File

@ -0,0 +1,38 @@
# Publish docker images to dockerhub
name: Docker Publish
on:
release:
types: [published]
jobs:
server_image:
name: Push InvenTree web server image to dockerhub
runs-on: ubuntu-latest
steps:
- name: Check out repo
uses: actions/checkout@v2
- name: Push to Docker Hub
uses: docker/build-push-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
repository: inventree/inventree
tag_with_ref: true
context: docker/inventree
nginx_image:
name: Push InvenTree nginx image to dockerhub
runs-on: ubuntu-latest
steps:
- name: Check out repo
uses: actions/checkout@v2
- name: Push to Docker Hub
uses: docker/build-push-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
repository: inventree/nginx
tag_with_ref: true
context: docker/nginx

View File

@ -1,46 +0,0 @@
name: MariaDB
on: ["push", "pull_request"]
jobs:
test:
runs-on: ubuntu-latest
env:
# Database backend configuration
INVENTREE_DB_ENGINE: django.db.backends.mysql
INVENTREE_DB_NAME: inventree
INVENTREE_DB_USER: root
INVENTREE_DB_PASSWORD: password
INVENTREE_DB_HOST: '127.0.0.1'
INVENTREE_DB_PORT: 3306
INVENTREE_DEBUG: info
services:
mariadb:
image: mariadb:latest
env:
MYSQL_ALLOW_EMPTY_PASSWORD: yes
MYSQL_DATABASE: inventree
MYSQL_USER: inventree
MYSQL_PASSWORD: password
MYSQL_ROOT_PASSWORD: password
ports:
- 3306:3306
steps:
- name: Checkout Code
uses: actions/checkout@v2
- name: Setup Python
uses: actions/setup-python@v2
with:
python-version: 3.7
- name: Install Dependencies
run: |
sudo apt-get install mysql-server libmysqlclient-dev
pip3 install invoke
pip3 install mysqlclient
invoke install
- name: Run Tests
run: invoke test

View File

@ -18,6 +18,8 @@ jobs:
INVENTREE_DB_HOST: '127.0.0.1' INVENTREE_DB_HOST: '127.0.0.1'
INVENTREE_DB_PORT: 3306 INVENTREE_DB_PORT: 3306
INVENTREE_DEBUG: info INVENTREE_DEBUG: info
INVENTREE_MEDIA_ROOT: ./media
INVENTREE_STATIC_ROOT: ./static
services: services:
mysql: mysql:

View File

@ -18,6 +18,8 @@ jobs:
INVENTREE_DB_HOST: '127.0.0.1' INVENTREE_DB_HOST: '127.0.0.1'
INVENTREE_DB_PORT: 5432 INVENTREE_DB_PORT: 5432
INVENTREE_DEBUG: info INVENTREE_DEBUG: info
INVENTREE_MEDIA_ROOT: ./media
INVENTREE_STATIC_ROOT: ./static
services: services:
postgres: postgres:

View File

@ -19,11 +19,12 @@ from rest_framework.views import APIView
from .views import AjaxView from .views import AjaxView
from .version import inventreeVersion, inventreeApiVersion, inventreeInstanceName from .version import inventreeVersion, inventreeApiVersion, inventreeInstanceName
from .status import is_worker_running
from plugins import plugins as inventree_plugins from plugins import plugins as inventree_plugins
logger = logging.getLogger(__name__) logger = logging.getLogger("inventree")
logger.info("Loading action plugins...") logger.info("Loading action plugins...")
@ -44,6 +45,7 @@ class InfoView(AjaxView):
'version': inventreeVersion(), 'version': inventreeVersion(),
'instance': inventreeInstanceName(), 'instance': inventreeInstanceName(),
'apiVersion': inventreeApiVersion(), 'apiVersion': inventreeApiVersion(),
'worker_running': is_worker_running(),
} }
return JsonResponse(data) return JsonResponse(data)

View File

@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-
import logging
from django.apps import AppConfig
from django.core.exceptions import AppRegistryNotReady
import InvenTree.tasks
logger = logging.getLogger("inventree")
class InvenTreeConfig(AppConfig):
name = 'InvenTree'
def ready(self):
self.start_background_tasks()
def start_background_tasks(self):
try:
from django_q.models import Schedule
except (AppRegistryNotReady):
return
logger.info("Starting background tasks...")
InvenTree.tasks.schedule_task(
'InvenTree.tasks.delete_successful_tasks',
schedule_type=Schedule.DAILY,
)
InvenTree.tasks.schedule_task(
'InvenTree.tasks.check_for_updates',
schedule_type=Schedule.DAILY
)
InvenTree.tasks.schedule_task(
'InvenTree.tasks.heartbeat',
schedule_type=Schedule.MINUTES,
minutes=15
)

View File

@ -30,10 +30,22 @@ def health_status(request):
request._inventree_health_status = True request._inventree_health_status = True
return { status = {
"system_healthy": InvenTree.status.check_system_health(), 'django_q_running': InvenTree.status.is_worker_running(),
} }
all_healthy = True
for k in status.keys():
if status[k] is not True:
all_healthy = False
status['system_healthy'] = all_healthy
status['up_to_date'] = InvenTree.version.isInvenTreeUpToDate()
return status
def status_codes(request): def status_codes(request):
""" """

View File

@ -0,0 +1,42 @@
"""
Custom management command, wait for the database to be ready!
"""
from django.core.management.base import BaseCommand
from django.db import connection
from django.db.utils import OperationalError, ImproperlyConfigured
import time
class Command(BaseCommand):
"""
django command to pause execution until the database is ready
"""
def handle(self, *args, **kwargs):
self.stdout.write("Waiting for database...")
connected = False
while not connected:
time.sleep(5)
try:
connection.ensure_connection()
connected = True
except OperationalError as e:
self.stdout.write(f"Could not connect to database: {e}")
except ImproperlyConfigured as e:
self.stdout.write(f"Improperly configured: {e}")
else:
if not connection.is_usable():
self.stdout.write("Database configuration is not usable")
if connected:
self.stdout.write("Database connection sucessful!")

View File

@ -8,7 +8,7 @@ import operator
from rest_framework.authtoken.models import Token from rest_framework.authtoken.models import Token
logger = logging.getLogger(__name__) logger = logging.getLogger("inventree")
class AuthRequiredMiddleware(object): class AuthRequiredMiddleware(object):

View File

@ -13,6 +13,9 @@ database setup in this file.
import logging import logging
import os import os
import random
import string
import shutil
import sys import sys
import tempfile import tempfile
from datetime import datetime from datetime import datetime
@ -46,14 +49,31 @@ def get_setting(environment_var, backup_val, default_value=None):
return default_value return default_value
# Determine if we are running in "test" mode e.g. "manage.py test"
TESTING = 'test' in sys.argv
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
cfg_filename = os.path.join(BASE_DIR, 'config.yaml') # Specify where the "config file" is located.
# By default, this is 'config.yaml'
cfg_filename = os.getenv('INVENTREE_CONFIG_FILE')
if cfg_filename:
cfg_filename = cfg_filename.strip()
cfg_filename = os.path.abspath(cfg_filename)
else:
# Config file is *not* specified - use the default
cfg_filename = os.path.join(BASE_DIR, 'config.yaml')
if not os.path.exists(cfg_filename): if not os.path.exists(cfg_filename):
print("Error: config.yaml not found") print("InvenTree configuration file 'config.yaml' not found - creating default file")
sys.exit(-1)
cfg_template = os.path.join(BASE_DIR, "config_template.yaml")
shutil.copyfile(cfg_template, cfg_filename)
print(f"Created config file {cfg_filename}")
with open(cfg_filename, 'r') as cfg: with open(cfg_filename, 'r') as cfg:
CONFIG = yaml.safe_load(cfg) CONFIG = yaml.safe_load(cfg)
@ -94,7 +114,18 @@ LOGGING = {
} }
# Get a logger instance for this setup file # Get a logger instance for this setup file
logger = logging.getLogger(__name__) logger = logging.getLogger("inventree")
"""
Specify a secret key to be used by django.
Following options are tested, in descending order of preference:
A) Check for environment variable INVENTREE_SECRET_KEY => Use raw key data
B) Check for environment variable INVENTREE_SECRET_KEY_FILE => Load key data from file
C) Look for default key file "secret_key.txt"
d) Create "secret_key.txt" if it does not exist
"""
if os.getenv("INVENTREE_SECRET_KEY"): if os.getenv("INVENTREE_SECRET_KEY"):
# Secret key passed in directly # Secret key passed in directly
@ -105,15 +136,22 @@ else:
key_file = os.getenv("INVENTREE_SECRET_KEY_FILE") key_file = os.getenv("INVENTREE_SECRET_KEY_FILE")
if key_file: if key_file:
if os.path.isfile(key_file): key_file = os.path.abspath(key_file)
logger.info("SECRET_KEY loaded by INVENTREE_SECRET_KEY_FILE")
else:
logger.error(f"Secret key file {key_file} not found")
exit(-1)
else: else:
# default secret key location # default secret key location
key_file = os.path.join(BASE_DIR, "secret_key.txt") key_file = os.path.join(BASE_DIR, "secret_key.txt")
logger.info(f"SECRET_KEY loaded from {key_file}") key_file = os.path.abspath(key_file)
if not os.path.exists(key_file):
logger.info(f"Generating random key file at '{key_file}'")
# Create a random key file
with open(key_file, 'w') as f:
options = string.digits + string.ascii_letters + string.punctuation
key = ''.join([random.choice(options) for i in range(100)])
f.write(key)
logger.info(f"Loading SECRET_KEY from '{key_file}'")
try: try:
SECRET_KEY = open(key_file, "r").read().strip() SECRET_KEY = open(key_file, "r").read().strip()
except Exception: except Exception:
@ -144,7 +182,7 @@ STATIC_URL = '/static/'
STATIC_ROOT = os.path.abspath( STATIC_ROOT = os.path.abspath(
get_setting( get_setting(
'INVENTREE_STATIC_ROOT', 'INVENTREE_STATIC_ROOT',
CONFIG.get('static_root', os.path.join(BASE_DIR, 'static')) CONFIG.get('static_root', '/home/inventree/static')
) )
) )
@ -162,7 +200,7 @@ MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.abspath( MEDIA_ROOT = os.path.abspath(
get_setting( get_setting(
'INVENTREE_MEDIA_ROOT', 'INVENTREE_MEDIA_ROOT',
CONFIG.get('media_root', os.path.join(BASE_DIR, 'media')) CONFIG.get('media_root', '/home/inventree/data/media')
) )
) )
@ -194,6 +232,7 @@ INSTALLED_APPS = [
'report.apps.ReportConfig', 'report.apps.ReportConfig',
'stock.apps.StockConfig', 'stock.apps.StockConfig',
'users.apps.UsersConfig', 'users.apps.UsersConfig',
'InvenTree.apps.InvenTreeConfig', # InvenTree app runs last
# Third part add-ons # Third part add-ons
'django_filters', # Extended filter functionality 'django_filters', # Extended filter functionality
@ -211,6 +250,7 @@ INSTALLED_APPS = [
'djmoney', # django-money integration 'djmoney', # django-money integration
'djmoney.contrib.exchange', # django-money exchange rates 'djmoney.contrib.exchange', # django-money exchange rates
'error_report', # Error reporting in the admin interface 'error_report', # Error reporting in the admin interface
'django_q',
] ]
MIDDLEWARE = CONFIG.get('middleware', [ MIDDLEWARE = CONFIG.get('middleware', [
@ -285,6 +325,18 @@ REST_FRAMEWORK = {
WSGI_APPLICATION = 'InvenTree.wsgi.application' WSGI_APPLICATION = 'InvenTree.wsgi.application'
# django-q configuration
Q_CLUSTER = {
'name': 'InvenTree',
'workers': 4,
'timeout': 90,
'retry': 120,
'queue_limit': 50,
'bulk': 10,
'orm': 'default',
'sync': False,
}
# Markdownx configuration # Markdownx configuration
# Ref: https://neutronx.github.io/django-markdownx/customization/ # Ref: https://neutronx.github.io/django-markdownx/customization/
MARKDOWNX_MEDIA_PATH = datetime.now().strftime('markdownx/%Y/%m/%d') MARKDOWNX_MEDIA_PATH = datetime.now().strftime('markdownx/%Y/%m/%d')
@ -331,6 +383,9 @@ logger.info("Configuring database backend:")
# Extract database configuration from the config.yaml file # Extract database configuration from the config.yaml file
db_config = CONFIG.get('database', {}) db_config = CONFIG.get('database', {})
if not db_config:
db_config = {}
# Environment variables take preference over config file! # Environment variables take preference over config file!
db_keys = ['ENGINE', 'NAME', 'USER', 'PASSWORD', 'HOST', 'PORT'] db_keys = ['ENGINE', 'NAME', 'USER', 'PASSWORD', 'HOST', 'PORT']
@ -350,7 +405,7 @@ reqiured_keys = ['ENGINE', 'NAME']
for key in reqiured_keys: for key in reqiured_keys:
if key not in db_config: if key not in db_config:
error_msg = f'Missing required database configuration value {key} in config.yaml' error_msg = f'Missing required database configuration value {key}'
logger.error(error_msg) logger.error(error_msg)
print('Error: ' + error_msg) print('Error: ' + error_msg)
@ -386,11 +441,6 @@ CACHES = {
'default': { 'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
}, },
'qr-code': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'LOCATION': 'qr-code-cache',
'TIMEOUT': 3600
}
} }
# Password validation # Password validation
@ -449,13 +499,19 @@ LOCALE_PATHS = (
os.path.join(BASE_DIR, 'locale/'), os.path.join(BASE_DIR, 'locale/'),
) )
TIME_ZONE = CONFIG.get('timezone', 'UTC') TIME_ZONE = get_setting(
'INVENTREE_TIMEZONE',
CONFIG.get('timezone', 'UTC')
)
USE_I18N = True USE_I18N = True
USE_L10N = True USE_L10N = True
USE_TZ = True # Do not use native timezone support in "test" mode
# It generates a *lot* of cruft in the logs
if not TESTING:
USE_TZ = True
DATE_INPUT_FORMATS = [ DATE_INPUT_FORMATS = [
"%Y-%m-%d", "%Y-%m-%d",

View File

@ -1,13 +1,46 @@
""" """
Provides system status functionality checks. Provides system status functionality checks.
""" """
# -*- coding: utf-8 -*-
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
import logging import logging
from datetime import datetime, timedelta
from django_q.models import Success
from django_q.monitor import Stat
logger = logging.getLogger("inventree")
logger = logging.getLogger(__name__) def is_worker_running(**kwargs):
"""
Return True if the background worker process is oprational
"""
clusters = Stat.get_all()
if len(clusters) > 0:
# TODO - Introspect on any cluster information
return True
"""
Sometimes Stat.get_all() returns [].
In this case we have the 'heartbeat' task running every 15 minutes.
Check to see if we have a result within the last 20 minutes
"""
now = datetime.now()
past = now - timedelta(minutes=20)
results = Success.objects.filter(
func='InvenTree.tasks.heartbeat',
started__gte=past
)
# If any results are returned, then the background worker is running!
return results.exists()
def check_system_health(**kwargs): def check_system_health(**kwargs):
@ -19,21 +52,11 @@ def check_system_health(**kwargs):
result = True result = True
if not check_celery_worker(**kwargs): if not is_worker_running(**kwargs):
result = False result = False
logger.warning(_("Celery worker check failed")) logger.warning(_("Background worker check failed"))
if not result: if not result:
logger.warning(_("InvenTree system health checks failed")) logger.warning(_("InvenTree system health checks failed"))
return result return result
def check_celery_worker(**kwargs):
"""
Check that a celery worker is running.
"""
# TODO - Checks that the configured celery worker thing is running
return True

View File

@ -0,0 +1,143 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import re
import json
import requests
import logging
from datetime import datetime, timedelta
from django.core.exceptions import AppRegistryNotReady
from django.db.utils import OperationalError, ProgrammingError
logger = logging.getLogger("inventree")
def schedule_task(taskname, **kwargs):
"""
Create a scheduled task.
If the task has already been scheduled, ignore!
"""
# If unspecified, repeat indefinitely
repeats = kwargs.pop('repeats', -1)
kwargs['repeats'] = repeats
try:
from django_q.models import Schedule
except (AppRegistryNotReady):
logger.warning("Could not start background tasks - App registry not ready")
return
try:
# If this task is already scheduled, don't schedule it again
# Instead, update the scheduling parameters
if Schedule.objects.filter(func=taskname).exists():
logger.info(f"Scheduled task '{taskname}' already exists - updating!")
Schedule.objects.filter(func=taskname).update(**kwargs)
else:
logger.info(f"Creating scheduled task '{taskname}'")
Schedule.objects.create(
name=taskname,
func=taskname,
**kwargs
)
except (OperationalError, ProgrammingError):
# Required if the DB is not ready yet
pass
def heartbeat():
"""
Simple task which runs at 5 minute intervals,
so we can determine that the background worker
is actually running.
(There is probably a less "hacky" way of achieving this)?
"""
try:
from django_q.models import Success
logger.warning("Could not perform heartbeat task - App registry not ready")
except AppRegistryNotReady:
return
threshold = datetime.now() - timedelta(minutes=30)
# Delete heartbeat results more than half an hour old,
# otherwise they just create extra noise
heartbeats = Success.objects.filter(
func='InvenTree.tasks.heartbeat',
started__lte=threshold
)
heartbeats.delete()
def delete_successful_tasks():
"""
Delete successful task logs
which are more than a month old.
"""
try:
from django_q.models import Success
except AppRegistryNotReady:
logger.warning("Could not perform 'delete_successful_tasks' - App registry not ready")
return
threshold = datetime.now() - timedelta(days=30)
results = Success.objects.filter(
started__lte=threshold
)
results.delete()
def check_for_updates():
"""
Check if there is an update for InvenTree
"""
try:
import common.models
except AppRegistryNotReady:
# Apps not yet loaded!
return
response = requests.get('https://api.github.com/repos/inventree/inventree/releases/latest')
if not response.status_code == 200:
raise ValueError(f'Unexpected status code from GitHub API: {response.status_code}')
data = json.loads(response.text)
tag = data.get('tag_name', None)
if not tag:
raise ValueError("'tag_name' missing from GitHub response")
match = re.match(r"^.*(\d+)\.(\d+)\.(\d+).*$", tag)
if not len(match.groups()) == 3:
logger.warning(f"Version '{tag}' did not match expected pattern")
return
latest_version = [int(x) for x in match.groups()]
if not len(latest_version) == 3:
raise ValueError(f"Version '{tag}' is not correct format")
logger.info(f"Latest InvenTree version: '{tag}'")
# Save the version to the database
common.models.InvenTreeSetting.set_setting(
'INVENTREE_LATEST_VERSION',
tag,
None
)

View File

@ -0,0 +1,43 @@
"""
Unit tests for task management
"""
from django.test import TestCase
from django_q.models import Schedule
import InvenTree.tasks
class ScheduledTaskTests(TestCase):
"""
Unit tests for scheduled tasks
"""
def get_tasks(self, name):
return Schedule.objects.filter(func=name)
def test_add_task(self):
"""
Ensure that duplicate tasks cannot be added.
"""
task = 'InvenTree.tasks.heartbeat'
self.assertEqual(self.get_tasks(task).count(), 0)
InvenTree.tasks.schedule_task(task, schedule_type=Schedule.MINUTES, minutes=10)
self.assertEqual(self.get_tasks(task).count(), 1)
t = Schedule.objects.get(func=task)
self.assertEqual(t.minutes, 10)
# Attempt to schedule the same task again
InvenTree.tasks.schedule_task(task, schedule_type=Schedule.MINUTES, minutes=5)
self.assertEqual(self.get_tasks(task).count(), 1)
# But the 'minutes' should have been updated
t = Schedule.objects.get(func=task)
self.assertEqual(t.minutes, 5)

View File

@ -7,6 +7,7 @@ from django.core.exceptions import ValidationError
from .validators import validate_overage, validate_part_name from .validators import validate_overage, validate_part_name
from . import helpers from . import helpers
from . import version
from mptt.exceptions import InvalidMove from mptt.exceptions import InvalidMove
@ -269,3 +270,33 @@ class TestSerialNumberExtraction(TestCase):
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
e("10, a, 7-70j", 4) e("10, a, 7-70j", 4)
class TestVersionNumber(TestCase):
"""
Unit tests for version number functions
"""
def test_tuple(self):
v = version.inventreeVersionTuple()
self.assertEqual(len(v), 3)
s = '.'.join([str(i) for i in v])
self.assertTrue(s in version.inventreeVersion())
def test_comparison(self):
"""
Test direct comparison of version numbers
"""
v_a = version.inventreeVersionTuple('1.2.0')
v_b = version.inventreeVersionTuple('1.2.3')
v_c = version.inventreeVersionTuple('1.2.4')
v_d = version.inventreeVersionTuple('2.0.0')
self.assertTrue(v_b > v_a)
self.assertTrue(v_c > v_b)
self.assertTrue(v_d > v_c)
self.assertTrue(v_d > v_a)

View File

@ -4,6 +4,7 @@ Provides information on the current InvenTree version
import subprocess import subprocess
import django import django
import re
import common.models import common.models
@ -23,6 +24,38 @@ def inventreeVersion():
return INVENTREE_SW_VERSION return INVENTREE_SW_VERSION
def inventreeVersionTuple(version=None):
""" Return the InvenTree version string as (maj, min, sub) tuple """
if version is None:
version = INVENTREE_SW_VERSION
match = re.match(r"^.*(\d+)\.(\d+)\.(\d+).*$", str(version))
return [int(g) for g in match.groups()]
def isInvenTreeUpToDate():
"""
Test if the InvenTree instance is "up to date" with the latest version.
A background task periodically queries GitHub for latest version,
and stores it to the database as INVENTREE_LATEST_VERSION
"""
latest = common.models.InvenTreeSetting.get_setting('INVENTREE_LATEST_VERSION', None)
# No record for "latest" version - we must assume we are up to date!
if not latest:
return True
# Extract "tuple" version (Python can directly compare version tuples)
latest_version = inventreeVersionTuple(latest)
inventree_version = inventreeVersionTuple()
return inventree_version >= latest_version
def inventreeApiVersion(): def inventreeApiVersion():
return INVENTREE_API_VERSION return INVENTREE_API_VERSION

View File

@ -12,7 +12,7 @@ from stock.serializers import StockItemSerializer, LocationSerializer
from part.serializers import PartSerializer from part.serializers import PartSerializer
logger = logging.getLogger(__name__) logger = logging.getLogger('inventree')
def hash_barcode(barcode_data): def hash_barcode(barcode_data):

View File

@ -500,7 +500,7 @@ class InvenTreeSetting(models.Model):
create: If True, create a new setting if the specified key does not exist. create: If True, create a new setting if the specified key does not exist.
""" """
if not user.is_staff: if user is not None and not user.is_staff:
return return
try: try:

View File

@ -9,7 +9,7 @@ from django.conf import settings
from PIL import UnidentifiedImageError from PIL import UnidentifiedImageError
logger = logging.getLogger(__name__) logger = logging.getLogger("inventree")
class CompanyConfig(AppConfig): class CompanyConfig(AppConfig):

View File

@ -7,11 +7,9 @@
# with the prefix INVENTREE_DB_ # with the prefix INVENTREE_DB_
# e.g INVENTREE_DB_NAME / INVENTREE_DB_USER / INVENTREE_DB_PASSWORD # e.g INVENTREE_DB_NAME / INVENTREE_DB_USER / INVENTREE_DB_PASSWORD
database: database:
# Default configuration - sqlite filesystem database # Uncomment (and edit) one of the database configurations below,
ENGINE: sqlite3 # or specify database options using environment variables
NAME: '../inventree_default_db.sqlite3'
# For more complex database installations, further parameters are required
# Refer to the django documentation for full list of options # Refer to the django documentation for full list of options
# --- Available options: --- # --- Available options: ---
@ -27,14 +25,22 @@ database:
# --- Example Configuration - sqlite3 --- # --- Example Configuration - sqlite3 ---
# ENGINE: sqlite3 # ENGINE: sqlite3
# NAME: '/path/to/database.sqlite3' # NAME: '/home/inventree/database.sqlite3'
# --- Example Configuration - MySQL --- # --- Example Configuration - MySQL ---
#ENGINE: django.db.backends.mysql #ENGINE: mysql
#NAME: inventree #NAME: inventree
#USER: inventree_username #USER: inventree
#PASSWORD: inventree_password #PASSWORD: inventree_password
#HOST: '127.0.0.1' #HOST: 'localhost'
#PORT: '3306'
# --- Example Configuration - Postgresql ---
#ENGINE: postgresql
#NAME: inventree
#USER: inventree
#PASSWORD: inventree_password
#HOST: 'localhost'
#PORT: '5432' #PORT: '5432'
# Select default system language (default is 'en-us') # Select default system language (default is 'en-us')
@ -43,6 +49,7 @@ language: en-us
# System time-zone (default is UTC) # System time-zone (default is UTC)
# Reference: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones # Reference: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
# Select an option from the "TZ database name" column # Select an option from the "TZ database name" column
# Use the environment variable INVENTREE_TIMEZONE
timezone: UTC timezone: UTC
# List of currencies supported by default. # List of currencies supported by default.
@ -57,6 +64,7 @@ currencies:
- USD - USD
# Set debug to False to run in production mode # Set debug to False to run in production mode
# Use the environment variable INVENTREE_DEBUG
debug: True debug: True
# Set debug_toolbar to True to enable a debugging toolbar for InvenTree # Set debug_toolbar to True to enable a debugging toolbar for InvenTree
@ -65,6 +73,7 @@ debug: True
debug_toolbar: False debug_toolbar: False
# Configure the system logging level # Configure the system logging level
# Use environment variable INVENTREE_LOG_LEVEL
# Options: DEBUG / INFO / WARNING / ERROR / CRITICAL # Options: DEBUG / INFO / WARNING / ERROR / CRITICAL
log_level: WARNING log_level: WARNING
@ -86,13 +95,14 @@ cors:
# - https://sub.example.com # - https://sub.example.com
# MEDIA_ROOT is the local filesystem location for storing uploaded files # MEDIA_ROOT is the local filesystem location for storing uploaded files
# By default, it is stored in a directory named 'inventree_media' local to the InvenTree directory # By default, it is stored under /home/inventree/data/media
# This should be changed for a production installation # Use environment variable INVENTREE_MEDIA_ROOT
media_root: '../inventree_media' media_root: '/home/inventree/data/media'
# STATIC_ROOT is the local filesystem location for storing static files # STATIC_ROOT is the local filesystem location for storing static files
# By default it is stored in a directory named 'inventree_static' local to the InvenTree directory # By default, it is stored under /home/inventree
static_root: '../inventree_static' # Use environment variable INVENTREE_STATIC_ROOT
static_root: '/home/inventree/static'
# Optional URL schemes to allow in URL fields # Optional URL schemes to allow in URL fields
# By default, only the following schemes are allowed: ['http', 'https', 'ftp', 'ftps'] # By default, only the following schemes are allowed: ['http', 'https', 'ftp', 'ftps']
@ -105,7 +115,8 @@ static_root: '../inventree_static'
# Backup options # Backup options
# Set the backup_dir parameter to store backup files in a specific location # Set the backup_dir parameter to store backup files in a specific location
# If unspecified, the local user's temp directory will be used # If unspecified, the local user's temp directory will be used
#backup_dir: '/home/inventree/backup/' # Use environment variable INVENTREE_BACKUP_DIR
backup_dir: '/home/inventree/data/backup/'
# Permit custom authentication backends # Permit custom authentication backends
#authentication_backends: #authentication_backends:

View File

@ -7,7 +7,7 @@ from django.apps import AppConfig
from django.conf import settings from django.conf import settings
logger = logging.getLogger(__name__) logger = logging.getLogger("inventree")
def hashFile(filename): def hashFile(filename):

View File

@ -32,7 +32,7 @@ except OSError as err:
sys.exit(1) sys.exit(1)
logger = logging.getLogger(__name__) logger = logging.getLogger("inventree")
def rename_label(instance, filename): def rename_label(instance, filename):

View File

@ -37,7 +37,7 @@ from InvenTree.views import InvenTreeRoleMixin
from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus, StockStatus from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus, StockStatus
logger = logging.getLogger(__name__) logger = logging.getLogger("inventree")
class PurchaseOrderIndex(InvenTreeRoleMixin, ListView): class PurchaseOrderIndex(InvenTreeRoleMixin, ListView):

View File

@ -9,7 +9,7 @@ from django.conf import settings
from PIL import UnidentifiedImageError from PIL import UnidentifiedImageError
logger = logging.getLogger(__name__) logger = logging.getLogger("inventree")
class PartConfig(AppConfig): class PartConfig(AppConfig):

View File

@ -52,7 +52,7 @@ import common.models
import part.settings as part_settings import part.settings as part_settings
logger = logging.getLogger(__name__) logger = logging.getLogger("inventree")
class PartCategory(InvenTreeTree): class PartCategory(InvenTreeTree):

View File

@ -5,7 +5,7 @@ import logging
import plugins.plugin as plugin import plugins.plugin as plugin
logger = logging.getLogger(__name__) logger = logging.getLogger("inventree")
class ActionPlugin(plugin.InvenTreePlugin): class ActionPlugin(plugin.InvenTreePlugin):

View File

@ -10,7 +10,7 @@ import plugins.action as action
from plugins.action.action import ActionPlugin from plugins.action.action import ActionPlugin
logger = logging.getLogger(__name__) logger = logging.getLogger("inventree")
def iter_namespace(pkg): def iter_namespace(pkg):

View File

@ -6,7 +6,7 @@ from django.apps import AppConfig
from django.conf import settings from django.conf import settings
logger = logging.getLogger(__name__) logger = logging.getLogger("inventree")
class ReportConfig(AppConfig): class ReportConfig(AppConfig):

View File

@ -38,7 +38,7 @@ except OSError as err:
sys.exit(1) sys.exit(1)
logger = logging.getLogger(__name__) logger = logging.getLogger("inventree")
class ReportFileUpload(FileSystemStorage): class ReportFileUpload(FileSystemStorage):

View File

@ -19,11 +19,20 @@
<col width='25'> <col width='25'>
<tr> <tr>
<td><span class='fas fa-hashtag'></span></td> <td><span class='fas fa-hashtag'></span></td>
<td>{% trans "InvenTree Version" %}</td><td><a href="https://github.com/inventree/InvenTree/releases">{% inventree_version %}</a></td> <td>{% trans "InvenTree Version" %}</td>
<td>
<a href="https://github.com/inventree/InvenTree/releases">{% inventree_version %}</a>
{% if up_to_date %}
<span class='label label-green float-right'>{% trans "Up to Date" %}</span>
{% else %}
<span class='label label-red float-right'>{% trans "Update Available" %}</span>
{% endif %}
</td>
</tr> </tr>
<tr> <tr>
<td><span class='fas fa-hashtag'></span></td> <td><span class='fas fa-hashtag'></span></td>
<td>{% trans "Django Version" %}</td><td><a href="https://www.djangoproject.com/">{% django_version %}</a></td> <td>{% trans "Django Version" %}</td>
<td><a href="https://www.djangoproject.com/">{% django_version %}</a></td>
</tr> </tr>
{% inventree_commit_hash as hash %} {% inventree_commit_hash as hash %}
{% if hash %} {% if hash %}
@ -69,4 +78,4 @@
</div> </div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -60,7 +60,9 @@
<li class='dropdown'> <li class='dropdown'>
<a class='dropdown-toggle' data-toggle='dropdown' href="#"> <a class='dropdown-toggle' data-toggle='dropdown' href="#">
{% if not system_healthy %} {% if not system_healthy %}
<span title='{% trans "InvenTree server issues detected" %}' class='fas fa-exclamation-triangle icon-red'></span> <span class='fas fa-exclamation-triangle icon-red'></span>
{% elif not up_to_date %}
<span class='fas fa-info-circle icon-green'></span>
{% endif %} {% endif %}
<span class="fas fa-user"></span> <b>{{ user.get_username }}</b></a> <span class="fas fa-user"></span> <b>{{ user.get_username }}</b></a>
<ul class='dropdown-menu'> <ul class='dropdown-menu'>
@ -78,11 +80,20 @@
{% if system_healthy %} {% if system_healthy %}
<span class='fas fa-server'> <span class='fas fa-server'>
{% else %} {% else %}
<span class='fas fa-exclamation-triangle icon-red'> <span class='fas fa-server icon-red'>
{% endif %} {% endif %}
</span> {% trans "System Information" %} </span> {% trans "System Information" %}
</a></li> </a></li>
<li id='launch-about'><a href='#'><span class="fas fa-info-circle"></span> {% trans "About InvenTree" %}</a></li> <li id='launch-about'>
<a href='#'>
{% if up_to_date %}
<span class="fas fa-info-circle">
{% else %}
<span class='fas fa-info-circle icon-red'>
{% endif %}
</span> {% trans "About InvenTree" %}
</a>
</li>
</ul> </ul>
</li> </li>
</ul> </ul>

View File

@ -13,8 +13,9 @@
<td>{% trans "Instance Name" %}</td> <td>{% trans "Instance Name" %}</td>
<td>{% inventree_instance_name %}</td> <td>{% inventree_instance_name %}</td>
</tr> </tr>
{% if user.is_staff %}
<tr> <tr>
<td><span class='fas fa-exclamation-triangle'></span></td> <td><span class='fas fa-server'></span></td>
<td>{% trans "Server status" %}</td> <td>{% trans "Server status" %}</td>
<td> <td>
{% if system_healthy %} {% if system_healthy %}
@ -24,6 +25,18 @@
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
<tr>
<td><span class='fas fa-tasks'></span></td>
<td>{% trans "Background Worker" %}</td>
<td>
{% if django_q_running %}
<span class='label label-green'>{% trans "Operational" %}</span>
{% else %}
<span class='label label-red'>{% trans "Not running" %}</span>
{% endif %}
</td>
</tr>
{% endif %}
{% if not system_healthy %} {% if not system_healthy %}
{% for issue in system_issues %} {% for issue in system_issues %}

View File

@ -15,7 +15,7 @@ from django.db.models.signals import post_save, post_delete
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger("inventree")
class RuleSet(models.Model): class RuleSet(models.Model):
@ -137,6 +137,13 @@ class RuleSet(models.Model):
'error_report_error', 'error_report_error',
'exchange_rate', 'exchange_rate',
'exchange_exchangebackend', 'exchange_exchangebackend',
# Django-q
'django_q_ormq',
'django_q_failure',
'django_q_task',
'django_q_schedule',
'django_q_success',
] ]
RULE_OPTIONS = [ RULE_OPTIONS = [

View File

@ -1,9 +1,10 @@
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Docker Pulls](https://img.shields.io/docker/pulls/inventree/inventree)](https://hub.docker.com/inventree/inventree)
[![Coverage Status](https://coveralls.io/repos/github/inventree/InvenTree/badge.svg)](https://coveralls.io/github/inventree/InvenTree) [![Coverage Status](https://coveralls.io/repos/github/inventree/InvenTree/badge.svg)](https://coveralls.io/github/inventree/InvenTree)
![PEP](https://github.com/inventree/inventree/actions/workflows/style.yaml/badge.svg) ![PEP](https://github.com/inventree/inventree/actions/workflows/style.yaml/badge.svg)
![Docker Build](https://github.com/inventree/inventree/actions/workflows/docker.yaml/badge.svg)
![SQLite](https://github.com/inventree/inventree/actions/workflows/coverage.yaml/badge.svg) ![SQLite](https://github.com/inventree/inventree/actions/workflows/coverage.yaml/badge.svg)
![MySQL](https://github.com/inventree/inventree/actions/workflows/mysql.yaml/badge.svg) ![MySQL](https://github.com/inventree/inventree/actions/workflows/mysql.yaml/badge.svg)
![MariaDB](https://github.com/inventree/inventree/actions/workflows/mariadb.yaml/badge.svg)
![PostgreSQL](https://github.com/inventree/inventree/actions/workflows/postgresql.yaml/badge.svg) ![PostgreSQL](https://github.com/inventree/inventree/actions/workflows/postgresql.yaml/badge.svg)
<img src="images/logo/inventree.png" alt="InvenTree" width="128"/> <img src="images/logo/inventree.png" alt="InvenTree" width="128"/>

46
deploy/supervisord.conf Normal file
View File

@ -0,0 +1,46 @@
; # Supervisor Config File
; Example configuration file for running InvenTree using supervisor
; There are two separate processes which must be managed:
;
; ## Web Server
; The InvenTree server must be launched and managed as a process
; The recommended way to handle the web server is to use gunicorn
;
; ## Background Tasks
; A background task manager processes long-running and periodic tasks
; InvenTree uses django-q for this purpose
[supervisord]
; Change this path if log files are stored elsewhere
logfile=/home/inventree/log/supervisor.log
user=inventree
[supervisorctl]
[inet_http_server]
port = 127.0.0.1:9001
; InvenTree Web Server Process
[program:inventree-server]
user=inventree
directory=/home/inventree/src/InvenTree
command=/home/inventree/env/bin/gunicorn -c gunicorn.conf.py InvenTree.wsgi
startsecs=10
autostart=true
autorestart=true
startretries=3
; Change these paths if log files are stored elsewhere
stderr_logfile=/home/inventree/log/server.err.log
stdout_logfile=/home/inventree/log/server.out.log
; InvenTree Background Worker Process
[program:inventree-cluster]
user=inventree
directory=/home/inventree/src/InvenTree
command=/home/inventree/env/bin/python manage.py qcluster
startsecs=10
autostart=true
autorestart=true
; Change these paths if log files are stored elsewhere
stderr_logfile=/home/inventree/log/cluster.err.log
stdout_logfile=/home/inventree/log/cluster.out.log

100
docker/docker-compose.yml Normal file
View File

@ -0,0 +1,100 @@
version: "3.8"
# Docker compose recipe for InvenTree
# - Runs PostgreSQL as the database backend
# - Runs Gunicorn as the web server
# - Runs nginx as a reverse proxy
# - Runs the background worker process
# ---------------------------------
# IMPORTANT - READ BEFORE STARTING!
# ---------------------------------
# Before running, ensure that you change the "/path/to/data" directory,
# specified in the "volumes" section at the end of this file.
# This path determines where the InvenTree data will be stored!
services:
# Database service
# Use PostgreSQL as the database backend
# Note: this can be changed to a different backend,
# just make sure that you change the INVENTREE_DB_xxx vars below
db:
container_name: db
image: postgres
ports:
- 5432/tcp
environment:
- PGDATA=/var/lib/postgresql/data/pgdb
- POSTGRES_USER=pguser
- POSTGRES_PASSWORD=pgpassword
volumes:
- data:/var/lib/postgresql/data/
restart: unless-stopped
# InvenTree web server services
# Uses gunicorn as the web server
inventree:
container_name: server
image: inventree/inventree:latest
expose:
- 8080
depends_on:
- db
volumes:
- data:/home/inventree/data
- static:/home/inventree/static
environment:
- INVENTREE_DB_ENGINE=postgresql
- INVENTREE_DB_NAME=inventree
- INVENTREE_DB_USER=pguser
- INVENTREE_DB_PASSWORD=pgpassword
- INVENTREE_DB_PORT=5432
- INVENTREE_DB_HOST=db
restart: unless-stopped
# nginx acts as a reverse proxy
# static files are served by nginx
# web requests are redirected to gunicorn
nginx:
container_name: nginx
image: inventree/nginx:latest
depends_on:
- inventree
ports:
# Change "1337" to the port where you want InvenTree web server to be available
- 1337:80
volumes:
- static:/home/inventree/static
# background worker process handles long-running or periodic tasks
worker:
container_name: worker
image: inventree/inventree:latest
entrypoint: ./start_worker.sh
depends_on:
- db
- inventree
volumes:
- data:/home/inventree/data
- static:/home/inventree/static
environment:
- INVENTREE_DB_ENGINE=postgresql
- INVENTREE_DB_NAME=inventree
- INVENTREE_DB_USER=pguser
- INVENTREE_DB_PASSWORD=pgpassword
- INVENTREE_DB_PORT=5432
- INVENTREE_DB_HOST=db
restart: unless-stopped
volumes:
# Static files, shared between containers
static:
# Persistent data, stored externally
data:
driver: local
driver_opts:
type: none
o: bind
# This directory specified where InvenTree data are stored "outside" the docker containers
# Change this path to a local system path where you want InvenTree data stored
device: /path/to/data

View File

@ -0,0 +1,95 @@
FROM python:alpine as production
# GitHub source
ARG repository="https://github.com/inventree/InvenTree.git"
ARG branch="master"
ENV PYTHONUNBUFFERED 1
# InvenTree key settings
ENV INVENTREE_HOME="/home/inventree"
# GitHub settings
ENV INVENTREE_REPO="${repository}"
ENV INVENTREE_BRANCH="${branch}"
ENV INVENTREE_LOG_LEVEL="INFO"
# InvenTree paths
ENV INVENTREE_SRC_DIR="${INVENTREE_HOME}/src"
ENV INVENTREE_MNG_DIR="${INVENTREE_SRC_DIR}/InvenTree"
ENV INVENTREE_DATA_DIR="${INVENTREE_HOME}/data"
ENV INVENTREE_STATIC_ROOT="${INVENTREE_HOME}/static"
ENV INVENTREE_MEDIA_ROOT="${INVENTREE_DATA_DIR}/media"
ENV INVENTREE_BACKUP_DIR="${INVENTREE_DATA_DIR}/backup"
ENV INVENTREE_CONFIG_FILE="${INVENTREE_DATA_DIR}/config.yaml"
ENV INVENTREE_SECRET_KEY_FILE="${INVENTREE_DATA_DIR}/secret_key.txt"
# Pass DB configuration through as environment variables
ENV INVENTREE_DB_ENGINE="${INVENTREE_DB_ENGINE}"
ENV INVENTREE_DB_NAME="${INVENTREE_DB_NAME}"
ENV INVENTREE_DB_HOST="${INVENTREE_DB_HOST}"
ENV INVENTREE_DB_PORT="${INVENTREE_DB_PORT}"
ENV INVENTREE_DB_USER="${INVENTREE_DB_USER}"
ENV INVENTREE_DB_PASSWORD="${INVENTREE_DB_PASSWORD}"
LABEL org.label-schema.schema-version="1.0" \
org.label-schema.build-date=${DATE} \
org.label-schema.vendor="inventree" \
org.label-schema.name="inventree/inventree" \
org.label-schema.url="https://hub.docker.com/r/inventree/inventree" \
org.label-schema.version=${INVENTREE_VERSION} \
org.label-schema.vcs-url=${INVENTREE_REPO} \
org.label-schema.vcs-branch=${BRANCH} \
org.label-schema.vcs-ref=${COMMIT}
# Create user account
RUN addgroup -S inventreegroup && adduser -S inventree -G inventreegroup
WORKDIR ${INVENTREE_HOME}
RUN mkdir ${INVENTREE_STATIC_ROOT}
# Install required system packages
RUN apk add --no-cache git make bash \
gcc libgcc g++ libstdc++ \
libjpeg-turbo libjpeg-turbo-dev jpeg jpeg-dev \
libffi libffi-dev \
zlib zlib-dev
RUN apk add --no-cache cairo cairo-dev pango pango-dev
RUN apk add --no-cache fontconfig ttf-droid ttf-liberation ttf-dejavu ttf-opensans ttf-ubuntu-font-family font-croscore font-noto
RUN apk add --no-cache python3
RUN apk add --no-cache postgresql-contrib postgresql-dev libpq
RUN apk add --no-cache mariadb-connector-c mariadb-dev
# Create required directories
#RUN mkdir ${INVENTREE_DATA_DIR}}/media ${INVENTREE_HOME}/static ${INVENTREE_HOME}/backup
# Install required python packages
RUN pip install --upgrade pip setuptools wheel
RUN pip install --no-cache-dir -U invoke
RUN pip install --no-cache-dir -U psycopg2 mysqlclient pgcli mariadb
RUN pip install --no-cache-dir -U gunicorn
# Clone source code
RUN echo "Downloading InvenTree from ${INVENTREE_REPO}"
RUN git clone --branch ${INVENTREE_BRANCH} --depth 1 ${INVENTREE_REPO} ${INVENTREE_SRC_DIR}
# Install InvenTree packages
RUN pip install --no-cache-dir -U -r ${INVENTREE_SRC_DIR}/requirements.txt
# Copy gunicorn config file
COPY gunicorn.conf.py ${INVENTREE_HOME}/gunicorn.conf.py
# Copy startup scripts
COPY start_server.sh ${INVENTREE_SRC_DIR}/start_server.sh
COPY start_worker.sh ${INVENTREE_SRC_DIR}/start_worker.sh
RUN chmod 755 ${INVENTREE_SRC_DIR}/start_server.sh
RUN chmod 755 ${INVENTREE_SRC_DIR}/start_worker.sh
# exec commands should be executed from the "src" directory
WORKDIR ${INVENTREE_SRC_DIR}
# Let us begin
CMD ["bash", "./start_server.sh"]

View File

@ -0,0 +1,6 @@
import multiprocessing
workers = multiprocessing.cpu_count() * 2 + 1
max_requests = 1000
max_requests_jitter = 50

View File

@ -0,0 +1,46 @@
#!/bin/sh
# Create required directory structure (if it does not already exist)
if [[ ! -d "$INVENTREE_STATIC_ROOT" ]]; then
echo "Creating directory $INVENTREE_STATIC_ROOT"
mkdir $INVENTREE_STATIC_ROOT
fi
if [[ ! -d "$INVENTREE_MEDIA_ROOT" ]]; then
echo "Creating directory $INVENTREE_MEDIA_ROOT"
mkdir $INVENTREE_MEDIA_ROOT
fi
if [[ ! -d "$INVENTREE_BACKUP_DIR" ]]; then
echo "Creating directory $INVENTREE_BACKUP_DIR"
mkdir $INVENTREE_BACKUP_DIR
fi
# Check if "config.yaml" has been copied into the correct location
if test -f "$INVENTREE_CONFIG_FILE"; then
echo "$INVENTREE_CONFIG_FILE exists - skipping"
else
echo "Copying config file to $INVENTREE_CONFIG_FILE"
cp $INVENTREE_SRC_DIR/InvenTree/config_template.yaml $INVENTREE_CONFIG_FILE
fi
echo "Starting InvenTree server..."
# Wait for the database to be ready
cd $INVENTREE_MNG_DIR
python manage.py wait_for_db
sleep 10
echo "Running InvenTree database migrations and collecting static files..."
# We assume at this stage that the database is up and running
# Ensure that the database schema are up to date
python manage.py check || exit 1
python manage.py migrate --noinput || exit 1
python manage.py migrate --run-syncdb || exit 1
python manage.py collectstatic --noinput || exit 1
python manage.py clearsessions || exit 1
# Now we can launch the server
gunicorn -c $INVENTREE_HOME/gunicorn.conf.py InvenTree.wsgi -b 0.0.0.0:8080

View File

@ -0,0 +1,14 @@
#!/bin/sh
echo "Starting InvenTree worker..."
sleep 5
# Wait for the database to be ready
cd $INVENTREE_MNG_DIR
python manage.py wait_for_db
sleep 10
# Now we can launch the background worker process
python manage.py qcluster

14
docker/nginx/Dockerfile Normal file
View File

@ -0,0 +1,14 @@
FROM nginx:1.19.0-alpine
# Create user account
RUN addgroup -S inventreegroup && adduser -S inventree -G inventreegroup
ENV HOME=/home/inventree
WORKDIR $HOME
# Create the "static" volume directory
RUN mkdir $HOME/static
RUN rm /etc/nginx/conf.d/default.conf
COPY nginx.conf /etc/nginx/conf.d

21
docker/nginx/nginx.conf Normal file
View File

@ -0,0 +1,21 @@
upstream inventree {
server inventree:8080;
}
server {
listen 80;
location / {
proxy_pass http://inventree;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_redirect off;
client_max_body_size 100M;
}
location /static/ {
alias /home/inventree/static/;
}
}

View File

@ -1,3 +1,4 @@
invoke>=1.4.0 # Invoke build tool
wheel>=0.34.2 # Wheel wheel>=0.34.2 # Wheel
Django==3.0.7 # Django package Django==3.0.7 # Django package
pillow==8.1.1 # Image manipulation pillow==8.1.1 # Image manipulation
@ -30,5 +31,7 @@ django-error-report==0.2.0 # Error report viewer for the admin interface
django-test-migrations==1.1.0 # Unit testing for database migrations django-test-migrations==1.1.0 # Unit testing for database migrations
python-barcode[images]==0.13.1 # Barcode generator python-barcode[images]==0.13.1 # Barcode generator
qrcode[pil]==6.1 # QR code generator qrcode[pil]==6.1 # QR code generator
django-q==1.3.4 # Background task scheduling
gunicorn>=20.0.4 # Gunicorn web server
inventree # Install the latest version of the InvenTree API python library inventree # Install the latest version of the InvenTree API python library

View File

@ -1,13 +1,15 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from invoke import task
from shutil import copyfile from shutil import copyfile
import random
import string
import os import os
import sys import sys
try:
from invoke import ctask as task
except:
from invoke import task
def apps(): def apps():
""" """
Returns a list of installed apps Returns a list of installed apps
@ -27,6 +29,7 @@ def apps():
'users', 'users',
] ]
def localDir(): def localDir():
""" """
Returns the directory of *THIS* file. Returns the directory of *THIS* file.
@ -35,6 +38,7 @@ def localDir():
""" """
return os.path.dirname(os.path.abspath(__file__)) return os.path.dirname(os.path.abspath(__file__))
def managePyDir(): def managePyDir():
""" """
Returns the directory of the manage.py file Returns the directory of the manage.py file
@ -42,6 +46,7 @@ def managePyDir():
return os.path.join(localDir(), 'InvenTree') return os.path.join(localDir(), 'InvenTree')
def managePyPath(): def managePyPath():
""" """
Return the path of the manage.py file Return the path of the manage.py file
@ -49,6 +54,7 @@ def managePyPath():
return os.path.join(managePyDir(), 'manage.py') return os.path.join(managePyDir(), 'manage.py')
def manage(c, cmd, pty=False): def manage(c, cmd, pty=False):
""" """
Runs a given command against django's "manage.py" script. Runs a given command against django's "manage.py" script.
@ -63,32 +69,11 @@ def manage(c, cmd, pty=False):
cmd=cmd cmd=cmd
), pty=pty) ), pty=pty)
@task(help={'length': 'Length of secret key (default=50)'})
def key(c, length=50, force=False):
"""
Generates a SECRET_KEY file which InvenTree uses for generating security hashes
"""
SECRET_KEY_FILE = os.path.join(localDir(), 'InvenTree', 'secret_key.txt') @task
# If a SECRET_KEY file does not exist, generate a new one!
if force or not os.path.exists(SECRET_KEY_FILE):
print("Generating SECRET_KEY file - " + SECRET_KEY_FILE)
with open(SECRET_KEY_FILE, 'w') as key_file:
options = string.digits + string.ascii_letters + string.punctuation
key = ''.join([random.choice(options) for i in range(length)])
key_file.write(key)
else:
print("SECRET_KEY file already exists - skipping")
@task(post=[key])
def install(c): def install(c):
""" """
Installs required python packages, and runs initial setup functions. Installs required python packages
""" """
# Install required Python packages with PIP # Install required Python packages with PIP
@ -111,6 +96,13 @@ def shell(c):
manage(c, 'shell', pty=True) manage(c, 'shell', pty=True)
@task
def worker(c):
"""
Run the InvenTree background worker process
"""
manage(c, 'qcluster', pty=True)
@task @task
def superuser(c): def superuser(c):
@ -128,6 +120,14 @@ def check(c):
manage(c, "check") manage(c, "check")
@task
def wait(c):
"""
Wait until the database connection is ready
"""
manage(c, "wait_for_db")
@task @task
def migrate(c): def migrate(c):
""" """
@ -154,7 +154,7 @@ def static(c):
as per Django requirements. as per Django requirements.
""" """
manage(c, "collectstatic") manage(c, "collectstatic --no-input")
@task(pre=[install, migrate, static]) @task(pre=[install, migrate, static])
@ -231,28 +231,6 @@ def coverage(c):
# Generate coverage report # Generate coverage report
c.run('coverage html') c.run('coverage html')
@task
def mysql(c):
"""
Install packages required for using InvenTree with a MySQL database.
"""
print('Installing packages required for MySQL')
c.run('sudo apt-get install mysql-server libmysqlclient-dev')
c.run('pip3 install mysqlclient')
@task
def postgresql(c):
"""
Install packages required for using InvenTree with a PostgreSQL database
"""
print("Installing packages required for PostgreSQL")
c.run('sudo apt-get install postgresql postgresql-contrib libpq-dev')
c.run('pip3 install psycopg2')
@task(help={'filename': "Output filename (default = 'data.json')"}) @task(help={'filename': "Output filename (default = 'data.json')"})
def export_records(c, filename='data.json'): def export_records(c, filename='data.json'):
""" """