1
0
mirror of https://github.com/daniviga/bite.git synced 2024-11-23 05:16:13 +01:00

Add testing suite (#18)

* Add some tests

* Add an example of NTP with encryption

* Enable TravisCI

* Run sims via Docker

* Improve simulators stage

* Final fix for travis

* Add README docs [skip ci]
This commit is contained in:
Daniele Viganò 2020-06-20 14:41:50 +02:00 committed by GitHub
parent b18030f5e5
commit 762605c5c4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 5465 additions and 3300 deletions

47
.travis.yml Normal file
View File

@ -0,0 +1,47 @@
language: python
dist: bionic
services:
- docker
before_install:
- pip -q install -U docker-compose
simulator: &simulator
stage: simulator
install:
- docker-compose -f docker/docker-compose.yml pull
- docker-compose -f docker/docker-compose.yml build
before_script:
- docker-compose -f docker/docker-compose.yml -f docker/edge/docker-compose.edge.yml up -d
- DOCKER_HOST='127.0.0.1:22375' docker-compose -f docker/docker-compose.yml -f docker/edge/docker-compose.edge.yml pull
- DOCKER_HOST='127.0.0.1:22375' docker-compose -f docker/docker-compose.yml -f docker/edge/docker-compose.edge.yml build
script:
- sleep 5 # warm-up
- sed -i 's/# IOT_SERIAL/IOT_SERIAL/g' docker/edge/docker-compose.modules.yml
- DOCKER_HOST='127.0.0.1:22375' docker-compose -f docker/edge/docker-compose.modules.yml up -d
- sleep 30 # collect some telemetry
- curl -sf http://localhost/telemetry/${IOT_TL}1234/last/
jobs:
include:
- stage: build
before_script:
- echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
script:
- docker-compose -f docker/docker-compose.yml build
- docker push daniviga/bite
- docker push daniviga/ntpd
if: branch = master
- stage: django
install:
- docker-compose -f docker/docker-compose.yml pull
- docker-compose -f docker/docker-compose.yml build
before_script:
- docker-compose -f docker/docker-compose.yml up -d
script:
- docker-compose -f docker/docker-compose.yml exec bite python manage.py test
- <<: *simulator
env: IOT_TL=http
- <<: *simulator
env: IOT_TL=mqtt

120
README.md
View File

@ -1,3 +1,123 @@
# BITE - Basic/IoT/Example # BITE - Basic/IoT/Example
Playing with IoT Playing with IoT
This project is for educational purposes only. It does not implement any authentication and/or encryption protocol, so it is not suitable for real production.
## Installation
### Requirements
- `docker-ce` or `moby`
- `docker-compose`
The project is compatible with Docker for Windows (using Linux executors), but it is advised to directly use a minimal Linux VM instead (via the preferred hypervisor).
The application stack is composed by the following components:
- [Django](https://www.djangoproject.com/) with [Django REST framework](https://www.django-rest-framework.org/) web application (running via `gunicorn` in production mode)
- `mqtt-to-db` custom daemon to dump telemetry into the timeseries database
- telemetry payload is stored as json object (via PostgreSQL JSON data type)
- [Timescale](https://www.timescale.com/) DB, a [PostgreSQL](https://www.postgresql.org/) database with a timeseries extension
- [Mosquitto](https://mosquitto.org/) MQTT broker (see alternatives below)
- [Nginx](http://nginx.org/) as ingress for HTTP (see alternative below)
- [Chrony](https://chrony.tuxfamily.org/) as NTP server (with optional `MD5` encryption)
## Deployment
### Development
```bash
docker-compose -f docker/docker-compose.yml up -d [--scale {bite,mqtt-to-db)=N]
```
It exposes:
- `http://localhost:80` (HTTP)
- `tcp://localhost:1883` (MQTT)
- `udp://localhost:123` (NTP)
Django runs with `DEBUG = True` and `SKIP_WHITELIST = True`
### Development with direct access to services
```bash
docker-compose -f docker/docker-compose.yml -f docker-compose.dev.yml up -d [--scale {bite,mqtt-to-db)=N]
```
It exposes:
- `http://localhost:80` (HTTP)
- `http://localhost:8080` (Django's `runserver`)
- `tcp://localhost:1883` (MQTT)
- `udp://localhost:123` (NTP)
- `tcp://localhost:5432` (PostgreSQL/Timescale)
Django runs with `DEBUG = True` and `SKIP_WHITELIST = True`
### Production
```bash
docker-compose -f docker/docker-compose.yml -f docker-compose.prod.yml up -d [--scale {bite,mqtt-to-db)=N]
```
It exposes:
- `http://localhost:80` (HTTP)
- `tcp://localhost:1883` (MQTT)
- `udp://localhost:123` (NTP)
Django runs with `DEBUG = False` and `SKIP_WHITELIST = False`
## Extra features
The project provides multiple modules that can be combined with the fore-mentioned configurations.
### Traefik
To use [Traefik](https://containo.us/traefik/) instead of Nginx use:
```bash
docker-compose -f docker/docker-compose.yml up -f docker/ingress/docker-compose.traefik.yml -d
```
### VerneMQ
A ~8x memory usage can be expected compared to Mosquitto.
To use [VerneMQ](https://vernemq.com/) instead of Mosquitto use:
```bash
docker-compose -f docker/docker-compose.yml up -f docker/mqtt/docker-compose.vernemq.yml -d
```
### RabbitMQ
RabbitMQ does provides AMQP protocol too, but ingestion on the application side is not implemented yet.
A ~10x memory usage can be expected compared to Mosquitto.
To use [RabbitMQ](https://www.rabbitmq.com/) (with the MQTT plugin enabled) instead of Mosquitto use:
```bash
docker-compose -f docker/docker-compose.yml up -f docker/mqtt/docker-compose.rabbitmq.yml -d
```
## EDGE gateway simulation (via dind)
An EDGE gateway, with containers as modules, may be simulated via dind (docker-in-docker).
### Start the EDGE
```bash
docker-compose -f docker/docker-compose.yml up -f docker/edge/docker-compose.edge.yml -d
```
### Run the modules inside the EDGE
```bash
DOCKER_HOST='127.0.0.1:22375' docker-compose -f docker-compose.modules.yml up -d [--scale {device-http,device-mqtt}=N]
```
## Ardunio
A simple Arduino UNO sketch is provided in the `arduino/tempLightSensor` folder. The sketch reads temperature and light from sensors. The simple schematic is:
![tempLightSensor](./arduino/tempLightSensor/tempLightSensor.svg)
The sketch does require an Ethernet shield and a bunch of libraries which are available as git submodules under `arduino/libraries`.
Be advised that some libraries (notably the NTP one) are customized.
Configuration parameters are stored and retrieved from the EEPROM. An helper sketch to update the EEPROM is available under `arduino/eeprom_prog`
An `ESP32` board (or similar Arduino) may be used, with some adaptions, too.

Binary file not shown.

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 291 KiB

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 292 KiB

View File

@ -1,3 +1,26 @@
from django.test import TestCase from django.test import TestCase, Client
from api.models import Device, WhiteList
# Create your tests here.
class ApiTestCase(TestCase):
c = Client()
def setUp(self):
WhiteList.objects.create(serial='test1234')
Device.objects.create(serial='test1234')
def test_no_whitelist(self):
response = self.c.post('/api/device/subscribe/',
{'serial': 'test12345'})
self.assertEqual(response.status_code, 400)
def test_subscribe_post(self):
WhiteList.objects.create(serial='test12345')
response = self.c.post('/api/device/subscribe/',
{'serial': 'test12345'})
self.assertEqual(response.status_code, 201)
def test_subscribe_get(self):
response = self.c.get('/api/device/list/')
self.assertEqual(
response.json()[0]['serial'], 'test1234')

View File

@ -0,0 +1,20 @@
# Generated by Django 3.0.7 on 2020-06-19 16:27
import django.contrib.postgres.fields.jsonb
from django.db import migrations
import telemetry.models
class Migration(migrations.Migration):
dependencies = [
('telemetry', '0007_telemetry_transport'),
]
operations = [
migrations.AlterField(
model_name='telemetry',
name='payload',
field=django.contrib.postgres.fields.jsonb.JSONField(validators=[telemetry.models.telemetry_validation]),
),
]

View File

@ -1,10 +1,16 @@
from django.db import models from django.db import models
from django.core.validators import MinValueValidator from django.core.validators import MinValueValidator
from django.core.exceptions import ValidationError
from django.contrib.postgres.fields import JSONField from django.contrib.postgres.fields import JSONField
from api.models import Device from api.models import Device
def telemetry_validation(value):
if not value:
raise ValidationError("No telemetry has been sent")
class Telemetry(models.Model): class Telemetry(models.Model):
device = models.ForeignKey(Device, on_delete=models.CASCADE) device = models.ForeignKey(Device, on_delete=models.CASCADE)
time = models.DateTimeField(primary_key=True, auto_now_add=True) time = models.DateTimeField(primary_key=True, auto_now_add=True)
@ -14,7 +20,7 @@ class Telemetry(models.Model):
clock = models.IntegerField( clock = models.IntegerField(
validators=[MinValueValidator(0)], validators=[MinValueValidator(0)],
null=True) null=True)
payload = JSONField() payload = JSONField(validators=[telemetry_validation])
class Meta: class Meta:
ordering = ['-time', 'device'] ordering = ['-time', 'device']

View File

@ -1,3 +1,53 @@
from django.test import TestCase import json
from django.test import TestCase, Client
from api.models import Device, WhiteList
# Create your tests here.
class ApiTestCase(TestCase):
c = Client()
payload = {
'id': 'sensor.server.domain',
'light': 434,
'temperature': {
'celsius': 27.02149,
'raw': 239,
'volts': 0.770215
}
}
telemetry = {
'device': 'test1234',
'clock': 1591194712,
'payload': json.dumps(payload)
}
def setUp(self):
WhiteList.objects.create(serial='test1234')
Device.objects.create(serial='test1234')
def test_no_device(self):
fake_telemetry = dict(self.telemetry) # make a copy of the dict
fake_telemetry['device'] = '1234test'
response = self.c.post('/telemetry/', fake_telemetry)
self.assertEqual(response.status_code, 400)
def test_empty_telemetry(self):
fake_telemetry = dict(self.telemetry) # make a copy of the dict
fake_telemetry['payload'] = ''
response = self.c.post('/telemetry/', fake_telemetry)
self.assertEqual(response.status_code, 400)
def test_telemetry_post(self):
response = self.c.post('/telemetry/', self.telemetry)
self.assertEqual(response.status_code, 201)
def test_telemetry_get(self):
response = self.c.post('/telemetry/', self.telemetry)
response = self.c.get('/telemetry/test1234/last/')
self.assertEqual(
response.json()['device'], 'test1234')
self.assertEqual(
response.json()['transport'], 'http')
self.assertJSONEqual(
json.dumps(response.json()['payload']), self.payload)

View File

@ -2,12 +2,11 @@ version: "3.7"
services: services:
edge-host: edge-host:
<<: *service_default
image: docker:dind image: docker:dind
privileged: true privileged: true
environment: environment:
DOCKER_TLS_CERTDIR: DOCKER_TLS_CERTDIR:
# networks: networks:
# - net - net
ports: ports:
- "127.0.0.1:22375:2375" - "127.0.0.1:22375:2375"

View File

@ -1,8 +1,5 @@
version: "3.7" version: "3.7"
networks:
localnet:
x-op-service-default: &service_default x-op-service-default: &service_default
restart: always restart: always
init: true init: true
@ -15,12 +12,11 @@ services:
context: ../simulator context: ../simulator
image: daniviga/bite-device-simulator image: daniviga/bite-device-simulator
environment: environment:
IOT_HTTP: "http://192.168.10.123:8000" IOT_HTTP: "http://ingress"
# IOT_SERIAL: "abcd1234" # IOT_SERIAL: "http1234"
# IOT_DELAY: 10 # IOT_DELAY: 10
IOT_DEBUG: 1 IOT_DEBUG: 1
networks: network_mode: "host"
- localnet
device-mqtt: device-mqtt:
<<: *service_default <<: *service_default
@ -28,11 +24,10 @@ services:
context: ../simulator context: ../simulator
image: daniviga/bite-device-simulator image: daniviga/bite-device-simulator
environment: environment:
IOT_HTTP: "http://192.168.10.123:8000" IOT_HTTP: "http://ingress"
IOT_MQTT: "192.168.10.123:1883" IOT_MQTT: "broker:1883"
# IOT_SERIAL: "abcd1234" # IOT_SERIAL: "mqtt1234"
# IOT_DELAY: 10 # IOT_DELAY: 10
IOT_DEBUG: 1 IOT_DEBUG: 1
command: ["/opt/bite/device_simulator.py", "-t", "mqtt"] command: ["/opt/bite/device_simulator.py", "-t", "mqtt"]
networks: network_mode: "host"
- localnet

View File

@ -0,0 +1,9 @@
FROM daniviga/ntpd
COPY ./chrony.keys /etc/chrony/chrony.keys
RUN echo "keyfile /etc/chrony/chrony.keys" \
>> /etc/chrony/chrony.conf
EXPOSE 123/udp
ENTRYPOINT ["chronyd", "-d", "-s", "-x"]

View File

@ -0,0 +1 @@
1 MD5 HEX:D8ACA3A254EF59F83C35CEEFF6EBD2C7C67DB3D9