Skip to content

Commit

Permalink
Merge pull request #622 from thorrak/dev
Browse files Browse the repository at this point in the history
Docker Support
  • Loading branch information
thorrak authored Apr 5, 2021
2 parents 8a7f920 + 361f72e commit d347b62
Show file tree
Hide file tree
Showing 101 changed files with 3,985 additions and 1,305 deletions.
6 changes: 6 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.*
!.gitignore
!.git
!.coveragerc
!.env
!.pylintrc
9 changes: 9 additions & 0 deletions .envs/.local/.django
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# General
# ------------------------------------------------------------------------------
USE_DOCKER=True
IPYTHONDIR=/app/.ipython
# Redis
# ------------------------------------------------------------------------------
REDIS_URL=redis://redis:6379/0


7 changes: 7 additions & 0 deletions .envs/.local/.postgres
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# PostgreSQL
# ------------------------------------------------------------------------------
POSTGRES_HOST=postgres
POSTGRES_PORT=5432
POSTGRES_DB=fermentrack
POSTGRES_USER=postgres_user
POSTGRES_PASSWORD=postgres_password
44 changes: 44 additions & 0 deletions .envs/.prod-sample/.django
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# General
# ------------------------------------------------------------------------------
# DJANGO_READ_DOT_ENV_FILE=True
DJANGO_SETTINGS_MODULE=fermentrack_django.settings
DJANGO_SECRET_KEY={secret_key}
DJANGO_ADMIN_URL={admin_url}/
DJANGO_ALLOWED_HOSTS=*
USE_DOCKER=True

# Security
# ------------------------------------------------------------------------------
# TIP: better off using DNS, however, redirect is OK too
DJANGO_SECURE_SSL_REDIRECT=False

# Email
# ------------------------------------------------------------------------------
DJANGO_SERVER_EMAIL=

MAILGUN_API_KEY=
MAILGUN_DOMAIN=


# django-allauth
# ------------------------------------------------------------------------------
DJANGO_ACCOUNT_ALLOW_REGISTRATION=True

# Gunicorn
# ------------------------------------------------------------------------------
WEB_CONCURRENCY=4

# Sentry
# ------------------------------------------------------------------------------
SENTRY_DSN=


# Redis
# ------------------------------------------------------------------------------
#REDIS_URL=redis://redis:6379/0
REDIS_URL=redis://127.0.0.1:6379/0


# Version Management
# ------------------------------------------------------------------------------
ENV_DJANGO_VERSION=1
11 changes: 11 additions & 0 deletions .envs/.prod-sample/.postgres
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# PostgreSQL
# ------------------------------------------------------------------------------
POSTGRES_HOST=127.0.0.1
POSTGRES_PORT=5432
POSTGRES_DB=fermentrack
POSTGRES_USER={postgres_user}
POSTGRES_PASSWORD={postgres_password}

# Version Management
# ------------------------------------------------------------------------------
ENV_POSTGRES_VERSION=1
41 changes: 41 additions & 0 deletions .github/workflows/docker-hub.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: 'Build & Push to Docker Hub'

on:
push:
branches:
- dev
- master

jobs:
buildx:
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v2
-
name: Set up QEMU
uses: docker/setup-qemu-action@v1
with:
platforms: all
-
name: Dockerhub login
env:
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
run: |
echo "${DOCKER_PASSWORD}" | docker login --username ${DOCKER_USERNAME} --password-stdin
-
name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v1
with:
version: latest
-
name: Build dockerfile (armv7/amd64/arm64)
run: |
docker buildx build \
--platform=linux/arm/v7,linux/amd64 \
--output "type=image,push=true" \
--file ./compose/production/django/Dockerfile . \
--tag jdbeeler/fermentrack:latest
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,9 @@ config.ini
*.bin
*.hex

.envs/
!.envs/.local/
!.envs/.prod-sample/

#ignore generated files
collected_static/*
4 changes: 2 additions & 2 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
MIT License

Copyright (c) 2016-2017 John Beeler
Copyright (c) 2016-2017 Fredrik Steen
Copyright (c) 2016-2020 John Beeler
Copyright (c) 2016-2020 Fredrik Steen

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
19 changes: 13 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

Fermentrack is an application designed to manage and log fermentation temperatures and specific gravity. It acts as a complete replacement for the web interface used by BrewPi written in Python using the Django web framework. It also can track Tilt Hydrometers and iSpindel specific gravity sensors - both alongside BrewPi controllers as well as by themselves.

Fermentrack is Python-based, does not require PHP5, and works with Raspbian Stretch or later including on Raspberry Pi 3 B+. Fermentrack is intended to be installed on a fresh installation of Raspbian and will conflict with brewpi-www if installed on the same device.
Fermentrack is Python-based, does not require PHP5, and works with Raspbian Buster or later including on Raspberry Pi 3 B+. Fermentrack is intended to be installed on a fresh installation of Raspbian (or another Debian-based OS for x86/x64 installations) and will conflict with brewpi-www if installed on the same device.

Want to see it in action? See videos of key Fermentrack features on [YouTube](https://www.youtube.com/playlist?list=PLCs4FqrNRHd00wsfsP7cTs83e19S2-Atf)!

Expand All @@ -32,7 +32,8 @@ One of the key reasons to write Fermentrack was to incorporate features that are
* Native support (including mDNS autodetection) for WiFi controllers
* Integrated specific gravity sensor support, including for Tilt Hydrometers and iSpindel devices

A full table of controllers/expected hardware availability is available [https://fermentrack.readthedocs.io/](https://fermentrack.readthedocs.io/).
A full table of controllers/expected hardware availability is available [in the documentation](http://docs.fermentrack.com/en/master/hardware.html).


## Installation & Documentation

Expand All @@ -48,9 +49,15 @@ Full documentation for Fermentrack (including complete installation instructions

## Requirements

* Raspberry Pi Zero, 2 B, or 3 /w Internet Connection
* Fresh Raspbian install (Stretch or later preferred, Jessie supported)
* 1GB of free space available
Fermentrack is designed to be run as part of a Docker compose stack and should be able to run on most armv7/x86/x64-based systems that are capable of running docker-compose. Most users, however, will install Fermentrack on a Raspberry Pi.

**For Raspberry Pi-based Installs:**
* Raspberry Pi 2 B, 3, 4, 400, or later /w Internet Connection
* 2GB of free space available
* Fresh Raspberry Pi OS (Raspbian) install (Buster or later) preferred


**PLEASE NOTE** - Fermentrack is currently intended to be installed on a fresh installation of Raspbian. It is **not** intended to be installed alongside brewpi-www and will conflict with the apache server brewpi-www installs.
**For x86/x64-based Installs:**
* Debian-based systems preferred (e.g. Ubuntu, Debian, etc)
* 2GB of free space available

18 changes: 13 additions & 5 deletions app/api/clog.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
from django.http import HttpResponse
from django.conf import settings
from app.models import BrewPiDevice
from gravity.models import GravitySensor
from pathlib import Path


def get_filepath_to_log(device_type, logfile="", device_id=None):
def get_filepath_to_log(device_type, logfile="", device_id=None) -> Path or None:
# get_filepath_to_log is being broken out so that we can use it in help/other templates to display which log file
# is being loaded
if device_type == "brewpi":
Expand All @@ -22,13 +22,17 @@ def get_filepath_to_log(device_type, logfile="", device_id=None):
log_filename = 'fermentrack-stderr.log'
elif device_type == "ispindel":
log_filename = 'ispindel_raw_output.log'
elif device_type == "huey":
log_filename = f'huey-{logfile}.log' # Logfile is stderr or stdout
elif device_type == "upgrade":
log_filename = 'upgrade.log'
elif device_type == "circusd":
log_filename = 'circusd.log'
else:
return None

# Once we've determined the filename from logfile and device_type, let's open it up & read it in
logfile_path = os.path.join(settings.BASE_DIR, 'log', log_filename)
logfile_path = settings.ROOT_DIR / 'log' / log_filename
return logfile_path


Expand All @@ -48,9 +52,13 @@ def get_device_log_combined(req, return_type, device_type, logfile, device_id=No
# Device_type determines the other part of the logfile to read. Valid options are:
# brewpi - A BrewPiDevice object
# gravity - A specific gravity sensor object
# spawner - the circus spawner
# spawner - the circus spawner (not the daemon)
# fermentrack - Fermentrack itself
valid_device_types = ['brewpi', 'gravity', 'spawner', 'fermentrack', 'ispindel', 'upgrade']
# ispindel - iSpindel raw log
# upgrade - The log of the upgrade process (from Git)
# huey - The Huey (task manager) logs
# circusd - The log for Circusd itself
valid_device_types = ['brewpi', 'gravity', 'spawner', 'fermentrack', 'ispindel', 'upgrade', 'huey', 'circusd']
if device_type not in valid_device_types:
# TODO - Log this
return HttpResponse("Cannot read log files for devices of type {} ".format(device_type), status=500)
Expand Down
27 changes: 7 additions & 20 deletions app/beer_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,7 @@ def beer_create(request, device_id):
else:
messages.success(request, "Beer {} already exists - assigning to device".format(form.cleaned_data['beer_name']))

if form.cleaned_data['device'].active_beer != new_beer:
form.cleaned_data['device'].active_beer = new_beer
form.cleaned_data['device'].save()

form.cleaned_data['device'].start_new_brew()
form.cleaned_data['device'].start_new_brew(new_beer)

else:
messages.error(request, "<p>Unable to create beer</p> %s" % form.errors['__all__'])
Expand All @@ -78,25 +74,16 @@ def beer_logging_status(request, device_id, logging_status):

# logging_status is the target status. Differs a bit from how the action is referred to in brewpi.py
if logging_status == BrewPiDevice.DATA_LOGGING_ACTIVE:
response = this_device.manage_logging(status='resume')
if response['status'] == 0:
messages.success(request, "Data logging has been resumed")
else:
messages.error(request, response['statusMessage'])
this_device.manage_logging(status='resume')
messages.success(request, "Request to resume logging sent to controller")

elif logging_status == BrewPiDevice.DATA_LOGGING_PAUSED:
response = this_device.manage_logging(status='pause')
if response['status'] == 0:
messages.success(request, "Data logging has been paused")
else:
messages.error(request, response['statusMessage'])
this_device.manage_logging(status='pause')
messages.success(request, "Request to pause logging sent to controller")

elif logging_status == BrewPiDevice.DATA_LOGGING_STOPPED:
response = this_device.manage_logging(status='stop')
if response['status'] == 0:
messages.success(request, "Data logging has been stopped")
else:
messages.error(request, response['statusMessage'])
this_device.manage_logging(status='stop')
messages.success(request, "Request to stop logging sent to controller")

else:
messages.error(request, "Requested status is invalid!")
Expand Down
16 changes: 9 additions & 7 deletions app/circus_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
from django.http import JsonResponse
from app.models import BrewPiDevice
from lib.ftcircus.client import CircusException
from django.db import transaction

logger = logging.getLogger(__name__)


def _JsonResponseIndent(request):
"""Simple function that returns JsonResponse with indent 4"""
return JsonResponse(request, json_dumps_params={'indent': 4})
Expand Down Expand Up @@ -46,13 +48,13 @@ def start_brewpi_device(request, device_id):
logger.error("Error loading device with ID: {}".format(device_id), exc_info=True)
ret = {'status': 'error', 'message': 'Unable to load device id: {}'.format(device_id)}
return _JsonResponseIndent(ret)
try:
active_device.start_process()
except (CircusException) as cerror:
logger.error("Error during circus call", exc_info=True)
ret = {'status': 'error', 'message': 'Error during circus call: {}'.format(cerror)}
return _JsonResponseIndent(ret)
ret = {'status': 'ok', 'message': 'process signaled to start'}

# Due to the way that Django handles transactions, starting the process here can cause a race condition with a
# transaction editing/creating the BrewPiDevice getting written to the database. We'll wrap the start process call
# in "on_commit" to ensure that it is called after the commit takes place.
transaction.on_commit(active_device.start_process)

ret = {'status': 'ok', 'message': 'process queued to start'}
return _JsonResponseIndent(ret)

@login_required
Expand Down
4 changes: 2 additions & 2 deletions app/connection_debug.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ def test_telnet(hostname, port):
return False, False, None

try:
tn.write("n\r\n")
version_string = tn.read_until("}",3)
tn.write(b"n\r\n")
version_string = tn.read_until(b"}", 3)
except:
return True, False, None
return True, True, version_string
Expand Down
41 changes: 18 additions & 23 deletions app/device_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import random


class DeviceForm(forms.Form):
class BrewPiDeviceModifyForm(forms.Form):
device_name = forms.CharField(max_length=48, help_text="Unique name for this device",
widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Device Name'}))

Expand Down Expand Up @@ -71,34 +71,14 @@ class DeviceForm(forms.Form):
help_text="Whether to autodetect the appropriate serial port " +
"using the device's USB serial number")

# modify_not_create is a flag that impacts the device name checking in clean_device_name()
modify_not_create = forms.BooleanField(widget=forms.HiddenInput, initial=False, required=False)

def clean_device_name(self):
if 'device_name' not in self.cleaned_data:
raise forms.ValidationError("A device name must be specified")
else:
device_name = self.cleaned_data['device_name']

# Name uniqueness is enforced on the sql CREATE, but since we're not using a ModelForm this form won't check to
# see if the name is actually uniqye. That said - we only need to check if we're creating the object. We do not
# need to check if we're modifying the object.
if 'modify_not_create' not in self.cleaned_data:
modify_not_create = False
else:
modify_not_create = self.cleaned_data['modify_not_create']

if not modify_not_create: # If we're creating, not modifying
try:
existing_device = BrewPiDevice.objects.get(device_name=device_name)
raise forms.ValidationError("A device already exists with the name {}".format(device_name))

except ObjectDoesNotExist:
# There was no existing device - we're good.
return device_name
else:
# For modifications, we always return the device name
return device_name
# For modifications, we always return the device name
return device_name

def clean(self):
cleaned_data = self.cleaned_data
Expand Down Expand Up @@ -177,6 +157,21 @@ def clean(self):
return cleaned_data


class BrewPiDeviceCreateForm(BrewPiDeviceModifyForm):
def clean_device_name(self):
if 'device_name' not in self.cleaned_data:
raise forms.ValidationError("A device name must be specified")
else:
device_name = self.cleaned_data['device_name']
try:
existing_device = BrewPiDevice.objects.get(device_name=device_name)
raise forms.ValidationError("A device already exists with the name {}".format(device_name))

except ObjectDoesNotExist:
# There was no existing device - we're good.
return device_name


class OldCCModelForm(ModelForm):
class Meta:
model = OldControlConstants
Expand Down
Loading

0 comments on commit d347b62

Please sign in to comment.