Add initial testRail bot implementation

Related-PROD: PRODX-5842
Change-Id: Id2ac9b2275ced80a95019d30ae9e0f7a967f07ec
diff --git a/testrail_bot/.env b/testrail_bot/.env
new file mode 100644
index 0000000..2f9dcb1
--- /dev/null
+++ b/testrail_bot/.env
@@ -0,0 +1,12 @@
+DEBUG=0
+SECRET_KEY=y3y+y(^(m*4=$g&a4mu^0u#2zcs4(fekxbwa(&3w@n_zqgmuiz
+DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1]
+SQL_ENGINE=django.db.backends.postgresql
+SQL_DATABASE=dev
+SQL_USER=dev
+SQL_PASSWORD=dev
+SQL_HOST=db
+SQL_PORT=5432
+DATABASE=postgres
+TESTRAIL_EMAIL=mosqa-eng@mirantis.com
+TESTRAIL_PASSWORD=mosqa-eng496F
diff --git a/testrail_bot/Dockerfile b/testrail_bot/Dockerfile
new file mode 100644
index 0000000..508de46
--- /dev/null
+++ b/testrail_bot/Dockerfile
@@ -0,0 +1,9 @@
+FROM python:3.8.3-alpine
+ENV PYTHONUNBUFFERED 1
+COPY . /testrail_bot/
+WORKDIR /testrail_bot
+
+RUN apk update
+RUN apk add postgresql-dev gcc python3-dev musl-dev linux-headers
+
+RUN pip install -r requirements.txt
diff --git a/testrail_bot/README.md b/testrail_bot/README.md
new file mode 100644
index 0000000..ded468d
--- /dev/null
+++ b/testrail_bot/README.md
@@ -0,0 +1,8 @@
+# TestRail bot
+
+## Download repository
+`git clone "https://gerrit.mcp.mirantis.com/mcp/osccore-qa-testing-tools"`
+
+## Start docker-compose
+`cd osccore-qa-testing-tools/testrail_bot`
+`docker-compose up`
diff --git a/testrail_bot/__init__.py b/testrail_bot/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/testrail_bot/__init__.py
diff --git a/testrail_bot/control/__init__.py b/testrail_bot/control/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/testrail_bot/control/__init__.py
diff --git a/testrail_bot/control/admin.py b/testrail_bot/control/admin.py
new file mode 100644
index 0000000..df2bfb3
--- /dev/null
+++ b/testrail_bot/control/admin.py
@@ -0,0 +1,6 @@
+from django.contrib import admin
+
+from .models import TestRun, Report
+
+admin.site.register(TestRun)
+admin.site.register(Report)
diff --git a/testrail_bot/control/apps.py b/testrail_bot/control/apps.py
new file mode 100644
index 0000000..a5f7085
--- /dev/null
+++ b/testrail_bot/control/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class ControlConfig(AppConfig):
+    name = 'control'
diff --git a/testrail_bot/control/celery_tasks/__init__.py b/testrail_bot/control/celery_tasks/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/testrail_bot/control/celery_tasks/__init__.py
diff --git a/testrail_bot/control/celery_tasks/api.py b/testrail_bot/control/celery_tasks/api.py
new file mode 100644
index 0000000..2954903
--- /dev/null
+++ b/testrail_bot/control/celery_tasks/api.py
@@ -0,0 +1,64 @@
+from testrail_api import TestRailAPI, StatusCodeError
+
+from django.conf import settings
+
+from .enums import StatusEnum
+
+api = TestRailAPI(
+    "https://mirantis.testrail.com/",
+    settings.TESTRAIL_EMAIL,
+    settings.TESTRAIL_PASSWORD)
+
+
+def get_project_id(project_name):
+    project = list(filter(
+        lambda x: x["name"] == project_name,
+        api.projects.get_projects()))
+    if project:
+        return project[0]["id"]
+    else:
+        return None
+
+
+def get_plans(project_id, plan_name, **kwargs):
+    plans = api.plans.get_plans(project_id, **kwargs)
+    return [x["id"] for x in filter(
+        lambda x: plan_name in x["name"], plans)]
+
+
+def get_entries(plan_id):
+    return api.plans.get_plan(plan_id)["entries"]
+
+
+def get_run_id(entries, run_name):
+    entries = list(filter(
+        lambda x: x["name"] == run_name,
+        entries))
+    if not entries:
+        return None
+    return entries[0]["runs"][0]["id"]
+
+
+def get_result_for_case(run_id, case_id):
+    try:
+        results = api.results.get_results_for_case(run_id, case_id)
+    except StatusCodeError:
+        return None
+    return results
+
+
+def get_failed_tests(last_run_id):
+    return api.tests.get_tests(
+        last_run_id, status_id=StatusEnum.failed)
+
+
+def add_result(test_id, update_dict):
+    api.results.add_result(test_id, **update_dict)
+
+
+def get_user_id(user_name):
+    users = api.users.get_users()
+    users = list(filter(lambda x: x["name"] == user_name, users))
+    if not users:
+        return None
+    return users[0]["id"]
diff --git a/testrail_bot/control/celery_tasks/enums.py b/testrail_bot/control/celery_tasks/enums.py
new file mode 100644
index 0000000..f7b7251
--- /dev/null
+++ b/testrail_bot/control/celery_tasks/enums.py
@@ -0,0 +1,13 @@
+class StatusEnum:
+    passed = "1"
+    blocked = "2"
+    untested = "3"
+    retest = "4"
+    failed = "5"
+    skipped = "6"
+    in_progress = "7"
+    product_failed = "8"
+    test_failed = "9"
+    wont_fix = "10"
+    mixes_success = "11"
+    wont_test = "12"
diff --git a/testrail_bot/control/celery_tasks/filters.py b/testrail_bot/control/celery_tasks/filters.py
new file mode 100644
index 0000000..f552dfc
--- /dev/null
+++ b/testrail_bot/control/celery_tasks/filters.py
@@ -0,0 +1,16 @@
+import re
+
+
+def filter_ip(data):
+    ip_addr_regex = re.compile(r'\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b')
+    return re.sub(ip_addr_regex, "x.x.x.x", data)
+
+
+def filter_uuid(data):
+    uuid4hex = re.compile(
+        r'[0-9a-f]{12}4[0-9a-f]{3}[89ab][0-9a-f]{15}\Z', re.I)
+    return re.sub(uuid4hex, "xxxx", data)
+
+
+def last_traceback_filter(data):
+    return data[data.rfind("Traceback"):]
diff --git a/testrail_bot/control/celery_tasks/tasks.py b/testrail_bot/control/celery_tasks/tasks.py
new file mode 100644
index 0000000..8566653
--- /dev/null
+++ b/testrail_bot/control/celery_tasks/tasks.py
@@ -0,0 +1,126 @@
+from __future__ import absolute_import, unicode_literals
+
+import difflib
+
+from celery import shared_task
+
+from .. import models
+
+from . import filters
+from . import api
+from .enums import StatusEnum
+
+
+def finish_report(report):
+    report.finished = True
+    report.save()
+
+
+def apply_filters(data, test_run):
+    if test_run.filter_last_traceback:
+        data = filters.last_traceback_filter(data)
+
+    if test_run.ip_filter:
+        data = filters.filter_ip(data)
+
+    if test_run.uuid_filter:
+        data = filters.filter_uuid(data)
+
+    if test_run.filter_func:
+        import rpdb;rpdb.set_trace()
+        exec(test_run.filter_func)
+        data = locals()["custom_filter"](data)
+    return data
+
+
+def process_plan(plan_id, case_id, last_comment, run):
+    run_id = api.get_run_id(api.get_entries(plan_id), run.run_name)
+    if not run_id:
+        return None, -1.0
+
+    results = api.get_result_for_case(run_id, case_id)
+
+    if not results:
+        return None, -2.0
+
+    status_code = str(results[0]["status_id"])
+    if status_code not in [StatusEnum.test_failed, StatusEnum.product_failed]:
+        return None, -3.0
+
+    comment = apply_filters(results[-1]["comment"], run)
+
+    ratio = difflib.SequenceMatcher(
+        lambda symbol: symbol in [" ", ",", "\n"],
+        last_comment, comment, autojunk=False).ratio()
+    if ratio > 0.9:
+        return results[0], ratio
+    return None, ratio
+
+
+@shared_task
+def process_run(run_id, report_id, path):
+    report = models.Report.objects.get(pk=report_id)
+    with open(path, "w") as f:
+        test_run = models.TestRun.objects.get(pk=run_id)
+        f.write("Start processing {}\n".format(test_run.run_name))
+        f.flush()
+
+        project_id = api.get_project_id(test_run.project_name)
+        if not project_id:
+            f.write("Incorrect Project {}. Stopping processing\n".format(
+                test_run.project_name))
+            f.flush()
+            finish_report(report)
+            return
+
+        created_by_id = api.get_user_id(test_run.created_by)
+        kw = {"limit": 100}
+        if created_by_id:
+            kw["created_by"] = created_by_id
+        plans = api.get_plans(project_id, test_run.plan_name, **kw)
+
+        last_plan = plans[0]
+        last_run_id = api.get_run_id(
+            api.get_entries(last_plan), test_run.run_name)
+        if not last_run_id:
+            f.write("No {} in {} plan\n".format(
+                test_run.run_name, last_plan))
+            f.flush()
+            finish_report(report)
+            return
+
+        failed_tests = api.get_failed_tests(last_run_id)
+        for test in failed_tests:
+            case_id = test["case_id"]
+
+            f.write("Processing test with id {}\n".format(test["id"]))
+            f.flush()
+
+            last_result = api.get_result_for_case(last_run_id, case_id)[0]
+
+            last_comment = apply_filters(
+                last_result["comment"], test_run)
+
+            for plan_id in plans[1:]:
+                sim_result, ratio = process_plan(
+                    plan_id, case_id, last_comment, test_run)
+                if sim_result:
+                    update_dict = {
+                        "status_id": sim_result["status_id"],
+                        "comment": "Marked by TestRailBot because "
+                                   "of similarity with test {} {}%".format(
+                            sim_result["test_id"], round(100.0 * ratio, 2)),
+                        "defects": sim_result["defects"]
+                    }
+                    f.write("Found similarity: {}\n".format(update_dict))
+                    f.flush()
+                    api.add_result(test["id"], update_dict)
+                    break
+                else:
+                    f.write(
+                        "Similarity not found due to similarity:{}%\n".format(
+                            round(100.0 * ratio, 2)))
+                    f.flush()
+        f.write("Test processing finished")
+        f.flush()
+        finish_report(report)
diff --git a/testrail_bot/control/forms.py b/testrail_bot/control/forms.py
new file mode 100644
index 0000000..9375f42
--- /dev/null
+++ b/testrail_bot/control/forms.py
@@ -0,0 +1,22 @@
+from django.forms import ModelForm
+from .models import TestRun
+
+
+class TestRunForm(ModelForm):
+    class Meta:
+        model = TestRun
+        fields = "__all__"
+        labels = {
+            "project_name":  "Name of the project",
+            "plan_name": "Name of the Test Plan without date",
+            "run_name": "Name of the run",
+            "created_by": "Test Run created by",
+            "filter_func": "Custom filter function",
+            "ip_filter": "Change ip to x.x.x.x",
+            "uuid_filter": "Change uuid to xxxx",
+            "filter_last_traceback": "Use only last traceback to "
+                                     "compare comments"
+        }
+        help_texts = {
+            "filter_func": "Leave blank if not used",
+        }
diff --git a/testrail_bot/control/migrations/0001_initial.py b/testrail_bot/control/migrations/0001_initial.py
new file mode 100644
index 0000000..774da4a
--- /dev/null
+++ b/testrail_bot/control/migrations/0001_initial.py
@@ -0,0 +1,24 @@
+# Generated by Django 3.0.7 on 2020-06-30 13:22
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    initial = True
+
+    dependencies = [
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='TestRun',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('project_name', models.CharField(max_length=300)),
+                ('plan_name', models.CharField(max_length=300)),
+                ('run_name', models.CharField(max_length=300)),
+                ('filter_func', models.TextField()),
+            ],
+        ),
+    ]
diff --git a/testrail_bot/control/migrations/0002_auto_20200701_0735.py b/testrail_bot/control/migrations/0002_auto_20200701_0735.py
new file mode 100644
index 0000000..b1070b9
--- /dev/null
+++ b/testrail_bot/control/migrations/0002_auto_20200701_0735.py
@@ -0,0 +1,23 @@
+# Generated by Django 3.0.7 on 2020-07-01 07:35
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('control', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='testrun',
+            name='ip_filter',
+            field=models.BooleanField(default=False),
+        ),
+        migrations.AddField(
+            model_name='testrun',
+            name='uuid_filter',
+            field=models.BooleanField(default=False),
+        ),
+    ]
diff --git a/testrail_bot/control/migrations/0003_report.py b/testrail_bot/control/migrations/0003_report.py
new file mode 100644
index 0000000..55b5d7b
--- /dev/null
+++ b/testrail_bot/control/migrations/0003_report.py
@@ -0,0 +1,23 @@
+# Generated by Django 3.0.7 on 2020-07-01 08:09
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('control', '0002_auto_20200701_0735'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='Report',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('path', models.FileField(upload_to='')),
+                ('report_name', models.CharField(max_length=300)),
+                ('test_run', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='control.TestRun')),
+            ],
+        ),
+    ]
diff --git a/testrail_bot/control/migrations/0004_auto_20200701_1342.py b/testrail_bot/control/migrations/0004_auto_20200701_1342.py
new file mode 100644
index 0000000..5928c8e
--- /dev/null
+++ b/testrail_bot/control/migrations/0004_auto_20200701_1342.py
@@ -0,0 +1,24 @@
+# Generated by Django 3.0.7 on 2020-07-01 13:42
+
+import django.core.files.storage
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('control', '0003_report'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='report',
+            name='path',
+            field=models.FileField(storage=django.core.files.storage.FileSystemStorage(location='media/reports'), upload_to=''),
+        ),
+        migrations.AlterField(
+            model_name='testrun',
+            name='filter_func',
+            field=models.TextField(null=True),
+        ),
+    ]
diff --git a/testrail_bot/control/migrations/0005_auto_20200701_1344.py b/testrail_bot/control/migrations/0005_auto_20200701_1344.py
new file mode 100644
index 0000000..46b38e8
--- /dev/null
+++ b/testrail_bot/control/migrations/0005_auto_20200701_1344.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.0.7 on 2020-07-01 13:44
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('control', '0004_auto_20200701_1342'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='testrun',
+            name='filter_func',
+            field=models.TextField(default='', null=True),
+        ),
+    ]
diff --git a/testrail_bot/control/migrations/0006_auto_20200701_1347.py b/testrail_bot/control/migrations/0006_auto_20200701_1347.py
new file mode 100644
index 0000000..9e40f0e
--- /dev/null
+++ b/testrail_bot/control/migrations/0006_auto_20200701_1347.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.0.7 on 2020-07-01 13:47
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('control', '0005_auto_20200701_1344'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='testrun',
+            name='filter_func',
+            field=models.TextField(blank=True, null=True),
+        ),
+    ]
diff --git a/testrail_bot/control/migrations/0007_auto_20200702_1416.py b/testrail_bot/control/migrations/0007_auto_20200702_1416.py
new file mode 100644
index 0000000..7940406
--- /dev/null
+++ b/testrail_bot/control/migrations/0007_auto_20200702_1416.py
@@ -0,0 +1,24 @@
+# Generated by Django 3.0.7 on 2020-07-02 14:16
+
+import django.core.files.storage
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('control', '0006_auto_20200701_1347'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='report',
+            name='finished',
+            field=models.BooleanField(default=False),
+        ),
+        migrations.AlterField(
+            model_name='report',
+            name='path',
+            field=models.FileField(blank=True, null=True, storage=django.core.files.storage.FileSystemStorage(location='media/reports'), upload_to=''),
+        ),
+    ]
diff --git a/testrail_bot/control/migrations/0008_testrun_created_by.py b/testrail_bot/control/migrations/0008_testrun_created_by.py
new file mode 100644
index 0000000..eb0ec87
--- /dev/null
+++ b/testrail_bot/control/migrations/0008_testrun_created_by.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.0.7 on 2020-07-03 08:42
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('control', '0007_auto_20200702_1416'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='testrun',
+            name='created_by',
+            field=models.CharField(default='', max_length=300),
+        ),
+    ]
diff --git a/testrail_bot/control/migrations/0009_testrun_filter_last_traceback.py b/testrail_bot/control/migrations/0009_testrun_filter_last_traceback.py
new file mode 100644
index 0000000..49aa60d
--- /dev/null
+++ b/testrail_bot/control/migrations/0009_testrun_filter_last_traceback.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.0.7 on 2020-07-03 11:48
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('control', '0008_testrun_created_by'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='testrun',
+            name='filter_last_traceback',
+            field=models.BooleanField(default=False),
+        ),
+    ]
diff --git a/testrail_bot/control/migrations/0010_auto_20200709_0831.py b/testrail_bot/control/migrations/0010_auto_20200709_0831.py
new file mode 100644
index 0000000..826aac8
--- /dev/null
+++ b/testrail_bot/control/migrations/0010_auto_20200709_0831.py
@@ -0,0 +1,19 @@
+# Generated by Django 3.0.7 on 2020-07-09 08:31
+
+import django.core.files.storage
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('control', '0009_testrun_filter_last_traceback'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='report',
+            name='path',
+            field=models.FileField(blank=True, null=True, storage=django.core.files.storage.FileSystemStorage(), upload_to=''),
+        ),
+    ]
diff --git a/testrail_bot/control/migrations/__init__.py b/testrail_bot/control/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/testrail_bot/control/migrations/__init__.py
diff --git a/testrail_bot/control/models.py b/testrail_bot/control/models.py
new file mode 100644
index 0000000..6a13c8e
--- /dev/null
+++ b/testrail_bot/control/models.py
@@ -0,0 +1,23 @@
+from django.core.files.storage import FileSystemStorage
+from django.db import models
+
+
+class TestRun(models.Model):
+    project_name = models.CharField(max_length=300)
+    plan_name = models.CharField(max_length=300)
+    run_name = models.CharField(max_length=300)
+    created_by = models.CharField(max_length=300, default="")
+    filter_func = models.TextField(null=True, blank=True)
+    ip_filter = models.BooleanField(default=False)
+    uuid_filter = models.BooleanField(default=False)
+    filter_last_traceback = models.BooleanField(default=False)
+
+
+fs = FileSystemStorage()
+
+
+class Report(models.Model):
+    path = models.FileField(storage=fs, null=True, blank=True)
+    test_run = models.ForeignKey(TestRun, on_delete=models.CASCADE)
+    report_name = models.CharField(max_length=300)
+    finished = models.BooleanField(default=False)
diff --git a/testrail_bot/control/templates/base.html b/testrail_bot/control/templates/base.html
new file mode 100644
index 0000000..e21330b
--- /dev/null
+++ b/testrail_bot/control/templates/base.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+{% load bootstrap3 %}
+{% bootstrap_css %}
+{% bootstrap_javascript %}
+
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <title>TestRail bot configuration</title>
+    <style>
+        body { padding-top: 70px; }
+    </style>
+    {% block head %}
+    {% endblock %}
+</head>
+<body>
+<nav class="navbar navbar-inverse navbar-fixed-top">
+    <div class="container-fluid">
+    <div class="navbar-header">
+      <a class="navbar-brand" href="{% url 'index' %}">TestRail bot</a>
+    </div>
+    <ul class="nav navbar-nav">
+        <li><a href="{% url 'index' %}">Test Runs</a></li>
+        <li><a href="{% url 'create_run' %}">Create New Test Run</a></li>
+        <li><a href="{% url 'list_reports' %}">Reports</a></li>
+        <li><a href="{% url 'help' %}">Help</a></li>
+
+    </ul>
+  </div>
+</nav>
+{% block section %}{% endblock %}
+</body>
+</html>
\ No newline at end of file
diff --git a/testrail_bot/control/templates/control/create_run.html b/testrail_bot/control/templates/control/create_run.html
new file mode 100644
index 0000000..9d6ddb1
--- /dev/null
+++ b/testrail_bot/control/templates/control/create_run.html
@@ -0,0 +1,31 @@
+{% extends "base.html" %}
+{% load bootstrap3 %}
+{% block section %}
+<div>
+  <form action="{% url 'create_run' %}" method="post" class="form">
+    {% csrf_token %}
+      {% bootstrap_form_errors form type='non_fields' %}
+      <div class="col-xs-8">{% bootstrap_field form.project_name  size='sm' %}</div>
+      <div class="col-xs-8">{% bootstrap_field form.plan_name size='sm' %}</div>
+      <div class="col-xs-8">{% bootstrap_field form.run_name  size='sm' %}</div>
+      <div class="col-xs-8">{% bootstrap_field form.created_by  size='sm' %}</div>
+      <div class="col-xs-8">{% bootstrap_field form.filter_func  size='sm' %}</div>
+      <div class="col-md-5">
+        {% bootstrap_field form.ip_filter  size='sm'%}
+      </div>
+      <div class="col-md-5">
+        {% bootstrap_field form.uuid_filter size='sm'%}
+      </div>
+      <div class="col-md-5">
+        {% bootstrap_field form.filter_last_traceback size='sm'%}
+      </div>
+    {% buttons %}
+    <div class="btn-toolbar col-xs-7">
+      <button type="submit" class="btn btn-primary">
+        {% bootstrap_icon "star" %} Save
+      </button>
+    </div>
+    {% endbuttons %}
+  </form>
+</div>
+{% endblock %}
\ No newline at end of file
diff --git a/testrail_bot/control/templates/control/help.html b/testrail_bot/control/templates/control/help.html
new file mode 100644
index 0000000..d4dbf69
--- /dev/null
+++ b/testrail_bot/control/templates/control/help.html
@@ -0,0 +1,71 @@
+{% extends "base.html" %}
+{% load static %}
+{% block section %}
+<h1 class="display-1 text-center">How to use</h1>
+<div class="col-xs-8">
+    <h3 class="display-3 text-center">Create Run</h3>
+    <ul>
+        <li>
+            Open <a href="{% url 'create_run' %}"><code>Create New Test Run</code></a> page.
+            <img style="width:100%" src="{% static 'control/help_1.png' %}"/>
+        </li>
+        <li>
+            Fill input forms:
+            <ol>
+                <li>Enter Project name i.e. <code>Mirantis Cloud Platform</code></li>
+                <li>Enter Plan name without date i.e. <code>[MCP2.0]OSCORE</code></li>
+                <li>Enter full name of the run i.e. <code>ussuri-core-ceph-ceph-dvr <[MCP2.0_USSURI]Tempest></code></li>
+                <li>Fill created by column i.e. <code>os-qa-bot</code></li>
+                <li>(Optional) Enter custom filtration function.
+                    If not used it should be empty. If used, the function definition should be like:
+                    <pre>
+def custom_filter(data):
+    ...
+    return modified_data</pre>
+                </li>
+                <li>Select <code>change ip to x.x.x.x</code> to replace all ip addresses to x.x.x.x</li>
+                <li>Select <code>change uuid to xxxx</code> to change all uuids to xxxx</li>
+                <li>Select <code>Use only last traceback to compare comments</code> to use data located after last <code>Traceback:</code></li>
+                <li>Click save to save record and run it later</li>
+            </ol>
+        </li>
+
+    </ul>
+</div>
+<div class="col-xs-8">
+    <h3 class="display-3 text-center">Change Run</h3>
+    <ul>
+        <li>Open <a href="{% url 'index' %}"><code>Test Runs</code></a> page</li>
+        <li>
+            Select run that you want to change from list of available runs.
+            <img style="width:100%" src="{% static 'control/help_2.png' %}"/>
+        </li>
+        <li>
+            Change fields like it's described in Create Run How To.
+        </li>
+        <li>Click <code>Save</code></li>
+    </ul>
+</div>
+<div class="col-xs-8">
+    <h3 class="display-3 text-center">Submit Run</h3>
+    <ul>
+        <li>Open <a href="{% url 'index' %}"><code>Test Runs</code></a> page</li>
+        <li>
+            Select run that you want to change from list of available runs.
+            <img style="width:100%" src="{% static 'control/help_2.png' %}"/>
+        </li>
+        <li>Click <code>Submit</code></li>
+        <li>You will be redirected to report for this run, where you can view how test run is being processed.</li>
+    </ul>
+</div>
+<div class="col-xs-8">
+    <h3 class="display-3 text-center">Generate Report</h3>
+    <ul>
+        <li>Open <a href="{% url 'list_reports' %}"><code>Reports</code></a> page</li>
+        <li>
+            Select report that you want to view.
+            <img style="width:100%" src="{% static 'control/help_3.png' %}"/>
+        </li>
+    </ul>
+</div>
+{% endblock %}
\ No newline at end of file
diff --git a/testrail_bot/control/templates/control/index.html b/testrail_bot/control/templates/control/index.html
new file mode 100644
index 0000000..4722575
--- /dev/null
+++ b/testrail_bot/control/templates/control/index.html
@@ -0,0 +1,11 @@
+{% extends "base.html" %}
+{% block section %}
+<p>Test Runs:</p>
+<div class="list-group">
+    {% for run in runs %}
+        <a href="{% url 'single_run' run.id  %}" class="list-group-item list-group-item-success">
+            Run {{run.run_name}}
+        </a>
+    {% endfor %}
+</div>
+{% endblock %}
\ No newline at end of file
diff --git a/testrail_bot/control/templates/control/report.html b/testrail_bot/control/templates/control/report.html
new file mode 100644
index 0000000..2adc34b
--- /dev/null
+++ b/testrail_bot/control/templates/control/report.html
@@ -0,0 +1,44 @@
+{% extends "base.html" %}
+{% block section %}
+{% load bootstrap3 %}
+{% bootstrap_javascript jquery=True %}
+<script>
+function getCookie(name) {
+    let cookieValue = null;
+    if (document.cookie && document.cookie !== '') {
+        const cookies = document.cookie.split(';');
+        for (let i = 0; i < cookies.length; i++) {
+            const cookie = cookies[i].trim();
+            // Does this cookie string begin with the name we want?
+            if (cookie.substring(0, name.length + 1) === (name + '=')) {
+                cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
+                break;
+            }
+        }
+    }
+    return cookieValue;
+};
+const csrftoken = getCookie('csrftoken');
+
+function send(){
+    $.ajax({
+        type: "post",
+        url: "{% url 'single_report' report_id %}",
+        dataType: "json",
+        headers: {'X-CSRFToken': csrftoken},
+        success:function(data)
+        {
+            tag = document.getElementById("data");
+            tag.innerHTML = data["data"];
+            //Send another request in 10 seconds.
+            if (data["finished"] != true){
+                setTimeout(function(){
+                    send();
+                }, 3000);
+            }
+        }
+    });
+}
+send();</script>
+<pre id="data">{{report}}</pre>
+{% endblock %}
\ No newline at end of file
diff --git a/testrail_bot/control/templates/control/reports.html b/testrail_bot/control/templates/control/reports.html
new file mode 100644
index 0000000..4f402a6
--- /dev/null
+++ b/testrail_bot/control/templates/control/reports.html
@@ -0,0 +1,11 @@
+{% extends "base.html" %}
+{% block section %}
+<p>Reports:</p>
+<div class="list-group">
+    {% for report in reports %}
+        <a href="{% url 'single_report' report.id  %}" class="list-group-item list-group-item-success">
+            {{report.report_name}}
+        </a>
+    {% endfor %}
+</div>
+{% endblock %}
\ No newline at end of file
diff --git a/testrail_bot/control/templates/control/update_run.html b/testrail_bot/control/templates/control/update_run.html
new file mode 100644
index 0000000..1893ee5
--- /dev/null
+++ b/testrail_bot/control/templates/control/update_run.html
@@ -0,0 +1,38 @@
+{% extends "base.html" %}
+{% load bootstrap3 %}
+{% block section %}
+<div>
+  <form action="{% url 'single_run' run_id %}" method="post" class="form">
+    {% csrf_token %}
+      {% bootstrap_form_errors form type='non_fields' %}
+      <div class="col-xs-8">{% bootstrap_field form.project_name  size='sm' %}</div>
+      <div class="col-xs-8">{% bootstrap_field form.plan_name size='sm' %}</div>
+      <div class="col-xs-8">{% bootstrap_field form.run_name  size='sm' %}</div>
+      <div class="col-xs-8">{% bootstrap_field form.created_by  size='sm' %}</div>
+      <div class="col-xs-8">{% bootstrap_field form.filter_func  size='sm' %}</div>
+      <div class="col-md-5">
+        {% bootstrap_field form.ip_filter  size='sm'%}
+      </div>
+      <div class="col-md-5">
+        {% bootstrap_field form.uuid_filter size='sm'%}
+      </div>
+      <div class="col-md-5">
+        {% bootstrap_field form.filter_last_traceback size='sm'%}
+      </div>
+    {% buttons %}
+    <div class="btn-toolbar col-xs-7">
+      <button type="submit" class="btn btn-primary">
+        {% bootstrap_icon "star" %} Save
+      </button>
+      <button type="submit" formaction="{% url 'submit_run' run_id %}" class="btn btn-primary">
+        <svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-chevron-double-right" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
+          <path fill-rule="evenodd" d="M3.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L9.293 8 3.646 2.354a.5.5 0 0 1 0-.708z"/>
+          <path fill-rule="evenodd" d="M7.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L13.293 8 7.646 2.354a.5.5 0 0 1 0-.708z"/>
+        </svg>
+         Submit
+      </button>
+    </div>
+    {% endbuttons %}
+  </form>
+</div>
+{% endblock %}
\ No newline at end of file
diff --git a/testrail_bot/control/tests.py b/testrail_bot/control/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/testrail_bot/control/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/testrail_bot/control/urls.py b/testrail_bot/control/urls.py
new file mode 100644
index 0000000..d530635
--- /dev/null
+++ b/testrail_bot/control/urls.py
@@ -0,0 +1,14 @@
+from django.urls import path
+
+from . import views
+
+urlpatterns = [
+    path("", views.redirect_to_index, name="redirect"),
+    path("runs/", views.create_run, name="create_run"),
+    path("runs/<int:run_id>/", views.single_run, name="single_run"),
+    path("runs/<int:run_id>/submit/", views.submit_run, name="submit_run"),
+    path("reports/", views.list_reports, name="list_reports"),
+    path("reports/<int:report_id>/", views.single_report, name="single_report"),
+    path('index/', views.index, name='index'),
+    path("help/", views.show_help, name="help"),
+]
diff --git a/testrail_bot/control/views.py b/testrail_bot/control/views.py
new file mode 100644
index 0000000..ee582eb
--- /dev/null
+++ b/testrail_bot/control/views.py
@@ -0,0 +1,83 @@
+from datetime import date
+import json
+import os
+
+from django.shortcuts import render, redirect, HttpResponse
+
+from . import models
+from . import forms
+from .celery_tasks.tasks import process_run
+
+
+def index(request):
+    runs = models.TestRun.objects.all()
+    return render(request, "control/index.html", {"runs": runs})
+
+
+def redirect_to_index(request):
+    return redirect("index")
+
+
+def single_run(request, run_id):
+    run = models.TestRun.objects.get(pk=run_id)
+    if request.method == "POST":
+        form = forms.TestRunForm(request.POST, instance=run)
+        if form.is_valid():
+            form.save()
+            return redirect("single_run", run_id)
+    else:
+        form = forms.TestRunForm(instance=run)
+
+    return render(request, "control/update_run.html",
+                  {"form": form, "run_id": run_id})
+
+
+def create_run(request):
+    if request.method == "POST":
+        form = forms.TestRunForm(request.POST)
+        if form.is_valid():
+            obj = form.save()
+            return redirect("single_run", obj.id)
+    else:
+        form = forms.TestRunForm()
+
+    return render(request, "control/create_run.html", {"form": form})
+
+
+def list_reports(request):
+    reports = models.Report.objects.all()
+    return render(request, "control/reports.html", {"reports": reports})
+
+
+def single_report(request, report_id):
+    report = models.Report.objects.get(pk=report_id)
+    data = report.path.read().decode("utf-8")
+    if request.method == "POST" and request.is_ajax():
+        return HttpResponse(
+            json.dumps({"data": data, "finished": report.finished}),
+            content_type="application/json")
+    return render(request, "control/report.html",
+                  {"report_id": report.id, "report": data})
+
+
+def submit_run(request, run_id):
+    run = models.TestRun.objects.get(pk=run_id)
+    report_name = "{}-{}".format(run.run_name, date.isoformat(date.today()))
+    path = os.path.join(models.fs.location, report_name)
+    with open(path, "w"):
+        pass
+
+    report = models.Report(
+        test_run=run,
+        report_name=report_name,
+        path=path)
+    report.save()
+    process_run.delay(run_id, report.id, path)
+
+    return render(
+        request, "control/report.html",
+        {"report": report.path.read().decode("utf-8"), "report_id": report.id})
+
+
+def show_help(request):
+    return render(request, "control/help.html")
diff --git a/testrail_bot/docker-compose.yml b/testrail_bot/docker-compose.yml
new file mode 100644
index 0000000..88dce9e
--- /dev/null
+++ b/testrail_bot/docker-compose.yml
@@ -0,0 +1,69 @@
+version: '3'
+
+services:
+  redis:
+    image: redis:6.0.5-alpine
+    networks:
+      - tr_bot
+  db:
+    image: postgres:12.0-alpine
+    volumes:
+      - postgres_data:/var/lib/postgresql/data/
+    environment:
+      - POSTGRES_USER=dev
+      - POSTGRES_PASSWORD=dev
+      - POSTGRES_DB=dev
+    networks:
+      - tr_bot
+  worker:
+    build: .
+    command: celery -A testrail_bot worker -l info
+    volumes:
+    - media_volume:/mediafiles
+    networks:
+      - tr_bot
+    depends_on:
+      - redis
+      - db
+    env_file:
+      - .env
+  web:
+    build: .
+    command: ./start_webapp.sh
+    volumes:
+    - .:/testrail_bot
+    - static_volume:/staticfiles
+    - media_volume:/mediafiles
+    networks:
+      - tr_bot
+    env_file:
+      - .env
+    expose:
+      - 8000
+    depends_on:
+      - db
+  nginx:
+    build: ./nginx
+    volumes:
+    - static_volume:/staticfiles
+    - media_volume:/mediafiles
+    ports:
+    - "80:80"
+    depends_on:
+      - web
+    networks:
+      - tr_bot
+
+
+networks:
+  tr_bot:
+    driver: bridge
+    ipam:
+      driver: default
+      config:
+        - subnet: 192.168.201.0/24
+
+volumes:
+  postgres_data:
+  static_volume:
+  media_volume:
\ No newline at end of file
diff --git a/testrail_bot/manage.py b/testrail_bot/manage.py
new file mode 100755
index 0000000..ff64df6
--- /dev/null
+++ b/testrail_bot/manage.py
@@ -0,0 +1,21 @@
+#!/usr/bin/env python
+"""Django's command-line utility for administrative tasks."""
+import os
+import sys
+
+
+def main():
+    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'testrail_bot.settings')
+    try:
+        from django.core.management import execute_from_command_line
+    except ImportError as exc:
+        raise ImportError(
+            "Couldn't import Django. Are you sure it's installed and "
+            "available on your PYTHONPATH environment variable? Did you "
+            "forget to activate a virtual environment?"
+        ) from exc
+    execute_from_command_line(sys.argv)
+
+
+if __name__ == '__main__':
+    main()
diff --git a/testrail_bot/media/images/control/help_1.png b/testrail_bot/media/images/control/help_1.png
new file mode 100644
index 0000000..c6ad4f1
--- /dev/null
+++ b/testrail_bot/media/images/control/help_1.png
Binary files differ
diff --git a/testrail_bot/media/images/control/help_2.png b/testrail_bot/media/images/control/help_2.png
new file mode 100644
index 0000000..c29c3bb
--- /dev/null
+++ b/testrail_bot/media/images/control/help_2.png
Binary files differ
diff --git a/testrail_bot/media/images/control/help_3.png b/testrail_bot/media/images/control/help_3.png
new file mode 100644
index 0000000..1a1124f
--- /dev/null
+++ b/testrail_bot/media/images/control/help_3.png
Binary files differ
diff --git a/testrail_bot/nginx/Dockerfile b/testrail_bot/nginx/Dockerfile
new file mode 100644
index 0000000..8e5916e
--- /dev/null
+++ b/testrail_bot/nginx/Dockerfile
@@ -0,0 +1,4 @@
+FROM nginx:1.19.0-alpine
+
+RUN rm /etc/nginx/conf.d/default.conf
+COPY nginx.conf /etc/nginx/conf.d
\ No newline at end of file
diff --git a/testrail_bot/nginx/nginx.conf b/testrail_bot/nginx/nginx.conf
new file mode 100644
index 0000000..5201782
--- /dev/null
+++ b/testrail_bot/nginx/nginx.conf
@@ -0,0 +1,21 @@
+upstream tr_bor {
+    server web:8000;
+
+}
+
+server {
+
+    listen 80;
+
+    location / {
+        proxy_pass http://web:8000;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        proxy_set_header Host $host;
+        proxy_redirect off;
+    }
+
+    location /staticfiles/ {
+        alias /staticfiles/;
+    }
+
+}
\ No newline at end of file
diff --git a/testrail_bot/requirements.txt b/testrail_bot/requirements.txt
new file mode 100644
index 0000000..eebc681
--- /dev/null
+++ b/testrail_bot/requirements.txt
@@ -0,0 +1,7 @@
+celery==4.4.6
+Django==3.0.7
+django-bootstrap3==14.0.0
+psycopg2-binary==2.8.5
+redis==3.5.3
+testrail-api==1.7.0
+uWSGI==2.0.19.1
diff --git a/testrail_bot/start_webapp.sh b/testrail_bot/start_webapp.sh
new file mode 100755
index 0000000..81fbd83
--- /dev/null
+++ b/testrail_bot/start_webapp.sh
@@ -0,0 +1,16 @@
+#!/bin/sh
+
+if [ "$DATABASE" = "postgres" ]
+then
+    echo "Waiting for postgres..."
+
+    while ! nc -z $SQL_HOST $SQL_PORT; do
+      sleep 0.1
+    done
+
+    echo "PostgreSQL started"
+fi
+
+python manage.py migrate --noinput
+python manage.py collectstatic --noinput
+uwsgi --http :8000 --module testrail_bot.wsgi
diff --git a/testrail_bot/testrail_bot/__init__.py b/testrail_bot/testrail_bot/__init__.py
new file mode 100644
index 0000000..070e835
--- /dev/null
+++ b/testrail_bot/testrail_bot/__init__.py
@@ -0,0 +1,7 @@
+from __future__ import absolute_import, unicode_literals
+
+# This will make sure the app is always imported when
+# Django starts so that shared_task will use this app.
+from .celery import app as celery_app
+
+__all__ = ('celery_app',)
diff --git a/testrail_bot/testrail_bot/asgi.py b/testrail_bot/testrail_bot/asgi.py
new file mode 100644
index 0000000..be76c4c
--- /dev/null
+++ b/testrail_bot/testrail_bot/asgi.py
@@ -0,0 +1,16 @@
+"""
+ASGI config for testrail_bot project.
+
+It exposes the ASGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/
+"""
+
+import os
+
+from django.core.asgi import get_asgi_application
+
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'testrail_bot.settings')
+
+application = get_asgi_application()
diff --git a/testrail_bot/testrail_bot/celery.py b/testrail_bot/testrail_bot/celery.py
new file mode 100644
index 0000000..9e4cd7b
--- /dev/null
+++ b/testrail_bot/testrail_bot/celery.py
@@ -0,0 +1,18 @@
+from __future__ import absolute_import, unicode_literals
+
+import os
+
+from celery import Celery
+
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'testrail_bot.settings')
+
+app = Celery('testrail_bot')
+
+app.config_from_object('django.conf:settings', namespace='CELERY')
+
+app.autodiscover_tasks()
+
+
+@app.task(bind=True)
+def debug_task(self):
+    print('Request: {0!r}'.format(self.request))
diff --git a/testrail_bot/testrail_bot/settings.py b/testrail_bot/testrail_bot/settings.py
new file mode 100644
index 0000000..b5b964a
--- /dev/null
+++ b/testrail_bot/testrail_bot/settings.py
@@ -0,0 +1,146 @@
+"""
+Django settings for testrail_bot project.
+
+Generated by 'django-admin startproject' using Django 3.0.7.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/3.0/topics/settings/
+
+For the full list of settings and their values, see
+https://docs.djangoproject.com/en/3.0/ref/settings/
+"""
+
+import os
+
+# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
+BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+
+
+# Quick-start development settings - unsuitable for production
+# See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/
+
+# SECURITY WARNING: keep the secret key used in production secret!
+SECRET_KEY = os.environ.get("SECRET_KEY", default="123")
+
+# SECURITY WARNING: don't run with debug turned on in production!
+DEBUG = int(os.environ.get("DEBUG", default=0))
+
+ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS", default="*").split(" ")
+
+
+# Application definition
+
+INSTALLED_APPS = [
+    'control.apps.ControlConfig',
+    'bootstrap3',
+    'django.contrib.admin',
+    'django.contrib.auth',
+    'django.contrib.contenttypes',
+    'django.contrib.sessions',
+    'django.contrib.messages',
+    'django.contrib.staticfiles',
+]
+
+MIDDLEWARE = [
+    'django.middleware.security.SecurityMiddleware',
+    'django.contrib.sessions.middleware.SessionMiddleware',
+    'django.middleware.common.CommonMiddleware',
+    'django.middleware.csrf.CsrfViewMiddleware',
+    'django.contrib.auth.middleware.AuthenticationMiddleware',
+    'django.contrib.messages.middleware.MessageMiddleware',
+    'django.middleware.clickjacking.XFrameOptionsMiddleware',
+]
+
+ROOT_URLCONF = 'testrail_bot.urls'
+
+TEMPLATES = [
+    {
+        'BACKEND': 'django.template.backends.django.DjangoTemplates',
+        'DIRS': [],
+        'APP_DIRS': True,
+        'OPTIONS': {
+            'context_processors': [
+                'django.template.context_processors.debug',
+                'django.template.context_processors.request',
+                'django.contrib.auth.context_processors.auth',
+                'django.contrib.messages.context_processors.messages',
+            ],
+        },
+    },
+]
+
+WSGI_APPLICATION = 'testrail_bot.wsgi.application'
+
+
+# Database
+# https://docs.djangoproject.com/en/3.0/ref/settings/#databases
+
+DATABASES = {
+    "default": {
+        "ENGINE": os.environ.get("SQL_ENGINE", "django.db.backends.sqlite3"),
+        "NAME": os.environ.get("SQL_DATABASE", os.path.join(BASE_DIR, "db.sqlite3")),
+        "USER": os.environ.get("SQL_USER", "user"),
+        "PASSWORD": os.environ.get("SQL_PASSWORD", "password"),
+        "HOST": os.environ.get("SQL_HOST", "localhost"),
+        "PORT": os.environ.get("SQL_PORT", "5432"),
+    }
+}
+
+
+# Password validation
+# https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators
+
+AUTH_PASSWORD_VALIDATORS = [
+    {
+        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
+    },
+    {
+        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
+    },
+    {
+        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
+    },
+    {
+        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
+    },
+]
+
+
+# Internationalization
+# https://docs.djangoproject.com/en/3.0/topics/i18n/
+
+LANGUAGE_CODE = 'en-us'
+
+TIME_ZONE = 'UTC'
+
+USE_I18N = True
+
+USE_L10N = True
+
+USE_TZ = True
+
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/3.0/howto/static-files/
+
+STATIC_URL = '/staticfiles/'
+STATIC_ROOT = '/staticfiles'
+
+STATICFILES_DIRS = (
+    os.path.join(BASE_DIR, "media/images"),
+)
+
+MEDIA_URL = "/mediafiles/"
+MEDIA_ROOT = "/mediafiles"
+
+
+# Celery configs
+CELERY_BROKER_URL = 'redis://redis:6379'
+CELERY_RESULT_BACKEND = 'redis://redis:6379'
+CELERY_ACCEPT_CONTENT = ['application/json']
+CELERY_TASK_SERIALIZER = 'json'
+CELERY_RESULT_SERIALIZER = 'json'
+
+# TestRail configs
+TESTRAIL_EMAIL = os.environ.get("TESTRAIL_EMAIL", default="123")
+TESTRAIL_PASSWORD = os.environ.get("TESTRAIL_PASSWORD", default="123")
diff --git a/testrail_bot/testrail_bot/urls.py b/testrail_bot/testrail_bot/urls.py
new file mode 100644
index 0000000..d8b6e77
--- /dev/null
+++ b/testrail_bot/testrail_bot/urls.py
@@ -0,0 +1,23 @@
+"""testrail_bot URL Configuration
+
+The `urlpatterns` list routes URLs to views. For more information please see:
+    https://docs.djangoproject.com/en/3.0/topics/http/urls/
+Examples:
+Function views
+    1. Add an import:  from my_app import views
+    2. Add a URL to urlpatterns:  path('', views.home, name='home')
+Class-based views
+    1. Add an import:  from other_app.views import Home
+    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
+Including another URLconf
+    1. Import the include() function: from django.urls import include, path
+    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
+"""
+from django.contrib import admin
+from django.urls import include, path
+
+
+urlpatterns = [
+    path("", include("control.urls")),
+    path('admin/', admin.site.urls),
+]
diff --git a/testrail_bot/testrail_bot/wsgi.py b/testrail_bot/testrail_bot/wsgi.py
new file mode 100644
index 0000000..5e7bafa
--- /dev/null
+++ b/testrail_bot/testrail_bot/wsgi.py
@@ -0,0 +1,16 @@
+"""
+WSGI config for testrail_bot project.
+
+It exposes the WSGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/3.0/howto/deployment/wsgi/
+"""
+
+import os
+
+from django.core.wsgi import get_wsgi_application
+
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'testrail_bot.settings')
+
+application = get_wsgi_application()