diff --git a/.github/workflows/coverage.yaml b/.github/workflows/coverage.yaml index 85362ae917..2b883490d2 100644 --- a/.github/workflows/coverage.yaml +++ b/.github/workflows/coverage.yaml @@ -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 diff --git a/.github/workflows/docker_build.yaml b/.github/workflows/docker_build.yaml new file mode 100644 index 0000000000..c9f8a69654 --- /dev/null +++ b/.github/workflows/docker_build.yaml @@ -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) + \ No newline at end of file diff --git a/.github/workflows/docker_publish.yaml b/.github/workflows/docker_publish.yaml new file mode 100644 index 0000000000..3b9ad13a98 --- /dev/null +++ b/.github/workflows/docker_publish.yaml @@ -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 diff --git a/.github/workflows/mariadb.yaml b/.github/workflows/mariadb.yaml deleted file mode 100644 index 2ae02c2bd0..0000000000 --- a/.github/workflows/mariadb.yaml +++ /dev/null @@ -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 diff --git a/.github/workflows/mysql.yaml b/.github/workflows/mysql.yaml index 7d3ee8d6ae..70acad66a1 100644 --- a/.github/workflows/mysql.yaml +++ b/.github/workflows/mysql.yaml @@ -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: diff --git a/.github/workflows/postgresql.yaml b/.github/workflows/postgresql.yaml index aab05205cc..76981e5a1b 100644 --- a/.github/workflows/postgresql.yaml +++ b/.github/workflows/postgresql.yaml @@ -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: diff --git a/InvenTree/InvenTree/api.py b/InvenTree/InvenTree/api.py index 2d04195d42..a006050694 100644 --- a/InvenTree/InvenTree/api.py +++ b/InvenTree/InvenTree/api.py @@ -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) diff --git a/InvenTree/InvenTree/apps.py b/InvenTree/InvenTree/apps.py new file mode 100644 index 0000000000..03eb2bcb60 --- /dev/null +++ b/InvenTree/InvenTree/apps.py @@ -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 + ) diff --git a/InvenTree/InvenTree/context.py b/InvenTree/InvenTree/context.py index 9fee5deaab..3e9af2f751 100644 --- a/InvenTree/InvenTree/context.py +++ b/InvenTree/InvenTree/context.py @@ -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): """ diff --git a/InvenTree/InvenTree/management/commands/wait_for_db.py b/InvenTree/InvenTree/management/commands/wait_for_db.py new file mode 100644 index 0000000000..b9fa4e5025 --- /dev/null +++ b/InvenTree/InvenTree/management/commands/wait_for_db.py @@ -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!") diff --git a/InvenTree/InvenTree/middleware.py b/InvenTree/InvenTree/middleware.py index 2f1cf3a157..a34df4b7bd 100644 --- a/InvenTree/InvenTree/middleware.py +++ b/InvenTree/InvenTree/middleware.py @@ -8,7 +8,7 @@ import operator from rest_framework.authtoken.models import Token -logger = logging.getLogger(__name__) +logger = logging.getLogger("inventree") class AuthRequiredMiddleware(object): diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index d941de969c..8b5400e374 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -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", diff --git a/InvenTree/InvenTree/status.py b/InvenTree/InvenTree/status.py index 646ebb8131..88acc69a7a 100644 --- a/InvenTree/InvenTree/status.py +++ b/InvenTree/InvenTree/status.py @@ -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 diff --git a/InvenTree/InvenTree/tasks.py b/InvenTree/InvenTree/tasks.py new file mode 100644 index 0000000000..4829514f19 --- /dev/null +++ b/InvenTree/InvenTree/tasks.py @@ -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 + ) diff --git a/InvenTree/InvenTree/test_tasks.py b/InvenTree/InvenTree/test_tasks.py new file mode 100644 index 0000000000..02e8d14e5e --- /dev/null +++ b/InvenTree/InvenTree/test_tasks.py @@ -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) diff --git a/InvenTree/InvenTree/tests.py b/InvenTree/InvenTree/tests.py index 96f32e7f57..8465473901 100644 --- a/InvenTree/InvenTree/tests.py +++ b/InvenTree/InvenTree/tests.py @@ -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) diff --git a/InvenTree/InvenTree/version.py b/InvenTree/InvenTree/version.py index 1a28900905..7bfb752b63 100644 --- a/InvenTree/InvenTree/version.py +++ b/InvenTree/InvenTree/version.py @@ -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 diff --git a/InvenTree/barcodes/barcode.py b/InvenTree/barcodes/barcode.py index 412065bf75..a00e91d7e4 100644 --- a/InvenTree/barcodes/barcode.py +++ b/InvenTree/barcodes/barcode.py @@ -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): diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 12b51438cb..79580aabd9 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -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: diff --git a/InvenTree/company/apps.py b/InvenTree/company/apps.py index 3fa3197183..53e884c50f 100644 --- a/InvenTree/company/apps.py +++ b/InvenTree/company/apps.py @@ -9,7 +9,7 @@ from django.conf import settings from PIL import UnidentifiedImageError -logger = logging.getLogger(__name__) +logger = logging.getLogger("inventree") class CompanyConfig(AppConfig): diff --git a/InvenTree/config_template.yaml b/InvenTree/config_template.yaml index 18e3197cca..a64e6d42c0 100644 --- a/InvenTree/config_template.yaml +++ b/InvenTree/config_template.yaml @@ -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: diff --git a/InvenTree/label/apps.py b/InvenTree/label/apps.py index 9f2d3ea9c4..4200b6e8bc 100644 --- a/InvenTree/label/apps.py +++ b/InvenTree/label/apps.py @@ -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): diff --git a/InvenTree/label/models.py b/InvenTree/label/models.py index c76b3f80c8..96850f4cb0 100644 --- a/InvenTree/label/models.py +++ b/InvenTree/label/models.py @@ -32,7 +32,7 @@ except OSError as err: sys.exit(1) -logger = logging.getLogger(__name__) +logger = logging.getLogger("inventree") def rename_label(instance, filename): diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index 7dc8b4efff..284a24fcf5 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -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): diff --git a/InvenTree/part/apps.py b/InvenTree/part/apps.py index d08e7680fe..11329abdd9 100644 --- a/InvenTree/part/apps.py +++ b/InvenTree/part/apps.py @@ -9,7 +9,7 @@ from django.conf import settings from PIL import UnidentifiedImageError -logger = logging.getLogger(__name__) +logger = logging.getLogger("inventree") class PartConfig(AppConfig): diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index e10dfba4ba..bd02672b3e 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -52,7 +52,7 @@ import common.models import part.settings as part_settings -logger = logging.getLogger(__name__) +logger = logging.getLogger("inventree") class PartCategory(InvenTreeTree): diff --git a/InvenTree/plugins/action/action.py b/InvenTree/plugins/action/action.py index 8d70302021..d61838f49b 100644 --- a/InvenTree/plugins/action/action.py +++ b/InvenTree/plugins/action/action.py @@ -5,7 +5,7 @@ import logging import plugins.plugin as plugin -logger = logging.getLogger(__name__) +logger = logging.getLogger("inventree") class ActionPlugin(plugin.InvenTreePlugin): diff --git a/InvenTree/plugins/plugins.py b/InvenTree/plugins/plugins.py index abb167d173..f6b68112bc 100644 --- a/InvenTree/plugins/plugins.py +++ b/InvenTree/plugins/plugins.py @@ -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): diff --git a/InvenTree/report/apps.py b/InvenTree/report/apps.py index 941133e481..77529263f6 100644 --- a/InvenTree/report/apps.py +++ b/InvenTree/report/apps.py @@ -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): diff --git a/InvenTree/report/models.py b/InvenTree/report/models.py index 6f3891d6be..4e218a2c50 100644 --- a/InvenTree/report/models.py +++ b/InvenTree/report/models.py @@ -38,7 +38,7 @@ except OSError as err: sys.exit(1) -logger = logging.getLogger(__name__) +logger = logging.getLogger("inventree") class ReportFileUpload(FileSystemStorage): diff --git a/InvenTree/templates/about.html b/InvenTree/templates/about.html index 3305df430f..3a650adba2 100644 --- a/InvenTree/templates/about.html +++ b/InvenTree/templates/about.html @@ -19,11 +19,20 @@ - {% trans "InvenTree Version" %}{% inventree_version %} + {% trans "InvenTree Version" %} + + {% inventree_version %} + {% if up_to_date %} + {% trans "Up to Date" %} + {% else %} + {% trans "Update Available" %} + {% endif %} + - {% trans "Django Version" %}{% django_version %} + {% trans "Django Version" %} + {% django_version %} {% inventree_commit_hash as hash %} {% if hash %} @@ -69,4 +78,4 @@ - \ No newline at end of file + diff --git a/InvenTree/templates/navbar.html b/InvenTree/templates/navbar.html index 6e3fe024ca..acd71f0cd8 100644 --- a/InvenTree/templates/navbar.html +++ b/InvenTree/templates/navbar.html @@ -60,7 +60,9 @@ diff --git a/InvenTree/templates/stats.html b/InvenTree/templates/stats.html index 7b8a9bb93a..30d3f3d881 100644 --- a/InvenTree/templates/stats.html +++ b/InvenTree/templates/stats.html @@ -13,8 +13,9 @@ {% trans "Instance Name" %} {% inventree_instance_name %} + {% if user.is_staff %} - + {% trans "Server status" %} {% if system_healthy %} @@ -24,6 +25,18 @@ {% endif %} + + + {% trans "Background Worker" %} + + {% if django_q_running %} + {% trans "Operational" %} + {% else %} + {% trans "Not running" %} + {% endif %} + + + {% endif %} {% if not system_healthy %} {% for issue in system_issues %} diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py index 42028e91f0..e454281b37 100644 --- a/InvenTree/users/models.py +++ b/InvenTree/users/models.py @@ -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 = [ diff --git a/README.md b/README.md index 63948c35af..28169a7f90 100644 --- a/README.md +++ b/README.md @@ -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) InvenTree diff --git a/deploy/supervisord.conf b/deploy/supervisord.conf new file mode 100644 index 0000000000..88a5af3e5c --- /dev/null +++ b/deploy/supervisord.conf @@ -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 \ No newline at end of file diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000000..90b5fa2668 --- /dev/null +++ b/docker/docker-compose.yml @@ -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 diff --git a/docker/inventree/Dockerfile b/docker/inventree/Dockerfile new file mode 100644 index 0000000000..fcba19e964 --- /dev/null +++ b/docker/inventree/Dockerfile @@ -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"] diff --git a/docker/inventree/gunicorn.conf.py b/docker/inventree/gunicorn.conf.py new file mode 100644 index 0000000000..1071c2d745 --- /dev/null +++ b/docker/inventree/gunicorn.conf.py @@ -0,0 +1,6 @@ +import multiprocessing + +workers = multiprocessing.cpu_count() * 2 + 1 + +max_requests = 1000 +max_requests_jitter = 50 diff --git a/docker/inventree/start_server.sh b/docker/inventree/start_server.sh new file mode 100644 index 0000000000..0436cd532f --- /dev/null +++ b/docker/inventree/start_server.sh @@ -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 diff --git a/docker/inventree/start_worker.sh b/docker/inventree/start_worker.sh new file mode 100644 index 0000000000..7d0921a7af --- /dev/null +++ b/docker/inventree/start_worker.sh @@ -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 diff --git a/docker/nginx/Dockerfile b/docker/nginx/Dockerfile new file mode 100644 index 0000000000..e754597f02 --- /dev/null +++ b/docker/nginx/Dockerfile @@ -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 + diff --git a/docker/nginx/nginx.conf b/docker/nginx/nginx.conf new file mode 100644 index 0000000000..0f25f51674 --- /dev/null +++ b/docker/nginx/nginx.conf @@ -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/; + } + +} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 2237b4d794..da6e219b96 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/tasks.py b/tasks.py index 579887c809..895f3183ce 100644 --- a/tasks.py +++ b/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'): """