diff --git a/practicedocker/imgs/screen1.png b/practicedocker/imgs/screen1.png new file mode 100644 index 0000000..ce3c9d6 Binary files /dev/null and b/practicedocker/imgs/screen1.png differ diff --git a/practicedocker/imgs/screen2.png b/practicedocker/imgs/screen2.png new file mode 100644 index 0000000..b7b0769 Binary files /dev/null and b/practicedocker/imgs/screen2.png differ diff --git a/practicedocker/imgs/screen3.png b/practicedocker/imgs/screen3.png new file mode 100644 index 0000000..3d7535a Binary files /dev/null and b/practicedocker/imgs/screen3.png differ diff --git a/practicedocker/imgs/screen4.png b/practicedocker/imgs/screen4.png new file mode 100644 index 0000000..a9cb25d Binary files /dev/null and b/practicedocker/imgs/screen4.png differ diff --git a/practicedocker/imgs/screen5.png b/practicedocker/imgs/screen5.png new file mode 100644 index 0000000..02305ec Binary files /dev/null and b/practicedocker/imgs/screen5.png differ diff --git a/practicedocker/linux_a/docker-compose.yml b/practicedocker/linux_a/docker-compose.yml new file mode 100644 index 0000000..903a2de --- /dev/null +++ b/practicedocker/linux_a/docker-compose.yml @@ -0,0 +1,82 @@ +services: + temp_1: + image: rodjonn/kulakov-sensor-simulator:latest + container_name: sensor_temp_1 + environment: + - SIM_TYPE=temperature + - SIM_NAME=temp_1 + - SIM_HOST=mosquitto + - SIM_PORT=1883 + - SIM_PERIOD=2 + networks: + - iot_network + restart: unless-stopped + + temp_2: + image: rodjonn/kulakov-sensor-simulator:latest + container_name: sensor_temp_2 + environment: + - SIM_TYPE=temperature + - SIM_NAME=temp_2 + - SIM_HOST=mosquitto + - SIM_PORT=1883 + - SIM_PERIOD=3 + networks: + - iot_network + restart: unless-stopped + + pressure_1: + image: rodjonn/kulakov-sensor-simulator:latest + container_name: sensor_pressure_1 + environment: + - SIM_TYPE=pressure + - SIM_NAME=pressure_1 + - SIM_HOST=mosquitto + - SIM_PORT=1883 + - SIM_PERIOD=5 + networks: + - iot_network + restart: unless-stopped + + current_1: + image: rodjonn/kulakov-sensor-simulator:latest + container_name: sensor_current_1 + environment: + - SIM_TYPE=current + - SIM_NAME=current_1 + - SIM_HOST=mosquitto + - SIM_PORT=1883 + - SIM_PERIOD=2 + networks: + - iot_network + restart: unless-stopped + + humidity_1: + image: rodjonn/kulakov-sensor-simulator:latest + container_name: sensor_humidity_1 + environment: + - SIM_TYPE=humidity + - SIM_NAME=humidity_1 + - SIM_HOST=mosquitto + - SIM_PORT=1883 + - SIM_PERIOD=4 + networks: + - iot_network + restart: unless-stopped + + humidity_2: + image: rodjonn/kulakov-sensor-simulator:latest + container_name: sensor_humidity_2 + environment: + - SIM_TYPE=humidity + - SIM_NAME=humidity_2 + - SIM_HOST=mosquitto + - SIM_PORT=1883 + - SIM_PERIOD=6 + networks: + - iot_network + restart: unless-stopped + +networks: + iot_network: + external: true diff --git a/practicedocker/linux_a/simulator/Dockerfile b/practicedocker/linux_a/simulator/Dockerfile new file mode 100644 index 0000000..cd0c8f5 --- /dev/null +++ b/practicedocker/linux_a/simulator/Dockerfile @@ -0,0 +1,6 @@ +FROM python:3.12-alpine3.19 +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +COPY . . +CMD ["python", "main.py"] diff --git a/practicedocker/linux_a/simulator/main.py b/practicedocker/linux_a/simulator/main.py new file mode 100644 index 0000000..0689d46 --- /dev/null +++ b/practicedocker/linux_a/simulator/main.py @@ -0,0 +1,49 @@ +import json +import os +import time + +import paho.mqtt.client as mqtt + +from sensor import SENSOR_TYPES + +BROKER_HOST = os.getenv("SIM_HOST", "mosquitto") +BROKER_PORT = int(os.getenv("SIM_PORT", "1883")) +CLIENT_NAME = os.getenv("SIM_NAME", "sensor_1") +SIM_PERIOD = float(os.getenv("SIM_PERIOD", "2.0")) +SIM_TYPE = os.getenv("SIM_TYPE", "temperature") + +sensor_class = SENSOR_TYPES.get(SIM_TYPE, SENSOR_TYPES["temperature"]) +sensor = sensor_class(CLIENT_NAME) +topic = f"sensors/{sensor.type}/{CLIENT_NAME}" + + +def on_publish(client, userdata, mid): + print(f"[{CLIENT_NAME}] published mid={mid} -> {topic}", flush=True) + + +def connect_with_retry(client, host, port, retries=15, delay=3): + for attempt in range(1, retries + 1): + try: + client.connect(host, port, keepalive=60) + print(f"[{CLIENT_NAME}] Connected to {host}:{port}", flush=True) + return + except Exception as exc: + print(f"[{CLIENT_NAME}] Attempt {attempt}/{retries} failed: {exc}. Retry in {delay}s...", flush=True) + time.sleep(delay) + raise RuntimeError(f"Could not connect to {host}:{port} after {retries} attempts") + + +client = mqtt.Client(client_id=CLIENT_NAME) +client.on_publish = on_publish + +connect_with_retry(client, BROKER_HOST, BROKER_PORT) +client.loop_start() + +print(f"[{CLIENT_NAME}] Sensor type={SIM_TYPE}, topic={topic}, period={SIM_PERIOD}s", flush=True) + +while True: + data = sensor.read() + payload = json.dumps(data) + client.publish(topic, payload, qos=1) + print(f"[{CLIENT_NAME}] {payload}", flush=True) + time.sleep(SIM_PERIOD) diff --git a/practicedocker/linux_a/simulator/requirements.txt b/practicedocker/linux_a/simulator/requirements.txt new file mode 100644 index 0000000..5f9c1b2 --- /dev/null +++ b/practicedocker/linux_a/simulator/requirements.txt @@ -0,0 +1 @@ +paho-mqtt==1.6.1 diff --git a/practicedocker/linux_a/simulator/sensor.py b/practicedocker/linux_a/simulator/sensor.py new file mode 100644 index 0000000..c8a7431 --- /dev/null +++ b/practicedocker/linux_a/simulator/sensor.py @@ -0,0 +1,80 @@ +import random +import time +from abc import ABC, abstractmethod + + +class Sensor(ABC): + unit: str = "" + + def __init__(self, name: str): + self.name = name + self.type = self.__class__.__name__.replace("Sensor", "").lower() + + @abstractmethod + def generate(self) -> float: + pass + + def read(self) -> dict: + return { + "sensor_id": self.name, + "type": self.type, + "value": round(self.generate(), 2), + "unit": self.unit, + "timestamp": time.strftime("%Y-%m-%dT%H:%M:%S"), + } + + +class TemperatureSensor(Sensor): + unit = "C" + + def __init__(self, name: str): + super().__init__(name) + self._value = 20.0 + + def generate(self) -> float: + self._value += random.uniform(-0.5, 0.5) + return max(-10.0, min(50.0, self._value)) + + +class PressureSensor(Sensor): + unit = "hPa" + + def __init__(self, name: str): + super().__init__(name) + self._value = 1013.25 + + def generate(self) -> float: + self._value += random.uniform(-1.0, 1.0) + return max(950.0, min(1080.0, self._value)) + + +class CurrentSensor(Sensor): + unit = "A" + + def __init__(self, name: str): + super().__init__(name) + self._value = 5.0 + + def generate(self) -> float: + self._value += random.uniform(-0.2, 0.2) + return max(0.0, min(20.0, self._value)) + + +class HumiditySensor(Sensor): + unit = "%" + + def __init__(self, name: str): + super().__init__(name) + self._value = 50.0 + + def generate(self) -> float: + self._value += random.uniform(-1.0, 1.0) + return max(0.0, min(100.0, self._value)) + + +SENSOR_TYPES = { + "temperature": TemperatureSensor, + "pressure": PressureSensor, + "current": CurrentSensor, + "humidity": HumiditySensor, +} diff --git a/practicedocker/linux_b/docker-compose.yml b/practicedocker/linux_b/docker-compose.yml new file mode 100644 index 0000000..d6f875b --- /dev/null +++ b/practicedocker/linux_b/docker-compose.yml @@ -0,0 +1,15 @@ +services: + mosquitto: + image: eclipse-mosquitto:2.0 + container_name: mosquitto + ports: + - "1883:1883" + volumes: + - ./mosquitto/mosquitto.conf:/mosquitto/config/mosquitto.conf + networks: + - iot_network + restart: unless-stopped + +networks: + iot_network: + external: true diff --git a/practicedocker/linux_b/mosquitto/mosquitto.conf b/practicedocker/linux_b/mosquitto/mosquitto.conf new file mode 100644 index 0000000..c8348ac --- /dev/null +++ b/practicedocker/linux_b/mosquitto/mosquitto.conf @@ -0,0 +1,2 @@ +listener 1883 +allow_anonymous true diff --git a/practicedocker/linux_c/docker-compose.yml b/practicedocker/linux_c/docker-compose.yml new file mode 100644 index 0000000..57ef4d8 --- /dev/null +++ b/practicedocker/linux_c/docker-compose.yml @@ -0,0 +1,50 @@ +services: + influxdb: + image: influxdb:1.8 + container_name: influxdb + ports: + - "8086:8086" + environment: + - INFLUXDB_DB=sensors + - INFLUXDB_HTTP_AUTH_ENABLED=false + volumes: + - influxdb_data:/var/lib/influxdb + networks: + - iot_network + restart: unless-stopped + + telegraf: + image: telegraf:1.28 + container_name: telegraf + volumes: + - ./telegraf/telegraf.conf:/etc/telegraf/telegraf.conf:ro + depends_on: + - influxdb + networks: + - iot_network + restart: unless-stopped + + grafana: + image: grafana/grafana:10.2.0 + container_name: grafana + ports: + - "3000:3000" + environment: + - GF_SECURITY_ADMIN_PASSWORD=admin + - GF_SECURITY_ADMIN_USER=admin + volumes: + - grafana_data:/var/lib/grafana + - ./grafana/provisioning:/etc/grafana/provisioning + depends_on: + - influxdb + networks: + - iot_network + restart: unless-stopped + +volumes: + influxdb_data: + grafana_data: + +networks: + iot_network: + external: true diff --git a/practicedocker/linux_c/grafana/provisioning/datasources/influxdb.yaml b/practicedocker/linux_c/grafana/provisioning/datasources/influxdb.yaml new file mode 100644 index 0000000..73326bd --- /dev/null +++ b/practicedocker/linux_c/grafana/provisioning/datasources/influxdb.yaml @@ -0,0 +1,9 @@ +apiVersion: 1 + +datasources: + - name: InfluxDB + type: influxdb + url: http://influxdb:8086 + database: sensors + isDefault: true + access: proxy diff --git a/practicedocker/linux_c/telegraf/telegraf.conf b/practicedocker/linux_c/telegraf/telegraf.conf new file mode 100644 index 0000000..f96f1d9 --- /dev/null +++ b/practicedocker/linux_c/telegraf/telegraf.conf @@ -0,0 +1,19 @@ +[agent] + interval = "5s" + flush_interval = "5s" + hostname = "telegraf" + +[[inputs.mqtt_consumer]] + servers = ["tcp://mosquitto:1883"] + topics = ["sensors/#"] + qos = 1 + data_format = "json" + json_name_key = "type" + tag_keys = ["sensor_id", "type", "unit"] + json_time_key = "timestamp" + json_time_format = "2006-01-02T15:04:05" + +[[outputs.influxdb]] + urls = ["http://influxdb:8086"] + database = "sensors" + skip_database_creation = false diff --git a/practicedocker/report.md b/practicedocker/report.md new file mode 100644 index 0000000..fc607c6 --- /dev/null +++ b/practicedocker/report.md @@ -0,0 +1,305 @@ +# Отчёт по практической работе «Docker» + +**Студент:** Кулаков Родион Андреевич +**Дата выполнения:** 23.05.2026 +**Docker Hub:** [rodjonn/kulakov-sensor-simulator](https://hub.docker.com/r/rodjonn/kulakov-sensor-simulator) + +--- + +## 1. Задание + +Развернуть IoT-систему сбора и визуализации данных с датчиков на базе Docker. Система состоит из трёх логических машин: + +- **Linux A** — 6 контейнеров-симуляторов датчиков, публикуют данные по протоколу MQTT +- **Linux B** — MQTT-брокер Mosquitto, принимает и раздаёт сообщения +- **Linux C** — стек InfluxDB + Telegraf + Grafana: сбор, хранение и визуализация данных + +--- + +## 2. Архитектура системы + +``` +[Linux A — симуляторы датчиков] + sensor_temp_1 (temperature, каждые 2с) + sensor_temp_2 (temperature, каждые 3с) + sensor_pressure_1 (pressure, каждые 5с) + sensor_current_1 (current, каждые 2с) + sensor_humidity_1 (humidity, каждые 4с) + sensor_humidity_2 (humidity, каждые 6с) + │ + │ MQTT (порт 1883) + ▼ +[Linux B — Mosquitto broker] + topics: sensors/# + │ + │ MQTT subscribe + ▼ +[Linux C — данные и визуализация] + Telegraf → парсит JSON, пишет в InfluxDB + InfluxDB → хранит временные ряды + Grafana → строит графики (http://localhost:3000) +``` + +Все контейнеры объединены в общую Docker-сеть `iot_network`. + +--- + +## 3. Структура проекта + +``` +dockerPractice/ +├── linux_a/ +│ ├── simulator/ +│ │ ├── sensor.py — базовый класс Sensor + 4 подкласса +│ │ ├── main.py — MQTT-клиент +│ │ ├── requirements.txt +│ │ └── Dockerfile +│ └── docker-compose.yml — 6 контейнеров датчиков +├── linux_b/ +│ ├── mosquitto/ +│ │ └── mosquitto.conf +│ └── docker-compose.yml +├── linux_c/ +│ ├── telegraf/ +│ │ └── telegraf.conf +│ ├── grafana/ +│ │ └── provisioning/datasources/influxdb.yaml +│ └── docker-compose.yml +├── imgs/ +├── explanations.md +└── report.md +``` + +--- + +## 4. Реализация + +### 4.1 Симулятор датчиков (`linux_a/simulator/sensor.py`) + +Базовый абстрактный класс `Sensor` и четыре конкретных подкласса: + +```python +from abc import ABC, abstractmethod + +class Sensor(ABC): + unit: str = "" + + def __init__(self, name: str): + self.name = name + self.type = self.__class__.__name__.replace("Sensor", "").lower() + + @abstractmethod + def generate(self) -> float: + pass + + def read(self) -> dict: + return { + "sensor_id": self.name, + "type": self.type, + "value": round(self.generate(), 2), + "unit": self.unit, + "timestamp": time.strftime("%Y-%m-%dT%H:%M:%S"), + } + +class TemperatureSensor(Sensor): + unit = "C" # диапазон: -10 … 50°C + +class PressureSensor(Sensor): + unit = "hPa" # диапазон: 950 … 1080 hPa + +class CurrentSensor(Sensor): + unit = "A" # диапазон: 0 … 20 A + +class HumiditySensor(Sensor): + unit = "%" # диапазон: 0 … 100% +``` + +### 4.2 MQTT-клиент (`linux_a/simulator/main.py`) + +Настраивается через переменные окружения: + +| Переменная | Описание | По умолчанию | +|-----------|---------|-------------| +| `SIM_TYPE` | Тип датчика | `temperature` | +| `SIM_NAME` | Имя датчика (попадает в топик) | `sensor_1` | +| `SIM_HOST` | Адрес брокера | `mosquitto` | +| `SIM_PORT` | Порт брокера | `1883` | +| `SIM_PERIOD` | Период публикации (секунды) | `2.0` | + +Топик публикации: `sensors/{type}/{name}`, например `sensors/temperature/temp_1`. + +Реализован механизм повторных попыток подключения (15 попыток с интервалом 3 секунды) на случай если брокер ещё не поднялся. + +### 4.3 Dockerfile (`linux_a/simulator/Dockerfile`) + +```dockerfile +FROM python:3.12-alpine3.19 +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +COPY . . +CMD ["python", "main.py"] +``` + +Образ опубликован на Docker Hub: `rodjonn/kulakov-sensor-simulator:latest` + +### 4.4 Docker Compose — Linux A (`linux_a/docker-compose.yml`) + +Шесть контейнеров из одного образа с разными переменными окружения: + +```yaml +services: + temp_1: + image: rodjonn/kulakov-sensor-simulator:latest + container_name: sensor_temp_1 + environment: + - SIM_TYPE=temperature + - SIM_NAME=temp_1 + - SIM_PERIOD=2 + networks: + - iot_network + + pressure_1: + image: rodjonn/kulakov-sensor-simulator:latest + container_name: sensor_pressure_1 + environment: + - SIM_TYPE=pressure + - SIM_NAME=pressure_1 + - SIM_PERIOD=5 + networks: + - iot_network + # ... и ещё 4 контейнера +``` + +### 4.5 Mosquitto (`linux_b/mosquitto/mosquitto.conf`) + +``` +listener 1883 +allow_anonymous true +``` + +### 4.6 Docker Compose — Linux B (`linux_b/docker-compose.yml`) + +```yaml +services: + mosquitto: + image: eclipse-mosquitto:2.0 + container_name: mosquitto + ports: + - "1883:1883" + volumes: + - ./mosquitto/mosquitto.conf:/mosquitto/config/mosquitto.conf + networks: + - iot_network +``` + +### 4.7 Telegraf (`linux_c/telegraf/telegraf.conf`) + +```toml +[[inputs.mqtt_consumer]] + servers = ["tcp://mosquitto:1883"] + topics = ["sensors/#"] + data_format = "json" + json_name_key = "type" + tag_keys = ["sensor_id", "type", "unit"] + json_time_key = "timestamp" + json_time_format = "2006-01-02T15:04:05" + +[[outputs.influxdb]] + urls = ["http://influxdb:8086"] + database = "sensors" +``` + +### 4.8 Docker Compose — Linux C (`linux_c/docker-compose.yml`) + +```yaml +services: + influxdb: + image: influxdb:1.8 + environment: + - INFLUXDB_DB=sensors + volumes: + - influxdb_data:/var/lib/influxdb + + telegraf: + image: telegraf:1.28 + volumes: + - ./telegraf/telegraf.conf:/etc/telegraf/telegraf.conf:ro + + grafana: + image: grafana/grafana:10.2.0 + ports: + - "3000:3000" + environment: + - GF_SECURITY_ADMIN_PASSWORD=admin + volumes: + - grafana_data:/var/lib/grafana + - ./grafana/provisioning:/etc/grafana/provisioning +``` + +--- + +## 5. Результаты + +### 5.1 Запущенные контейнеры (`docker ps`) + +Все 6 контейнеров датчиков запущены из образа `rodjonn/kulakov-sensor-simulator:latest`. + +![docker ps — список контейнеров](imgs/screen1.png) + +### 5.2 Образ на Docker Hub + +Образ `rodjonn/kulakov-sensor-simulator:latest` успешно опубликован. Размер: 23.37 MB. + +![Docker Hub — образ](imgs/screen2.png) + +### 5.3 Логи датчика температуры + +Контейнер `sensor_temp_1` подключился к брокеру и публикует JSON-данные в топик `sensors/temperature/temp_1` каждые 2 секунды. + +![Логи sensor_temp_1](imgs/screen3.png) + +### 5.4 График данных в Grafana + +Grafana отображает данные температуры из InfluxDB в реальном времени. Значения находятся в диапазоне 12–26°C, что соответствует работе симулятора `TemperatureSensor`. + +![Grafana — график температуры](imgs/screen4.png) + +### 5.5 Логи брокера Mosquitto + +Mosquitto принял подключения от всех 6 датчиков и от Telegraf-Consumer. Все клиенты успешно зарегистрированы на порту 1883. + +![Логи Mosquitto](imgs/screen5.png) + +--- + +## 6. Порядок воспроизведения + +```bash +# 1. Создать сеть +docker network create iot_network + +# 2. Запустить брокер (Linux B) +cd linux_b && docker compose up -d + +# 3. Запустить базу данных и визуализацию (Linux C) +cd linux_c && docker compose up -d + +# 4. Загрузить образ с Docker Hub и запустить датчики (Linux A) +cd linux_a && docker compose up -d + +# 5. Открыть Grafana +# http://localhost:3000 (логин: admin, пароль: admin) +``` + +--- + +## 7. Выводы + +В ходе практической работы была развёрнута система сбора и визуализации IoT-данных на базе Docker: + +- Написан симулятор датчиков на Python с базовым классом `Sensor` и четырьмя подклассами (температура, давление, ток, влажность) +- Образ опубликован на Docker Hub (`rodjonn/kulakov-sensor-simulator:latest`) и используется для запуска 6 контейнеров с разными конфигурациями через переменные окружения +- MQTT-брокер Mosquitto принимает сообщения от всех датчиков на порту 1883 +- Telegraf подписывается на все топики (`sensors/#`), парсит JSON и записывает данные в InfluxDB +- Grafana строит графики в реальном времени по данным из InfluxDB