diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e87a705e10..2371f411a9 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -4,3 +4,9 @@ # plugins are co-owned /InvenTree/plugin/ @SchrodingersGat @matmair /InvenTree/plugins/ @SchrodingersGat @matmair + +# Installer functions +.pkgr.yml @matmair +Procfile @matmair +runtime.txt @matmair +/contrib/ @matmair diff --git a/.pkgr.yml b/.pkgr.yml new file mode 100644 index 0000000000..9a4cc0f950 --- /dev/null +++ b/.pkgr.yml @@ -0,0 +1,46 @@ +name: inventree +description: Open Source Inventory Management System +homepage: https://inventree.org +notifications: false +buildpack: https://github.com/mjmair/heroku-buildpack-python#v216-mjmair +env: + - STACK=heroku-20 + - DISABLE_COLLECTSTATIC=1 + - INVENTREE_DB_ENGINE=sqlite3 + - INVENTREE_DB_NAME=database.sqlite3 + - INVENTREE_PLUGINS_ENABLED + - INVENTREE_MEDIA_ROOT=/opt/inventree/media + - INVENTREE_STATIC_ROOT=/opt/inventree/static + - INVENTREE_PLUGIN_FILE=/opt/inventree/plugins.txt + - INVENTREE_CONFIG_FILE=/opt/inventree/config.yaml +after_install: contrib/packager.io/postinstall.sh +targets: + ubuntu-20.04: + dependencies: + - curl + - python3 + - python3-venv + - python3-pip + - python3-cffi + - python3-brotli + - python3-wheel + - libpango-1.0-0 + - libharfbuzz0b + - libpangoft2-1.0-0 + - gettext + - nginx + - jq + debian-11: + dependencies: + - curl + - python3 + - python3-venv + - python3-pip + - python3-cffi + - python3-brotli + - python3-wheel + - libpango-1.0-0 + - libpangoft2-1.0-0 + - gettext + - nginx + - jq diff --git a/InvenTree/InvenTree/config.py b/InvenTree/InvenTree/config.py index 4c95e84970..51f194ffb0 100644 --- a/InvenTree/InvenTree/config.py +++ b/InvenTree/InvenTree/config.py @@ -22,6 +22,16 @@ def get_base_dir() -> Path: return Path(__file__).parent.parent.resolve() +def ensure_dir(path: Path) -> None: + """Ensure that a directory exists. + + If it does not exist, create it. + """ + + if not path.exists(): + path.mkdir(parents=True, exist_ok=True) + + def get_config_file(create=True) -> Path: """Returns the path of the InvenTree configuration file. @@ -39,6 +49,7 @@ def get_config_file(create=True) -> Path: if not cfg_filename.exists() and create: print("InvenTree configuration file 'config.yaml' not found - creating default file") + ensure_dir(cfg_filename.parent) cfg_template = base_dir.joinpath("config_template.yaml") shutil.copyfile(cfg_template, cfg_filename) @@ -169,6 +180,7 @@ def get_plugin_file(): if not plugin_file.exists(): logger.warning("Plugin configuration file does not exist - creating default file") logger.info(f"Creating plugin file at '{plugin_file}'") + ensure_dir(plugin_file.parent) # If opening the file fails (no write permission, for example), then this will throw an error plugin_file.write_text("# InvenTree Plugins (uses PIP framework to install)\n\n") @@ -201,6 +213,7 @@ def get_secret_key(): if not secret_key_file.exists(): logger.info(f"Generating random key file at '{secret_key_file}'") + ensure_dir(secret_key_file.parent) # Create a random key file options = string.digits + string.ascii_letters + string.punctuation diff --git a/InvenTree/config_template.yaml b/InvenTree/config_template.yaml index 538a832f21..f1405b48de 100644 --- a/InvenTree/config_template.yaml +++ b/InvenTree/config_template.yaml @@ -1,4 +1,8 @@ +# Secret key for backend +# Use the environment variable INVENTREE_SECRET_KEY_FILE +#secret_key_file: '/etc/inventree/secret_key.txt' + # Database backend selection - Configure backend database settings # Documentation: https://inventree.readthedocs.io/en/latest/start/config/ @@ -22,6 +26,13 @@ database: # HOST: Database host address (if required) # PORT: Database host port (if required) + # --- Database settings --- + #ENGINE: sampleengine + #NAME: '/path/to/database' + #USER: sampleuser + #PASSWORD: samplepassword + #HOST: samplehost + #PORT: sampleport # --- Example Configuration - MySQL --- #ENGINE: mysql @@ -105,8 +116,8 @@ sentry_enabled: False # Set this variable to True to enable InvenTree Plugins # Alternatively, use the environment variable INVENTREE_PLUGINS_ENABLED plugins_enabled: False -#plugin_file: /path/to/plugins.txt -#plugin_dir: /path/to/plugins/ +#plugin_file: '/path/to/plugins.txt' +#plugin_dir: '/path/to/plugins/' # Allowed hosts (see ALLOWED_HOSTS in Django settings documentation) # A list of strings representing the host/domain names that this Django site can serve. diff --git a/Procfile b/Procfile new file mode 100644 index 0000000000..d1a313b067 --- /dev/null +++ b/Procfile @@ -0,0 +1,2 @@ +web: env/bin/gunicorn --chdir $APP_HOME/InvenTree -c InvenTree/gunicorn.conf.py InvenTree.wsgi -b 0.0.0.0:$PORT +worker: env/bin/python InvenTree/manage.py qcluster diff --git a/README.md b/README.md index a0ca4e75e2..a37c887b52 100644 --- a/README.md +++ b/README.md @@ -136,6 +136,11 @@ There are several options to deploy InvenTree. Bare Metal +Single line install: +```bash +curl https://raw.githubusercontent.com/InvenTree/InvenTree/master/contrib/install.sh | sh +``` + ## :wave: Contributing diff --git a/contrib/install.sh b/contrib/install.sh new file mode 100755 index 0000000000..ef222dee74 --- /dev/null +++ b/contrib/install.sh @@ -0,0 +1,54 @@ +get_distribution() { + lsb_dist="" + # Every system that we officially support has /etc/os-release + if [ -r /etc/os-release ]; then + lsb_dist="$(. /etc/os-release && echo "$ID")" + fi + # Returning an empty string here should be alright since the + # case statements don't act unless you provide an actual value + echo "$lsb_dist" +} + +get_distribution +case "$lsb_dist" in +ubuntu) + if command_exists lsb_release; then + dist_version="$(lsb_release -r | cut -f2)" + fi + if [ -z "$dist_version" ] && [ -r /etc/lsb-release ]; then + dist_version="$(. /etc/lsb-release && echo "$DISTRIB_RELEASE")" + fi + ;; +debian | raspbian) + dist_version="$(sed 's/\/.*//' /etc/debian_version | sed 's/\..*//')" + lsb_dist="debian" + ;; +centos | rhel | sles) + if [ -z "$dist_version" ] && [ -r /etc/os-release ]; then + dist_version="$(. /etc/os-release && echo "$VERSION_ID")" + fi + ;; +*) + if command_exists lsb_release; then + dist_version="$(lsb_release --release | cut -f2)" + fi + if [ -z "$dist_version" ] && [ -r /etc/os-release ]; then + dist_version="$(. /etc/os-release && echo "$VERSION_ID")" + fi + ;; +esac +echo "### ${lsb_dist} ${dist_version} detected" + +# Make sure the depencies are there +sudo apt-get install wget apt-transport-https -y + +echo "### Add key and package source" +# Add key +wget -qO- https://dl.packager.io/srv/matmair/InvenTree/key | sudo apt-key add - +# Add packagelist +sudo wget -O /etc/apt/sources.list.d/inventree.list https://dl.packager.io/srv/matmair/InvenTree/deploy-test/installer/${lsb_dist}/${dist_version}.repo + +echo "### Install InvenTree" +# Update repos and install inventree +sudo apt-get update +sudo apt-get install inventree -y diff --git a/contrib/packager.io/functions.sh b/contrib/packager.io/functions.sh new file mode 100755 index 0000000000..a772f38379 --- /dev/null +++ b/contrib/packager.io/functions.sh @@ -0,0 +1,289 @@ +#!/bin/bash +# +# packager.io postinstall script functions +# + +function detect_docker() { + if [ -n "$(grep docker /dev/null)" ]; then + INIT_CMD="systemctl" + elif [ -n "$(which initctl 2>/dev/null)" ]; then + INIT_CMD="initctl" + else + function sysvinit() { + service $2 $1 + } + INIT_CMD="sysvinit" + fi + + if [ "${DOCKER}" == "yes" ]; then + INIT_CMD="initctl" + fi +} + +function detect_ip() { + # Get the IP address of the server + + if [ "${SETUP_NO_CALLS}" == "true" ]; then + # Use local IP address + echo "# Getting the IP address of the first local IP address" + export INVENTREE_IP=$(hostname -I | awk '{print $1}') + else + # Use web service to get the IP address + echo "# Getting the IP address of the server via web service" + export INVENTREE_IP=$(curl -s https://checkip.amazonaws.com) + fi + + echo "IP address is ${INVENTREE_IP}" +} + +function get_env() { + envname=$1 + + pid=$$ + while [ -z "${!envname}" -a $pid != 1 ]; do + ppid=`ps -oppid -p$pid|tail -1|awk '{print $1}'` + env=`strings /proc/$ppid/environ` + export $envname=`echo "$env"|awk -F= '$1 == "'$envname'" { print $2; }'` + pid=$ppid + done + + if [ -n "${SETUP_DEBUG}" ]; then + echo "Done getting env $envname: ${!envname}" + fi +} + +function detect_local_env() { + # Get all possible envs for the install + + if [ -n "${SETUP_DEBUG}" ]; then + echo "# Printing local envs - before #++#" + printenv + fi + + for i in ${SETUP_ENVS//,/ } + do + get_env $i + done + + if [ -n "${SETUP_DEBUG}" ]; then + echo "# Printing local envs - after #++#" + printenv + fi +} + +function detect_envs() { + # Detect all envs that should be passed to setup commands + + echo "# Setting base environment variables" + + export INVENTREE_CONFIG_FILE=${CONF_DIR}/config.yaml + + if test -f "${INVENTREE_CONFIG_FILE}"; then + echo "# Using existing config file: ${INVENTREE_CONFIG_FILE}" + + # Install parser + pip install jc -q + + # Load config + local conf=$(cat ${INVENTREE_CONFIG_FILE} | jc --yaml) + + # Parse the config file + export INVENTREE_MEDIA_ROOT=$conf | jq '.[].media_root' + export INVENTREE_STATIC_ROOT=$conf | jq '.[].static_root' + export INVENTREE_PLUGINS_ENABLED=$conf | jq '.[].plugins_enabled' + export INVENTREE_PLUGIN_FILE=$conf | jq '.[].plugin_file' + export INVENTREE_SECRET_KEY_FILE=$conf | jq '.[].secret_key_file' + + export INVENTREE_DB_ENGINE=$conf | jq '.[].database.ENGINE' + export INVENTREE_DB_NAME=$conf | jq '.[].database.NAME' + export INVENTREE_DB_USER=$conf | jq '.[].database.USER' + export INVENTREE_DB_PASSWORD=$conf | jq '.[].database.PASSWORD' + export INVENTREE_DB_HOST=$conf | jq '.[].database.HOST' + export INVENTREE_DB_PORT=$conf | jq '.[].database.PORT' + else + echo "# No config file found: ${INVENTREE_CONFIG_FILE}, using envs or defaults" + + if [ -n "${SETUP_DEBUG}" ]; then + echo "# Print current envs" + printenv | grep INVENTREE_ + printenv | grep SETUP_ + fi + + export INVENTREE_MEDIA_ROOT=${INVENTREE_MEDIA_ROOT:-${DATA_DIR}/media} + export INVENTREE_STATIC_ROOT=${DATA_DIR}/static + export INVENTREE_PLUGINS_ENABLED=true + export INVENTREE_PLUGIN_FILE=${CONF_DIR}/plugins.txt + export INVENTREE_SECRET_KEY_FILE=${CONF_DIR}/secret_key.txt + + export INVENTREE_DB_ENGINE=${INVENTREE_DB_ENGINE:-sqlite3} + export INVENTREE_DB_NAME=${INVENTREE_DB_NAME:-${DATA_DIR}/database.sqlite3} + export INVENTREE_DB_USER=${INVENTREE_DB_USER:-sampleuser} + export INVENTREE_DB_PASSWORD=${INVENTREE_DB_PASSWORD:-samplepassword} + export INVENTREE_DB_HOST=${INVENTREE_DB_HOST:-samplehost} + export INVENTREE_DB_PORT=${INVENTREE_DB_PORT:-sampleport} + + export SETUP_CONF_LOADED=true + fi + + # For debugging pass out the envs + echo "# Collected environment variables:" + echo "# INVENTREE_MEDIA_ROOT=${INVENTREE_MEDIA_ROOT}" + echo "# INVENTREE_STATIC_ROOT=${INVENTREE_STATIC_ROOT}" + echo "# INVENTREE_PLUGINS_ENABLED=${INVENTREE_PLUGINS_ENABLED}" + echo "# INVENTREE_PLUGIN_FILE=${INVENTREE_PLUGIN_FILE}" + echo "# INVENTREE_SECRET_KEY_FILE=${INVENTREE_SECRET_KEY_FILE}" + echo "# INVENTREE_DB_ENGINE=${INVENTREE_DB_ENGINE}" + echo "# INVENTREE_DB_NAME=${INVENTREE_DB_NAME}" + echo "# INVENTREE_DB_USER=${INVENTREE_DB_USER}" + if [ -n "${SETUP_DEBUG}" ]; then + echo "# INVENTREE_DB_PASSWORD=${INVENTREE_DB_PASSWORD}" + fi + echo "# INVENTREE_DB_HOST=${INVENTREE_DB_HOST}" + echo "# INVENTREE_DB_PORT=${INVENTREE_DB_PORT}" +} + +function create_initscripts() { + + # Make sure python env exsists + if test -f "${APP_HOME}/env"; then + echo "# python enviroment already present - skipping" + else + echo "# Setting up python enviroment" + sudo -u ${APP_USER} --preserve-env=$SETUP_ENVS bash -c "cd ${APP_HOME} && python3 -m venv env && pip install invoke" + + if [ -n "${SETUP_EXTRA_PIP}" ]; then + echo "# Installing extra pip packages" + if [ -n "${SETUP_DEBUG}" ]; then + echo "# Extra pip packages: ${SETUP_EXTRA_PIP}" + fi + sudo -u ${APP_USER} --preserve-env=$SETUP_ENVS bash -c "cd ${APP_HOME} && env/bin/pip install ${SETUP_EXTRA_PIP}" + fi + fi + + # Unlink default config if it exists + if test -f "/etc/nginx/sites-enabled/default"; then + echo "# Unlinking default nginx config\n# Old file still in /etc/nginx/sites-available/default" + sudo unlink /etc/nginx/sites-enabled/default + fi + + # Create InvenTree specific nginx config + echo "# Stopping nginx" + ${INIT_CMD} stop nginx + echo "# Setting up nginx to ${SETUP_NGINX_FILE}" + # Always use the latest nginx config; important if new headers are added / needed for security + cp ${APP_HOME}/docker/production/nginx.prod.conf ${SETUP_NGINX_FILE} + sed -i s/inventree-server:8000/localhost:6000/g ${SETUP_NGINX_FILE} + sed -i s=var/www=opt/inventree/data=g ${SETUP_NGINX_FILE} + # Start nginx + echo "# Starting nginx" + ${INIT_CMD} start nginx + + echo "# (Re)creating init scripts" + # This reset scale parameters to a known state + inventree scale web="1" worker="1" + + echo "# Enabling InvenTree on boot" + ${INIT_CMD} enable inventree +} + +function create_admin() { + # Create data for admin user + + if test -f "${SETUP_ADMIN_PASSWORD_FILE}"; then + echo "# Admin data already exists - skipping" + else + echo "# Creating admin user data" + + # Static admin data + export INVENTREE_ADMIN_USER=${INVENTREE_ADMIN_USER:-admin} + export INVENTREE_ADMIN_EMAIL=${INVENTREE_ADMIN_EMAIL:-admin@example.com} + + # Create password if not set + if [ -z "${INVENTREE_ADMIN_PASSWORD}" ]; then + openssl rand -base64 32 >${SETUP_ADMIN_PASSWORD_FILE} + export INVENTREE_ADMIN_PASSWORD=$(cat ${SETUP_ADMIN_PASSWORD_FILE}) + fi + fi +} + +function start_inventree() { + echo "# Starting InvenTree" + ${INIT_CMD} start inventree +} + +function stop_inventree() { + echo "# Stopping InvenTree" + ${INIT_CMD} stop inventree +} + +function update_or_install() { + + # Set permissions so app user can write there + chown ${APP_USER}:${APP_GROUP} ${APP_HOME} -R + + # Run update as app user + echo "# Updating InvenTree" + sudo -u ${APP_USER} --preserve-env=$SETUP_ENVS bash -c "cd ${APP_HOME} && invoke update | sed -e 's/^/# inv update| /;'" + + # Make sure permissions are correct again + echo "# Set permissions for data dir and media: ${DATA_DIR}" + chown ${APP_USER}:${APP_GROUP} ${DATA_DIR} -R + chown ${APP_USER}:${APP_GROUP} ${CONF_DIR} -R +} + +function set_env() { + echo "# Setting up InvenTree config values" + + inventree config:set INVENTREE_CONFIG_FILE=${INVENTREE_CONFIG_FILE} + + # Changing the config file + echo "# Writing the settings to the config file ${INVENTREE_CONFIG_FILE}" + # Media Root + sed -i s=#media_root:\ \'/home/inventree/data/media\'=media_root:\ \'${INVENTREE_MEDIA_ROOT}\'=g ${INVENTREE_CONFIG_FILE} + # Static Root + sed -i s=#static_root:\ \'/home/inventree/data/static\'=static_root:\ \'${INVENTREE_STATIC_ROOT}\'=g ${INVENTREE_CONFIG_FILE} + # Plugins enabled + sed -i s=plugins_enabled:\ False=plugins_enabled:\ ${INVENTREE_PLUGINS_ENABLED}=g ${INVENTREE_CONFIG_FILE} + # Plugin file + sed -i s=#plugin_file:\ \'/path/to/plugins.txt\'=plugin_file:\ \'${INVENTREE_PLUGIN_FILE}\'=g ${INVENTREE_CONFIG_FILE} + # Secret key file + sed -i s=#secret_key_file:\ \'/etc/inventree/secret_key.txt\'=secret_key_file:\ \'${INVENTREE_SECRET_KEY_FILE}\'=g ${INVENTREE_CONFIG_FILE} + # Debug mode + sed -i s=debug:\ True=debug:\ False=g ${INVENTREE_CONFIG_FILE} + + # Database engine + sed -i s=#ENGINE:\ sampleengine=ENGINE:\ ${INVENTREE_DB_ENGINE}=g ${INVENTREE_CONFIG_FILE} + # Database name + sed -i s=#NAME:\ \'/path/to/database\'=NAME:\ \'${INVENTREE_DB_NAME}\'=g ${INVENTREE_CONFIG_FILE} + # Database user + sed -i s=#USER:\ sampleuser=USER:\ ${INVENTREE_DB_USER}=g ${INVENTREE_CONFIG_FILE} + # Database password + sed -i s=#PASSWORD:\ samplepassword=PASSWORD:\ ${INVENTREE_DB_PASSWORD}=g ${INVENTREE_CONFIG_FILE} + # Database host + sed -i s=#HOST:\ samplehost=HOST:\ ${INVENTREE_DB_HOST}=g ${INVENTREE_CONFIG_FILE} + # Database port + sed -i s=#PORT:\ sampleport=PORT:\ ${INVENTREE_DB_PORT}=g ${INVENTREE_CONFIG_FILE} + + # Fixing the permissions + chown ${APP_USER}:${APP_GROUP} ${DATA_DIR} ${INVENTREE_CONFIG_FILE} +} + +function final_message() { + echo -e "####################################################################################" + echo -e "This InvenTree install uses nginx, the settings for the webserver can be found in" + echo -e "${SETUP_NGINX_FILE}" + echo -e "Try opening InvenTree with either\nhttp://localhost/ or http://${INVENTREE_IP}/\n" + echo -e "Admin user data:" + echo -e " Email: ${INVENTREE_ADMIN_EMAIL}" + echo -e " Username: ${INVENTREE_ADMIN_USER}" + echo -e " Password: ${INVENTREE_ADMIN_PASSWORD}" + echo -e "####################################################################################" +} diff --git a/contrib/packager.io/postinstall.sh b/contrib/packager.io/postinstall.sh new file mode 100755 index 0000000000..62efbc3b94 --- /dev/null +++ b/contrib/packager.io/postinstall.sh @@ -0,0 +1,49 @@ +#!/bin/bash +# +# packager.io postinstall script +# + +exec > >(tee ${APP_HOME}/log/setup_$(date +"%F_%H_%M_%S").log) 2>&1 + +PATH=${APP_HOME}/env/bin:${APP_HOME}/:/sbin:/bin:/usr/sbin:/usr/bin: + +# import functions +. ${APP_HOME}/contrib/packager.io/functions.sh + +# Envs that should be passed to setup commands +export SETUP_ENVS=PATH,APP_HOME,INVENTREE_MEDIA_ROOT,INVENTREE_STATIC_ROOT,INVENTREE_PLUGINS_ENABLED,INVENTREE_PLUGIN_FILE,INVENTREE_CONFIG_FILE,INVENTREE_SECRET_KEY_FILE,INVENTREE_DB_ENGINE,INVENTREE_DB_NAME,INVENTREE_DB_USER,INVENTREE_DB_PASSWORD,INVENTREE_DB_HOST,INVENTREE_DB_PORT,INVENTREE_ADMIN_USER,INVENTREE_ADMIN_EMAIL,INVENTREE_ADMIN_PASSWORD,SETUP_NGINX_FILE,SETUP_ADMIN_PASSWORD_FILE,SETUP_NO_CALLS,SETUP_DEBUG,SETUP_EXTRA_PIP + +# Get the envs +detect_local_env + +# default config +export CONF_DIR=/etc/inventree +export DATA_DIR=${APP_HOME}/data +# Setup variables +export SETUP_NGINX_FILE=${SETUP_NGINX_FILE:-/etc/nginx/sites-enabled/inventree.conf} +export SETUP_ADMIN_PASSWORD_FILE=${CONF_DIR}/admin_password.txt +export SETUP_NO_CALLS=${SETUP_NO_CALLS:-false} +# SETUP_DEBUG can be set to get debug info +# SETUP_EXTRA_PIP can be set to install extra pip packages + +# get base info +detect_envs +detect_docker +detect_initcmd +detect_ip + +# create processes +create_initscripts +create_admin + +# run updates +stop_inventree +update_or_install +# Write config file +if [ "${SETUP_CONF_LOADED}" = "true" ]; then + set_env +fi +start_inventree + +# show info +final_message diff --git a/runtime.txt b/runtime.txt new file mode 100644 index 0000000000..119ff10234 --- /dev/null +++ b/runtime.txt @@ -0,0 +1 @@ +python-3.10.7