mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
commit
a2ff3e3474
2
.github/workflows/coverage.yaml
vendored
2
.github/workflows/coverage.yaml
vendored
@ -16,6 +16,8 @@ jobs:
|
||||
INVENTREE_DB_NAME: './test_db.sqlite'
|
||||
INVENTREE_DB_ENGINE: django.db.backends.sqlite3
|
||||
INVENTREE_DEBUG: info
|
||||
INVENTREE_MEDIA_ROOT: ./media
|
||||
INVENTREE_STATIC_ROOT: ./static
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
|
18
.github/workflows/docker_build.yaml
vendored
Normal file
18
.github/workflows/docker_build.yaml
vendored
Normal 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
38
.github/workflows/docker_publish.yaml
vendored
Normal 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
|
46
.github/workflows/mariadb.yaml
vendored
46
.github/workflows/mariadb.yaml
vendored
@ -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
|
2
.github/workflows/mysql.yaml
vendored
2
.github/workflows/mysql.yaml
vendored
@ -18,6 +18,8 @@ jobs:
|
||||
INVENTREE_DB_HOST: '127.0.0.1'
|
||||
INVENTREE_DB_PORT: 3306
|
||||
INVENTREE_DEBUG: info
|
||||
INVENTREE_MEDIA_ROOT: ./media
|
||||
INVENTREE_STATIC_ROOT: ./static
|
||||
|
||||
services:
|
||||
mysql:
|
||||
|
2
.github/workflows/postgresql.yaml
vendored
2
.github/workflows/postgresql.yaml
vendored
@ -18,6 +18,8 @@ jobs:
|
||||
INVENTREE_DB_HOST: '127.0.0.1'
|
||||
INVENTREE_DB_PORT: 5432
|
||||
INVENTREE_DEBUG: info
|
||||
INVENTREE_MEDIA_ROOT: ./media
|
||||
INVENTREE_STATIC_ROOT: ./static
|
||||
|
||||
services:
|
||||
postgres:
|
||||
|
@ -19,11 +19,12 @@ from rest_framework.views import APIView
|
||||
|
||||
from .views import AjaxView
|
||||
from .version import inventreeVersion, inventreeApiVersion, inventreeInstanceName
|
||||
from .status import is_worker_running
|
||||
|
||||
from plugins import plugins as inventree_plugins
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = logging.getLogger("inventree")
|
||||
|
||||
|
||||
logger.info("Loading action plugins...")
|
||||
@ -44,6 +45,7 @@ class InfoView(AjaxView):
|
||||
'version': inventreeVersion(),
|
||||
'instance': inventreeInstanceName(),
|
||||
'apiVersion': inventreeApiVersion(),
|
||||
'worker_running': is_worker_running(),
|
||||
}
|
||||
|
||||
return JsonResponse(data)
|
||||
|
44
InvenTree/InvenTree/apps.py
Normal file
44
InvenTree/InvenTree/apps.py
Normal 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
|
||||
)
|
@ -30,10 +30,22 @@ def health_status(request):
|
||||
|
||||
request._inventree_health_status = True
|
||||
|
||||
return {
|
||||
"system_healthy": InvenTree.status.check_system_health(),
|
||||
status = {
|
||||
'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):
|
||||
"""
|
||||
|
42
InvenTree/InvenTree/management/commands/wait_for_db.py
Normal file
42
InvenTree/InvenTree/management/commands/wait_for_db.py
Normal 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!")
|
@ -8,7 +8,7 @@ import operator
|
||||
|
||||
from rest_framework.authtoken.models import Token
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = logging.getLogger("inventree")
|
||||
|
||||
|
||||
class AuthRequiredMiddleware(object):
|
||||
|
@ -13,6 +13,9 @@ database setup in this file.
|
||||
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
import string
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
from datetime import datetime
|
||||
@ -46,14 +49,31 @@ def get_setting(environment_var, backup_val, default_value=None):
|
||||
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, ...)
|
||||
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):
|
||||
print("Error: config.yaml not found")
|
||||
sys.exit(-1)
|
||||
print("InvenTree configuration file 'config.yaml' not found - creating default file")
|
||||
|
||||
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:
|
||||
CONFIG = yaml.safe_load(cfg)
|
||||
@ -94,7 +114,18 @@ LOGGING = {
|
||||
}
|
||||
|
||||
# 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"):
|
||||
# Secret key passed in directly
|
||||
@ -105,15 +136,22 @@ else:
|
||||
key_file = os.getenv("INVENTREE_SECRET_KEY_FILE")
|
||||
|
||||
if key_file:
|
||||
if os.path.isfile(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)
|
||||
key_file = os.path.abspath(key_file)
|
||||
else:
|
||||
# default secret key location
|
||||
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:
|
||||
SECRET_KEY = open(key_file, "r").read().strip()
|
||||
except Exception:
|
||||
@ -144,7 +182,7 @@ STATIC_URL = '/static/'
|
||||
STATIC_ROOT = os.path.abspath(
|
||||
get_setting(
|
||||
'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(
|
||||
get_setting(
|
||||
'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',
|
||||
'stock.apps.StockConfig',
|
||||
'users.apps.UsersConfig',
|
||||
'InvenTree.apps.InvenTreeConfig', # InvenTree app runs last
|
||||
|
||||
# Third part add-ons
|
||||
'django_filters', # Extended filter functionality
|
||||
@ -211,6 +250,7 @@ INSTALLED_APPS = [
|
||||
'djmoney', # django-money integration
|
||||
'djmoney.contrib.exchange', # django-money exchange rates
|
||||
'error_report', # Error reporting in the admin interface
|
||||
'django_q',
|
||||
]
|
||||
|
||||
MIDDLEWARE = CONFIG.get('middleware', [
|
||||
@ -285,6 +325,18 @@ REST_FRAMEWORK = {
|
||||
|
||||
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
|
||||
# Ref: https://neutronx.github.io/django-markdownx/customization/
|
||||
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
|
||||
db_config = CONFIG.get('database', {})
|
||||
|
||||
if not db_config:
|
||||
db_config = {}
|
||||
|
||||
# Environment variables take preference over config file!
|
||||
|
||||
db_keys = ['ENGINE', 'NAME', 'USER', 'PASSWORD', 'HOST', 'PORT']
|
||||
@ -350,7 +405,7 @@ reqiured_keys = ['ENGINE', 'NAME']
|
||||
|
||||
for key in reqiured_keys:
|
||||
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)
|
||||
|
||||
print('Error: ' + error_msg)
|
||||
@ -386,11 +441,6 @@ CACHES = {
|
||||
'default': {
|
||||
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
|
||||
},
|
||||
'qr-code': {
|
||||
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
|
||||
'LOCATION': 'qr-code-cache',
|
||||
'TIMEOUT': 3600
|
||||
}
|
||||
}
|
||||
|
||||
# Password validation
|
||||
@ -449,13 +499,19 @@ LOCALE_PATHS = (
|
||||
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_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 = [
|
||||
"%Y-%m-%d",
|
||||
|
@ -1,13 +1,46 @@
|
||||
"""
|
||||
Provides system status functionality checks.
|
||||
"""
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
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):
|
||||
@ -19,21 +52,11 @@ def check_system_health(**kwargs):
|
||||
|
||||
result = True
|
||||
|
||||
if not check_celery_worker(**kwargs):
|
||||
if not is_worker_running(**kwargs):
|
||||
result = False
|
||||
logger.warning(_("Celery worker check failed"))
|
||||
logger.warning(_("Background worker check failed"))
|
||||
|
||||
if not result:
|
||||
logger.warning(_("InvenTree system health checks failed"))
|
||||
|
||||
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
|
||||
|
143
InvenTree/InvenTree/tasks.py
Normal file
143
InvenTree/InvenTree/tasks.py
Normal 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
|
||||
)
|
43
InvenTree/InvenTree/test_tasks.py
Normal file
43
InvenTree/InvenTree/test_tasks.py
Normal 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)
|
@ -7,6 +7,7 @@ from django.core.exceptions import ValidationError
|
||||
|
||||
from .validators import validate_overage, validate_part_name
|
||||
from . import helpers
|
||||
from . import version
|
||||
|
||||
from mptt.exceptions import InvalidMove
|
||||
|
||||
@ -269,3 +270,33 @@ class TestSerialNumberExtraction(TestCase):
|
||||
|
||||
with self.assertRaises(ValidationError):
|
||||
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)
|
||||
|
@ -4,6 +4,7 @@ Provides information on the current InvenTree version
|
||||
|
||||
import subprocess
|
||||
import django
|
||||
import re
|
||||
|
||||
import common.models
|
||||
|
||||
@ -23,6 +24,38 @@ def inventreeVersion():
|
||||
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():
|
||||
return INVENTREE_API_VERSION
|
||||
|
||||
|
@ -12,7 +12,7 @@ from stock.serializers import StockItemSerializer, LocationSerializer
|
||||
from part.serializers import PartSerializer
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = logging.getLogger('inventree')
|
||||
|
||||
|
||||
def hash_barcode(barcode_data):
|
||||
|
@ -500,7 +500,7 @@ class InvenTreeSetting(models.Model):
|
||||
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
|
||||
|
||||
try:
|
||||
|
@ -9,7 +9,7 @@ from django.conf import settings
|
||||
|
||||
from PIL import UnidentifiedImageError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = logging.getLogger("inventree")
|
||||
|
||||
|
||||
class CompanyConfig(AppConfig):
|
||||
|
@ -7,11 +7,9 @@
|
||||
# with the prefix INVENTREE_DB_
|
||||
# e.g INVENTREE_DB_NAME / INVENTREE_DB_USER / INVENTREE_DB_PASSWORD
|
||||
database:
|
||||
# Default configuration - sqlite filesystem database
|
||||
ENGINE: sqlite3
|
||||
NAME: '../inventree_default_db.sqlite3'
|
||||
|
||||
# For more complex database installations, further parameters are required
|
||||
# Uncomment (and edit) one of the database configurations below,
|
||||
# or specify database options using environment variables
|
||||
|
||||
# Refer to the django documentation for full list of options
|
||||
|
||||
# --- Available options: ---
|
||||
@ -27,14 +25,22 @@ database:
|
||||
|
||||
# --- Example Configuration - sqlite3 ---
|
||||
# ENGINE: sqlite3
|
||||
# NAME: '/path/to/database.sqlite3'
|
||||
# NAME: '/home/inventree/database.sqlite3'
|
||||
|
||||
# --- Example Configuration - MySQL ---
|
||||
#ENGINE: django.db.backends.mysql
|
||||
#ENGINE: mysql
|
||||
#NAME: inventree
|
||||
#USER: inventree_username
|
||||
#USER: inventree
|
||||
#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'
|
||||
|
||||
# Select default system language (default is 'en-us')
|
||||
@ -43,6 +49,7 @@ language: en-us
|
||||
# System time-zone (default is UTC)
|
||||
# Reference: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
|
||||
# Select an option from the "TZ database name" column
|
||||
# Use the environment variable INVENTREE_TIMEZONE
|
||||
timezone: UTC
|
||||
|
||||
# List of currencies supported by default.
|
||||
@ -57,6 +64,7 @@ currencies:
|
||||
- USD
|
||||
|
||||
# Set debug to False to run in production mode
|
||||
# Use the environment variable INVENTREE_DEBUG
|
||||
debug: True
|
||||
|
||||
# Set debug_toolbar to True to enable a debugging toolbar for InvenTree
|
||||
@ -65,6 +73,7 @@ debug: True
|
||||
debug_toolbar: False
|
||||
|
||||
# Configure the system logging level
|
||||
# Use environment variable INVENTREE_LOG_LEVEL
|
||||
# Options: DEBUG / INFO / WARNING / ERROR / CRITICAL
|
||||
log_level: WARNING
|
||||
|
||||
@ -86,13 +95,14 @@ cors:
|
||||
# - https://sub.example.com
|
||||
|
||||
# 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
|
||||
# This should be changed for a production installation
|
||||
media_root: '../inventree_media'
|
||||
# By default, it is stored under /home/inventree/data/media
|
||||
# Use environment variable INVENTREE_MEDIA_ROOT
|
||||
media_root: '/home/inventree/data/media'
|
||||
|
||||
# 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
|
||||
static_root: '../inventree_static'
|
||||
# By default, it is stored under /home/inventree
|
||||
# Use environment variable INVENTREE_STATIC_ROOT
|
||||
static_root: '/home/inventree/static'
|
||||
|
||||
# Optional URL schemes to allow in URL fields
|
||||
# By default, only the following schemes are allowed: ['http', 'https', 'ftp', 'ftps']
|
||||
@ -105,7 +115,8 @@ static_root: '../inventree_static'
|
||||
# Backup options
|
||||
# Set the backup_dir parameter to store backup files in a specific location
|
||||
# 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
|
||||
#authentication_backends:
|
||||
|
@ -7,7 +7,7 @@ from django.apps import AppConfig
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = logging.getLogger("inventree")
|
||||
|
||||
|
||||
def hashFile(filename):
|
||||
|
@ -32,7 +32,7 @@ except OSError as err:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = logging.getLogger("inventree")
|
||||
|
||||
|
||||
def rename_label(instance, filename):
|
||||
|
@ -37,7 +37,7 @@ from InvenTree.views import InvenTreeRoleMixin
|
||||
|
||||
from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus, StockStatus
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = logging.getLogger("inventree")
|
||||
|
||||
|
||||
class PurchaseOrderIndex(InvenTreeRoleMixin, ListView):
|
||||
|
@ -9,7 +9,7 @@ from django.conf import settings
|
||||
|
||||
from PIL import UnidentifiedImageError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = logging.getLogger("inventree")
|
||||
|
||||
|
||||
class PartConfig(AppConfig):
|
||||
|
@ -52,7 +52,7 @@ import common.models
|
||||
import part.settings as part_settings
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = logging.getLogger("inventree")
|
||||
|
||||
|
||||
class PartCategory(InvenTreeTree):
|
||||
|
@ -5,7 +5,7 @@ import logging
|
||||
import plugins.plugin as plugin
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = logging.getLogger("inventree")
|
||||
|
||||
|
||||
class ActionPlugin(plugin.InvenTreePlugin):
|
||||
|
@ -10,7 +10,7 @@ import plugins.action as action
|
||||
from plugins.action.action import ActionPlugin
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = logging.getLogger("inventree")
|
||||
|
||||
|
||||
def iter_namespace(pkg):
|
||||
|
@ -6,7 +6,7 @@ from django.apps import AppConfig
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = logging.getLogger("inventree")
|
||||
|
||||
|
||||
class ReportConfig(AppConfig):
|
||||
|
@ -38,7 +38,7 @@ except OSError as err:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = logging.getLogger("inventree")
|
||||
|
||||
|
||||
class ReportFileUpload(FileSystemStorage):
|
||||
|
@ -19,11 +19,20 @@
|
||||
<col width='25'>
|
||||
<tr>
|
||||
<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>
|
||||
<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>
|
||||
{% inventree_commit_hash as hash %}
|
||||
{% if hash %}
|
||||
@ -69,4 +78,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -60,7 +60,9 @@
|
||||
<li class='dropdown'>
|
||||
<a class='dropdown-toggle' data-toggle='dropdown' href="#">
|
||||
{% 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 %}
|
||||
<span class="fas fa-user"></span> <b>{{ user.get_username }}</b></a>
|
||||
<ul class='dropdown-menu'>
|
||||
@ -78,11 +80,20 @@
|
||||
{% if system_healthy %}
|
||||
<span class='fas fa-server'>
|
||||
{% else %}
|
||||
<span class='fas fa-exclamation-triangle icon-red'>
|
||||
<span class='fas fa-server icon-red'>
|
||||
{% endif %}
|
||||
</span> {% trans "System Information" %}
|
||||
</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>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -13,8 +13,9 @@
|
||||
<td>{% trans "Instance Name" %}</td>
|
||||
<td>{% inventree_instance_name %}</td>
|
||||
</tr>
|
||||
{% if user.is_staff %}
|
||||
<tr>
|
||||
<td><span class='fas fa-exclamation-triangle'></span></td>
|
||||
<td><span class='fas fa-server'></span></td>
|
||||
<td>{% trans "Server status" %}</td>
|
||||
<td>
|
||||
{% if system_healthy %}
|
||||
@ -24,6 +25,18 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
</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 %}
|
||||
{% for issue in system_issues %}
|
||||
|
@ -15,7 +15,7 @@ from django.db.models.signals import post_save, post_delete
|
||||
import logging
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = logging.getLogger("inventree")
|
||||
|
||||
|
||||
class RuleSet(models.Model):
|
||||
@ -137,6 +137,13 @@ class RuleSet(models.Model):
|
||||
'error_report_error',
|
||||
'exchange_rate',
|
||||
'exchange_exchangebackend',
|
||||
|
||||
# Django-q
|
||||
'django_q_ormq',
|
||||
'django_q_failure',
|
||||
'django_q_task',
|
||||
'django_q_schedule',
|
||||
'django_q_success',
|
||||
]
|
||||
|
||||
RULE_OPTIONS = [
|
||||
|
@ -1,9 +1,10 @@
|
||||
[![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)
|
||||
![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)
|
||||
![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)
|
||||
|
||||
<img src="images/logo/inventree.png" alt="InvenTree" width="128"/>
|
||||
|
46
deploy/supervisord.conf
Normal file
46
deploy/supervisord.conf
Normal 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
100
docker/docker-compose.yml
Normal 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
|
95
docker/inventree/Dockerfile
Normal file
95
docker/inventree/Dockerfile
Normal 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"]
|
6
docker/inventree/gunicorn.conf.py
Normal file
6
docker/inventree/gunicorn.conf.py
Normal file
@ -0,0 +1,6 @@
|
||||
import multiprocessing
|
||||
|
||||
workers = multiprocessing.cpu_count() * 2 + 1
|
||||
|
||||
max_requests = 1000
|
||||
max_requests_jitter = 50
|
46
docker/inventree/start_server.sh
Normal file
46
docker/inventree/start_server.sh
Normal 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
|
14
docker/inventree/start_worker.sh
Normal file
14
docker/inventree/start_worker.sh
Normal 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
14
docker/nginx/Dockerfile
Normal 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
21
docker/nginx/nginx.conf
Normal 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/;
|
||||
}
|
||||
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
invoke>=1.4.0 # Invoke build tool
|
||||
wheel>=0.34.2 # Wheel
|
||||
Django==3.0.7 # Django package
|
||||
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
|
||||
python-barcode[images]==0.13.1 # Barcode 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
|
||||
|
78
tasks.py
78
tasks.py
@ -1,13 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from invoke import task
|
||||
from shutil import copyfile
|
||||
|
||||
import random
|
||||
import string
|
||||
import os
|
||||
import sys
|
||||
|
||||
try:
|
||||
from invoke import ctask as task
|
||||
except:
|
||||
from invoke import task
|
||||
|
||||
|
||||
def apps():
|
||||
"""
|
||||
Returns a list of installed apps
|
||||
@ -27,6 +29,7 @@ def apps():
|
||||
'users',
|
||||
]
|
||||
|
||||
|
||||
def localDir():
|
||||
"""
|
||||
Returns the directory of *THIS* file.
|
||||
@ -35,6 +38,7 @@ def localDir():
|
||||
"""
|
||||
return os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
||||
def managePyDir():
|
||||
"""
|
||||
Returns the directory of the manage.py file
|
||||
@ -42,6 +46,7 @@ def managePyDir():
|
||||
|
||||
return os.path.join(localDir(), 'InvenTree')
|
||||
|
||||
|
||||
def managePyPath():
|
||||
"""
|
||||
Return the path of the manage.py file
|
||||
@ -49,6 +54,7 @@ def managePyPath():
|
||||
|
||||
return os.path.join(managePyDir(), 'manage.py')
|
||||
|
||||
|
||||
def manage(c, cmd, pty=False):
|
||||
"""
|
||||
Runs a given command against django's "manage.py" script.
|
||||
@ -63,32 +69,11 @@ def manage(c, cmd, pty=False):
|
||||
cmd=cmd
|
||||
), 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')
|
||||
|
||||
# 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])
|
||||
@task
|
||||
def install(c):
|
||||
"""
|
||||
Installs required python packages, and runs initial setup functions.
|
||||
Installs required python packages
|
||||
"""
|
||||
|
||||
# Install required Python packages with PIP
|
||||
@ -111,6 +96,13 @@ def shell(c):
|
||||
|
||||
manage(c, 'shell', pty=True)
|
||||
|
||||
@task
|
||||
def worker(c):
|
||||
"""
|
||||
Run the InvenTree background worker process
|
||||
"""
|
||||
|
||||
manage(c, 'qcluster', pty=True)
|
||||
|
||||
@task
|
||||
def superuser(c):
|
||||
@ -128,6 +120,14 @@ def check(c):
|
||||
|
||||
manage(c, "check")
|
||||
|
||||
@task
|
||||
def wait(c):
|
||||
"""
|
||||
Wait until the database connection is ready
|
||||
"""
|
||||
|
||||
manage(c, "wait_for_db")
|
||||
|
||||
@task
|
||||
def migrate(c):
|
||||
"""
|
||||
@ -154,7 +154,7 @@ def static(c):
|
||||
as per Django requirements.
|
||||
"""
|
||||
|
||||
manage(c, "collectstatic")
|
||||
manage(c, "collectstatic --no-input")
|
||||
|
||||
|
||||
@task(pre=[install, migrate, static])
|
||||
@ -231,28 +231,6 @@ def coverage(c):
|
||||
# Generate coverage report
|
||||
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')"})
|
||||
def export_records(c, filename='data.json'):
|
||||
"""
|
||||
|
Loading…
Reference in New Issue
Block a user