Добавлен проект

This commit is contained in:
stirelshka8 2024-11-28 21:07:52 +03:00
commit 52e9b3aaa6
44 changed files with 3231 additions and 0 deletions

11
.gitignore vendored Normal file
View File

@ -0,0 +1,11 @@
venv
.idea
/flask_session/
/instance/
/migrations/
/logs/
/.init
/.create
/flask.pid
/.env
/test.py

155
README.md Normal file
View File

@ -0,0 +1,155 @@
# Проект dashboard'а команды Printum
**Основные возможности и функционал:**
1. Резервирование виртуальной машины (далее - ВМ) за исполнителем с указанием задачи.
2. Редактирование данных ВМ и удаление из списка.
3. Управление ВМ - запуск, остановка и перезагрузка.
***Описание и функционал:***
    Перед первоначальным запуском необходимо настроить параметры в файле .env:
DB_TYPE - поддерживаемые БД SQLite (указать - sqlite), PostgreSQL (указать - postgresql), MySQL (указать - mysql).
**<u>При установке параметра == sqlite, указывать только DB_NAME.</u>**
DB_USER - имя пользователя БД.
DB_PASS - пароль пользователя БД
DB_HOST - хост БД.
DB_PORT - порт БД.
DB_NAME - имя БД.
SESSION_TYPE - место хранения сессии пользователя, возможна установка параметров file (хранение сессии локально), redis (хранение сессии на сервере Redis)
REDIS_HOST - хост сервера Redis.
REDIS_PORT - порт сервера Redis.
REDIS_DB - номер БД сервера Redis.
REDIS_PASS - пароль БД сервера Redis.
SESSION_LIFETIME - время жизни сессии пользователя.
SESSION_KEY_PREFIX - префикс для сессии.
HYPER1_HOST - хост 1-го гипервизора.
HYPER2_HOST - хост 2-го гипервизора.
HYPER3_HOST - хост 3-го гипервизора.
HYPER4_HOST - хост 4-го гипервизора.
HYPER1_USER - пользователь 1-го гипервизора.
HYPER2_USER - пользователь 2-го гипервизора.
HYPER3_USER - пользователь 3-го гипервизора.
HYPER4_USER - пользователь 4-го гипервизора.
HYPER1_PASS - пароль 1-го гипервизора.
HYPER2_PASS - пароль 2-го гипервизора.
HYPER3_PASS - пароль 3-го гипервизора.
HYPER4_PASS - пароль 4-го гипервизора.
DISABLING_TASK - отключение выполнения фоновых задач (треюуется рестарт сервиса).
PERFORMANCE - параметр устанавливающий как производить полное обновление. По времени (параметр - time) или череч определенный промежуток времени (параметр - period).
FULL_UPDATE - при выборе параметра period установить количество минут через какое проводить полное обновление.
HOUR_FULL_UPDATE - при выборе параметра time установить время в формате 00:00 в которое будет запаскаться полное обновление.
POWER_STATUS_UPDATE - период в минутах через которое будет запускаться обновление статусов ВМ.
REGISTER_OFF - при установленном значение True будет отключена регистрация.
SECRET - секретный ключ.
***Запуск***
    Для запуска сервиса выполните скрипт start.sh с правами администратора. При передаче скрипту параметра -dev приложение запустится в режиме разработки.
Пример:
```bash
sudo ./start.sh
```
При таком запуске приложение будет запущено на 5000 порту.
```bash
sudo ./start.sh -dev
```
При таком запуске приложение будет запущено в режиме разработчика.
```bash
sudo ./start.sh 80
```
При таком запуске приложение будет запущено на 80 порту (порт указать возможно любой).
***Остановка***
Для остановки выполните скрипт stop.sh
Пример:
```bash
sudo ./stop.sh
```
***Обновление***
    Перед обновление <mark>ОБЯЗАТЕЛЬНО</mark> сделать снапшот системы и остановить сервис. После запустить выполнение скрипта create-update.sh (файлы которые "попадут" в обновление указываются в upd-file.txt)
```bash
sudo ./create-update.sh
```
При выполнении с параметром -copy созданный во время выполнения скрипта архив будет передан на сервер проекта в директорию /tmp/update-dashboard (будет запрошен пароль от пользователя user), далее необходимо созданный архив распаковать на сервере проекта в директорю /tmp/update-dashboard и выполнить скрипт update.sh
```bash
sudo ./update.sh
```
При передачи параметра -all файлы которые есть в обновлении но нет в проекте будут добавлены без запроса, иначе на каждый файл будет запрос добавления (обновление будет производится согласно файла upd-file.txt)

265
app.py Normal file
View File

@ -0,0 +1,265 @@
from vms import connect_to_vcenter, get_vm_info_and_save_to_db, update_vm_power_status
from flask import Flask, render_template, flash, send_from_directory, redirect, url_for
from db_manager import db, User, VirtualMachine, Stables
from flask_login import LoginManager, login_required, current_user
from routers.user_routers import user_blueprint
from routers.vm_routers import vm_blueprint
from flask_principal import Principal
from flask_socketio import SocketIO
from flask_migrate import Migrate
from flask_session import Session
from flask.cli import AppGroup
from dotenv import load_dotenv
from docx import Document
from io import BytesIO
import threading
import schedule
import requests
import logging
import redis
import time
import os
import re
# Создаем директорию для логов, если ее нет
log_dir = 'logs'
if not os.path.exists(log_dir):
os.makedirs(log_dir)
# Настройки логирования
log_format = '%(name)s - %(levelname)s - %(message)s'
log_path = os.path.join(log_dir, 'app.log')
logging.basicConfig(filename=log_path, level=logging.INFO, format=log_format, filemode='w')
dotenv_path = os.path.join(os.path.dirname(__file__), '.env')
if os.path.exists(dotenv_path):
load_dotenv(dotenv_path)
else:
exit('[STOP SYSTEM STARTUP] >> Не обнаружен файл переменных окружения ".env". \n'
'Файл должен располагаться на одном уровне с "app.py".')
app = Flask(__name__)
socketio = SocketIO(app)
login_manager = LoginManager()
app.register_blueprint(user_blueprint)
app.register_blueprint(vm_blueprint)
login_manager.init_app(app)
principal = Principal(app)
migrate = Migrate(app, db)
app.secret_key = os.environ.get('SECRET')
# Блок создания кастомных комманд
group_command_one = AppGroup('backtask')
@group_command_one.command('full')
def command():
logging.info('Запустить в ручном режиме выполнение выполнения - full_upd')
full_upd()
@group_command_one.command('power')
def command():
logging.info('Запустить в ручном режиме выполнение выполнения - power_status_upd')
power_status_upd()
@group_command_one.command('version')
def command():
logging.info('Запустить в ручном режиме выполнение запуска - стабильный_upd')
stables_upd()
app.cli.add_command(group_command_one)
# End block
if (os.environ.get('DB_TYPE')).lower() == "sqlite":
app.config['SQLALCHEMY_DATABASE_URI'] = f"sqlite:///{os.environ.get('DB_NAME')}.db"
elif (os.environ.get('DB_TYPE')).lower() == "postgresql":
app.config['SQLALCHEMY_DATABASE_URI'] = (f"postgresql://{os.environ.get('DB_USER')}:{os.environ.get('DB_PASS')}@"
f"{os.environ.get('DB_HOST')}:{os.environ.get('DB_PORT')}/"
f"{os.environ.get('DB_NAME')}")
elif (os.environ.get('DB_TYPE')).lower() == "mysql":
app.config['SQLALCHEMY_DATABASE_URI'] = (f"mysql+mysqlconnector://{os.environ.get('DB_USER')}:"
f"{os.environ.get('DB_PASS')}@{os.environ.get('DB_HOST')}:"
f"{os.environ.get('DB_PORT')}/{os.environ.get('DB_NAME')}")
else:
exit('[DB ERROR] >> Неверно указаны настройки базы данных (параметр - DB_TYPE)!')
db.init_app(app)
if (os.environ.get('SESSION_TYPE')).lower() == 'redis':
app.config['SESSION_TYPE'] = 'redis'
app.config['SESSION_PERMANENT'] = True
app.config['SESSION_USE_SIGNER'] = True
app.config['SESSION_KEY_PREFIX'] = os.environ.get('SESSION_KEY_PREFIX')
app.config['SESSION_REDIS'] = redis.StrictRedis(
host=os.environ.get('REDIS_HOST'),
port=os.environ.get('REDIS_PORT'),
db=os.environ.get('REDIS_DB'),
password=os.environ.get('REDIS_PASS')
)
app.config['PERMANENT_SESSION_LIFETIME'] = int(os.environ.get('SESSION_LIFETIME'))
elif (os.environ.get('SESSION_TYPE')).lower() == 'file':
app.config['SESSION_TYPE'] = 'filesystem'
else:
exit('[SESSION ERROR] >> Неверно указаны настройки сессии(параметр - SESSION_TYPE)!')
Session(app)
@app.route('/')
def index():
total_vm = VirtualMachine.query.count()
number_of_employees = VirtualMachine.query.filter_by(status="Занято").count()
number_of_technical = VirtualMachine.query.filter_by(technical=True).count()
quantity_for_tests = int(total_vm) - (int(number_of_technical) + int(number_of_employees))
stables_version = Stables.query.all()
return render_template('home.html', total_vm=total_vm,
number_of_employees=number_of_employees,
number_of_technical=number_of_technical,
quantity_for_tests=quantity_for_tests,
stables_version=stables_version)
@app.route('/favicon.ico')
def fav():
return send_from_directory(os.path.join(app.root_path, 'static'), 'image/fav.ico')
@app.route('/about')
def about():
return render_template('about.html')
@app.route('/admin')
@login_required
def admin():
if current_user.is_admin:
all_user = User.query.all()
return render_template('admin.html', all_user=all_user)
else:
flash(f'Пользователь {current_user} не администратор!', 'danger')
return redirect(url_for('index'))
@login_manager.user_loader
def load_user(user_id):
return db.session.get(User, user_id)
@login_manager.unauthorized_handler
def unauthorized():
flash('Доступ разрешен только авторизованным!', 'danger')
return render_template('login.html')
vcenter_connections = [
{"host": f"{os.environ.get('HYPER1_HOST')}", "user": f"{os.environ.get('HYPER1_USER')}",
"password": f"{os.environ.get('HYPER1_PASS')}"},
{"host": f"{os.environ.get('HYPER2_HOST')}", "user": f"{os.environ.get('HYPER2_USER')}",
"password": f"{os.environ.get('HYPER2_PASS')}"},
{"host": f"{os.environ.get('HYPER3_HOST')}", "user": f"{os.environ.get('HYPER3_USER')}",
"password": f"{os.environ.get('HYPER3_PASS')}"},
{"host": f"{os.environ.get('HYPER4_HOST')}", "user": f"{os.environ.get('HYPER4_USER')}",
"password": f"{os.environ.get('HYPER4_PASS')}"}
]
def stables_upd():
with app.app_context():
url = "https://s3.printum.io/stablerefs/Printum%20software.docx"
response = requests.get(url)
docx_file = BytesIO(response.content)
doc = Document(docx_file)
pattern = re.compile(r'(\d+\.\d+\.\d+)')
target_values = []
for paragraph in doc.paragraphs:
matches = pattern.findall(paragraph.text)
if matches:
target_values.extend(matches)
monitoring_value = target_values[0]
printmanager_value = target_values[2]
existing_stables = Stables.query.first()
if existing_stables:
existing_stables.monitoring = monitoring_value
existing_stables.printmanager = printmanager_value
db.session.commit()
else:
new_version = Stables(monitoring=monitoring_value, printmanager=printmanager_value)
db.session.add(new_version)
db.session.commit()
logging.info('Фоновая задача «stables_upd» выполнена успешно.')
def power_status_upd():
with app.app_context():
for connection in vcenter_connections:
content = connect_to_vcenter(connection['host'], connection['user'], connection['password'])
if content:
update_vm_power_status(content, connection['host'])
logging.info('Фоновая задача «power_status_upd» выполнена успешно.')
def full_upd():
with app.app_context():
for connection in vcenter_connections:
content = connect_to_vcenter(connection['host'], connection['user'], connection['password'])
if content:
get_vm_info_and_save_to_db(content, connection['host'])
logging.info('Фоновая задача «full_upd» выполнена успешно.')
# Настройки обработки фоновых задач
if (os.environ.get('DISABLING_TASK')).lower() == 'false':
lock = threading.Lock()
def run_tasks():
while True:
lock.acquire()
schedule.run_pending()
lock.release()
time.sleep(1)
schedule.every(int(os.environ.get('STABLES_UPDATE'))).minutes.do(stables_upd)
schedule.every(int(os.environ.get('POWER_STATUS_UPDATE'))).minutes.do(power_status_upd)
if (os.environ.get('PERFORMANCE')).lower() == 'period':
schedule.every(int(os.environ.get('FULL_UPDATE'))).minutes.do(full_upd)
elif (os.environ.get('PERFORMANCE')).lower() == 'time':
schedule.every().day.at(f"{os.environ.get('HOUR_FULL_UPDATE')}").do(full_upd)
else:
logging.error("Неверно указано время или период выполнения полного обновления!")
exit(1)
thread = threading.Thread(target=run_tasks)
thread.start()
else:
logging.error("Выполнение фоновых задач отключено!!")
# Первичная инициализация БД при первом запуске
if not os.path.exists(".init") and os.path.exists(".create"):
print("Первоначальная инициализация и загрузка данных в базу данных осуществляется...")
try:
full_upd()
stables_upd()
with open(".init", "w") as f:
f.write("Первая инициализация БД прошла успешно")
except Exception as e:
logging.error(f"ОШИБКА при первой инициализации: {e}")
if __name__ == '__main__':
socketio.run(app, debug=True, allow_unsafe_werkzeug=True)

46
create-update.sh Executable file
View File

@ -0,0 +1,46 @@
#!/bin/bash
copy_files=false
user_ssh=root
host_ssh=10.0.133.74
RED='\033[0;31m'
GREEN='\033[0;32m'
NC='\033[0m'
for arg in "$@"
do
if [ "$arg" == "-copy" ]; then
copy_files=true
fi
done
archive_name="update_$(date +'%d-%m-%Y_%H-%M').tar.gz"
current_dir=$(pwd)
if [ ! -f "upd-file.txt" ]; then
echo -e "${RED}Ошибка:${NC} файл upd-file.txt не найден."
exit 1
fi
tar -czvf "$archive_name" -T upd-file.txt
if [ $? -ne 0 ]; then
echo -e "${RED}Ошибка при создании архива.${NC}"
exit 1
fi
echo -e "${GREEN}Архивирование завершено. Архив создан в: $current_dir/$archive_name.${NC}"
if [ "$copy_files" == true ]; then
scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -P 22 "$archive_name" $user_ssh@$host_ssh:/tmp/update-dashboard
if [ $? -eq 0 ]; then
echo -e "${GREEN}Архив $archive_name успешно скопирован на удаленный сервер $host_ssh.${NC}"
rm "$archive_name"
echo -e "${GREEN}Архив $archive_name удален.${NC}"
else
echo -e "${RED}Ошибка при копировании архива $archive_name на удаленный сервер $host_ssh.${NC}"
fi
fi

69
db_manager.py Normal file
View File

@ -0,0 +1,69 @@
from flask_sqlalchemy import SQLAlchemy
from passlib.hash import sha256_crypt
from flask_login import UserMixin
from datetime import datetime
db = SQLAlchemy()
class User(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100))
email = db.Column(db.String(100), unique=True)
username = db.Column(db.String(30), unique=True)
password = db.Column(db.String(100))
user_information = db.Column(db.String(300))
registration_date = db.Column(db.DateTime, default=datetime.utcnow)
last_successful_entry = db.Column(db.DateTime)
last_address = db.Column(db.String(30))
is_admin = db.Column(db.Boolean, default=False)
class VirtualMachine(db.Model):
id = db.Column(db.Integer, primary_key=True)
hyper = db.Column(db.String(50))
ip_addres = db.Column(db.String(50))
id_vm = db.Column(db.String(50))
name = db.Column(db.String(100))
os = db.Column(db.String(100))
memory = db.Column(db.Integer)
cpu = db.Column(db.Integer)
power_status = db.Column(db.String(20))
status = db.Column(db.String(20))
task = db.Column(db.String(100))
busy_date = db.Column(db.String(20))
who_borrowed = db.Column(db.String(20))
who_borrowed_username = db.Column(db.String(20))
technical = db.Column(db.Boolean, default=False)
information = db.Column(db.String(1000))
appointment = db.Column(db.String(100))
class Stables(db.Model): # type: ignore
id = db.Column(db.Integer, primary_key=True)
monitoring = db.Column(db.String(100))
printmanager = db.Column(db.String(100))
class Actions(db.Model):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer)
action_type = db.Column(db.String(50))
vm = db.Column(db.Integer)
action_info = db.Column(db.String(100))
action_timestamp = db.Column(db.DateTime, default=datetime.now)
def create_user(name, email, username, password):
hashed_password = sha256_crypt.hash(password)
new_user = User(name=name, email=email, username=username, password=hashed_password)
db.session.add(new_user)
db.session.commit()
def get_user_by_username(username):
return User.query.filter_by(username=username).first()
def get_vm_by_vms(vmhyper, vmname):
return VirtualMachine.query.filter_by(hyper=vmhyper, name=vmname).first()

49
default.env Normal file
View File

@ -0,0 +1,49 @@
#-------Database setup-------
#-------Possible values are sqlite and postgresql-------
DB_TYPE=postgresql
DB_USER=USER
DB_PASS=5512
DB_HOST=192.168.1.111
DB_PORT=5434
DB_NAME=db
#-------Setting up Redis to store sessions-------
#-------Possible values are file and redis-------
SESSION_TYPE=redis
REDIS_HOST=192.168.1.111
REDIS_PORT=6379
REDIS_DB=0
REDIS_PASS=
#-------Setting the session lifetime-------
SESSION_LIFETIME=1200
#-------Setting the session prefix-------
SESSION_KEY_PREFIX=flpy_
#-------Hypervisor settings-------
HYPER1_HOST=5.9.87.101
HYPER2_HOST=135.181.138.180
HYPER3_HOST=136.243.43.245
HYPER4_HOST=65.108.193.220
HYPER1_USER=USER
HYPER2_USER=USER
HYPER3_USER=USER
HYPER4_USER=USER
HYPER1_PASS=0000000000
HYPER2_PASS=0000000000
HYPER3_PASS=0000000000
HYPER4_PASS=0000000000
#-------Disable background tasks. When initially initializing the database, you should DISABLE it.-------
DISABLING_TASK=False
#-------Setting to perform a full update at intervals or at a set time (period/time)-------
PERFORMANCE=time
#-------Setting the value in MINUTES for a complete VM update, at set value PERFORMANCE = period-------
FULL_UPDATE=20
#-------Time to complete a full update in HH:MM format (UTC), at set value PERFORMANCE = time-------
HOUR_FULL_UPDATE=21:00
#-------Setting the time in MINUTES, the frequency of updating VM statuses-------
POWER_STATUS_UPDATE=2
#-------If the value is True then registration will be disabled-------
REGISTER_OFF=False
#-------Setting the secret value-------
SECRET=secret123
#-------Setting the time in MINUTES, the version update frequency-------
STABLES_UPDATE=30

43
forms.py Normal file
View File

@ -0,0 +1,43 @@
from wtforms import Form, StringField, PasswordField, validators, IntegerField, BooleanField
class RegisterForm(Form):
name = StringField('Фамилия', [
validators.Length(min=2, max=40, message='Имя должно быть от 2 до 40 символов')])
username = StringField('Имя пользователя', [
validators.Length(min=2, max=30, message='Имя пользователя должно быть от 2 до 30 символов')])
email = StringField('Email', [
validators.Length(min=2, max=35),
validators.Email(message='Некорректный адрес электронной почты')])
password = PasswordField('Пароль', [
validators.DataRequired(),
validators.Length(min=4, max=20),
validators.EqualTo('confirm', message='Пароли не совпадают')])
confirm = PasswordField('Подтверждение пароля')
class UpdateUser(Form):
name = StringField('Фамилия', [validators.Length(min=1, max=255)])
user_information = StringField('Дополнительная информация', [validators.Length(min=1, max=3000)])
class UpdateVmInfo(Form):
information = StringField('Дополнительная информация', [validators.Length(min=0, max=1000)])
class UpdateUserPass(Form):
new_password = PasswordField('Новый пароль', [validators.Length(min=4, max=255)])
confirm_password = PasswordField('Подтвердите новый пароль',
[validators.EqualTo('new_password', message='Passwords must match')])
class FormVirtualMachine(Form):
hyper = StringField('Адрес гипервизора', [validators.Length(min=1, max=50)])
ip_addres = StringField('Адрес виртуальной машины', [validators.Length(min=1, max=50)])
id_vm = IntegerField('ID виртуальной машины', [validators.NumberRange(min=1, max=100)])
name = StringField('Имя виртуальной машины', [validators.Length(min=1, max=100)])
appointment = StringField('Назначение виртуальной машины', [validators.Length(min=1, max=100)])
os = StringField('Операционная система виртуальной машины', [validators.Length(min=1, max=100)])
memory = IntegerField('ОЗУ виртуальной машины', [validators.NumberRange(min=1, max=10000)])
cpu = IntegerField('ЦПУ виртуальной машины', [validators.NumberRange(min=1, max=100)])
technical = BooleanField()

62
requirements.txt Normal file
View File

@ -0,0 +1,62 @@
alembic==1.12.0
amqp==5.2.0
asgiref==3.7.2
async-timeout==4.0.3
bidict==0.22.1
billiard==4.2.0
blinker==1.6.2
cachelib==0.10.2
click==8.1.7
click-didyoumean==0.3.0
click-plugins==1.1.1
click-repl==0.3.0
dnspython==2.4.2
email-validator==2.0.0.post2
Flask==2.3.3
Flask-CKEditor==0.4.6
Flask-Login==0.6.2
Flask-Migrate==4.0.4
flask-paginate==2022.1.8
Flask-Principal==0.4.0
Flask-Session==0.5.0
Flask-SocketIO==5.3.6
Flask-SQLAlchemy==3.0.5
Flask-Uploads==0.2.1
Flask-WTF==1.1.1
greenlet==2.0.2
h11==0.14.0
humanize==4.9.0
idna==3.4
itsdangerous==2.1.2
Jinja2==3.1.2
kombu==5.3.5
Mako==1.2.4
Markdown==3.4.4
MarkupSafe==2.1.3
mysql-connector-python==8.3.0
passlib==1.7.4
prometheus-client==0.19.0
prompt-toolkit==3.0.43
psycopg2-binary==2.9.9
python-dateutil==2.8.2
python-dotenv==1.0.0
python-engineio==4.8.2
python-socketio==5.11.0
pytz==2023.4
pyvmomi==8.0.2.0.1
redis==5.0.0
rq==1.15.1
schedule==1.2.1
simple-websocket==1.0.0
six==1.16.0
SQLAlchemy==2.0.20
sqlparse==0.4.4
tornado==6.4
typing_extensions==4.7.1
tzdata==2023.4
vine==5.1.0
wcwidth==0.2.13
Werkzeug==2.3.7
wsproto==1.2.0
WTForms==3.0.1
python-docx==1.1.0

0
routers/__init__.py Normal file
View File

172
routers/user_routers.py Normal file
View File

@ -0,0 +1,172 @@
from flask import render_template, flash, redirect, request, url_for, session, Blueprint
from flask_login import login_required, current_user, login_user
from forms import RegisterForm, UpdateUserPass, UpdateUser
from passlib.hash import sha256_crypt
from db_manager import db, User, Actions, get_user_by_username, VirtualMachine
import secrets
import os
user_blueprint = Blueprint('user', __name__)
@user_blueprint.route('/logout')
@login_required
def logout():
session.clear()
flash('Вы вышли из системы', 'success')
return redirect(url_for('index'))
@user_blueprint.route('/register', methods=['GET', 'POST'])
def register():
if (os.environ.get('REGISTER_OFF')).lower() == 'false':
form = RegisterForm(request.form)
if request.method == 'POST' and form.validate():
name = form.name.data
email = form.email.data
username = form.username.data
password = sha256_crypt.hash(str(form.password.data))
existing_user = User.query.filter_by(username=username).first()
existing_email = User.query.filter_by(email=email).first()
if existing_user:
flash('Пользователь с таким именем уже существует.', 'danger')
return redirect(url_for('user.register'))
if existing_email:
flash('Пользователь с таким email уже существует.', 'danger')
return redirect(url_for('user.register'))
new_user = User(name=name, email=email, username=username, password=password, is_admin=False)
new_user.token = secrets.token_hex(16)
db.session.add(new_user)
db.session.commit()
flash('Теперь вы зарегистрированы и можете войти. Добро пожаловать в PrintumVMs!!', 'success')
return redirect(url_for('user.login'))
return render_template('register.html', form=form)
else:
return render_template('register_off.html', img='/static/image/stop.png')
@user_blueprint.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form['username']
password_candidate = request.form['password']
users = User.query.filter_by(username=username).first()
if users and sha256_crypt.verify(password_candidate, users.password):
session['logged_in'] = True
session['username'] = username
session['names'] = users.name
session['user_ip'] = request.remote_addr
session['is_admin'] = users.is_admin
users.last_successful_entry = datetime.now()
users.last_address = session['user_ip']
db.session.commit()
login_user(users)
new_action = Actions(user_id=get_user_by_username(username).id,
action_type='login_user',
action_info=request.remote_addr)
db.session.add(new_action)
db.session.commit()
flash('Вы успешно авторизовались', 'success')
return redirect(url_for('index'))
else:
flash('Неверное имя пользователя или пароль', 'danger')
return render_template('login.html')
return render_template('login.html')
@user_blueprint.route('/update_user_info', methods=['GET', 'POST'])
@login_required
def update_user_info():
if request.method == 'POST':
form = UpdateUser(request.form)
if form.validate():
current_user.name = form.name.data
current_user.user_information = form.user_information.data
db.session.commit()
flash('Информация о пользователе успешно обновлена.', 'success')
else:
flash('Ошибка при обновлении информации о пользователе.', 'danger')
return redirect(url_for('index'))
users = User.query.filter_by(username=session['username']).first()
return render_template('edit_info.html', user=users)
@user_blueprint.route('/update_pass', methods=['GET', 'POST'])
@login_required
def update_pass():
if request.method == 'POST':
form = UpdateUserPass(request.form)
if form.new_password.data and form.validate():
current_user.password = sha256_crypt.hash(str(form.new_password.data))
db.session.commit()
flash('Пароль именён!.', 'success')
else:
flash('Ошибка смены пароля!.', 'danger')
return redirect(url_for('index'))
users = User.query.filter_by(username=session['username']).first()
return render_template('edit_pass.html', user=users)
@user_blueprint.route('/delete_user/<string:username>', methods=['POST'])
@login_required
def delete_user(username):
if current_user.is_admin:
user_to_delete = User.query.filter_by(username=username).first()
if user_to_delete:
db.session.delete(user_to_delete)
db.session.commit()
flash('Пользователь успешно удален.', 'success')
return redirect(url_for('admin'))
else:
flash('Ошибка при удалении пользователя.', 'danger')
return redirect(url_for('admin'))
else:
flash('🔔 Вы не администратор! 🔔', 'danger')
return redirect(url_for('index'))
@user_blueprint.route('/user_info/<string:username>', methods=['GET'])
@login_required
def user_info(username):
if current_user.is_admin:
page = request.args.get('page', 1, type=int)
user = User.query.filter_by(username=username).first()
user_data = db.session.query(Actions, VirtualMachine.name, VirtualMachine.hyper) \
.outerjoin(VirtualMachine, Actions.vm == VirtualMachine.id) \
.filter(Actions.user_id == user.id) \
.order_by(Actions.action_timestamp.desc()) \
.paginate(page=page, per_page=50)
return render_template('user_info.html', user_data=user_data, user_pg=user.username)
else:
flash('🔔 Вы не администратор! 🔔', 'danger')
return redirect(url_for('index'))

289
routers/vm_routers.py Normal file
View File

@ -0,0 +1,289 @@
import logging
from flask import render_template, Blueprint, request, redirect, url_for, flash, session
from flask_login import login_required, current_user
from forms import FormVirtualMachine, UpdateVmInfo
from db_manager import db, VirtualMachine, Actions, get_user_by_username, get_vm_by_vms
from dotenv import load_dotenv
from datetime import datetime
from pyVim import connect
from sqlalchemy import func
import ssl
import os
vm_blueprint = Blueprint('vm', __name__)
load_dotenv()
hypervisors = {
"5.9.87.101": {"user": os.getenv("HYPER1_USER"), "password": os.getenv("HYPER1_PASS"),
"host": os.getenv("HYPER1_HOST")},
"135.181.138.180": {"user": os.getenv("HYPER2_USER"), "password": os.getenv("HYPER2_PASS"),
"host": os.getenv("HYPER2_HOST")},
"136.243.43.245": {"user": os.getenv("HYPER3_USER"), "password": os.getenv("HYPER3_PASS"),
"host": os.getenv("HYPER3_HOST")},
"65.108.193.220": {"user": os.getenv("HYPER4_USER"), "password": os.getenv("HYPER4_PASS"),
"host": os.getenv("HYPER4_HOST")}
}
@vm_blueprint.route('/dashboard', methods=['GET', 'POST'])
@login_required
def dashboard():
virtual_machines = VirtualMachine.query.all()
return render_template('dashboard.html', virtual_machines=virtual_machines)
@vm_blueprint.route('/vms', methods=['GET', 'POST'])
@login_required
def vms():
virtual_machines = VirtualMachine.query.all()
return render_template('vms.html', virtual_machines=virtual_machines)
@vm_blueprint.route('/manage', methods=['GET', 'POST'])
@login_required
def manage():
virtual_machines = VirtualMachine.query.all()
return render_template('manage.html', virtual_machines=virtual_machines)
@vm_blueprint.route('/add', methods=['GET', 'POST'])
@login_required
def add_virtual_machine():
form = FormVirtualMachine(request.form)
if request.method == 'POST':
new_vm = VirtualMachine(hyper=form.hyper.data, id_vm=form.id_vm.data, name=form.name.data,
os=form.os.data, memory=form.memory.data, cpu=form.cpu.data,
technical=form.technical.data, status="Свободно")
db.session.add(new_vm)
db.session.commit()
return redirect(url_for('vm.dashboard'))
return render_template('add.html', form=form)
@vm_blueprint.route('/delete/<int:id>', methods=['GET', 'POST'])
@login_required
def delete_virtual_machine(id):
vm_to_delete = VirtualMachine.query.get_or_404(id)
new_action = Actions(user_id=get_user_by_username(current_user.username).id,
action_type='del_vm',
vm=get_vm_by_vms(vm_to_delete.hyper, vm_to_delete.name).id)
db.session.add(new_action)
db.session.commit()
db.session.delete(vm_to_delete)
db.session.commit()
flash(f'Виртуальная машина {vm_to_delete.name} удалена!', 'success')
return redirect(url_for('vm.vms'))
@vm_blueprint.route('/edit/<int:id>', methods=['GET', 'POST'])
@login_required
def edit_virtual_machine(id):
vm_to_edit = VirtualMachine.query.get_or_404(id)
if request.method == 'POST':
vm_to_edit.hyper = request.form['hyper']
vm_to_edit.ip_addres = request.form['ip_addres']
vm_to_edit.name = request.form['name']
vm_to_edit.appointment = request.form['appointment']
vm_to_edit.os = request.form['os']
vm_to_edit.memory = request.form['memory']
vm_to_edit.cpu = request.form['cpu']
vm_to_edit.technical = request.form.get('technical', False) == 'on'
new_action = Actions(user_id=get_user_by_username(current_user.username).id,
action_type='edit_vm',
vm=get_vm_by_vms(vm_to_edit.hyper, vm_to_edit.name).id)
db.session.add(new_action)
db.session.commit()
flash(f'Виртуальная машина {vm_to_edit.name} отредактирована!', 'success')
return redirect(url_for('vm.vms'))
return render_template('edit.html', virtual_machine=vm_to_edit)
@vm_blueprint.route('/occupy/<int:id>', methods=['GET', 'POST'])
@login_required
def occupy_vm(id):
vm = VirtualMachine.query.get_or_404(id)
if request.method == 'POST':
vm.status = 'Занято'
vm.task = request.form['task']
vm.busy_date = datetime.now().strftime('%d.%m.%Y %H:%M')
vm.who_borrowed = current_user.name
vm.who_borrowed_username = current_user.username
new_action = Actions(user_id=get_user_by_username(current_user.username).id,
action_type='occupy_vm',
vm=get_vm_by_vms(vm.hyper, vm.name).id)
db.session.add(new_action)
db.session.commit()
return redirect(url_for('vm.dashboard'))
return render_template('index.html', virtual_machines=VirtualMachine.query.all())
@vm_blueprint.route('/release/<int:id>', methods=['GET', 'POST'])
@login_required
def release_vm(id):
vm = VirtualMachine.query.get_or_404(id)
if request.method == 'POST':
vm.status = 'Свободно'
vm.task = 'Свободно'
vm.busy_date = 'Свободно'
vm.who_borrowed = 'Свободно'
vm.who_borrowed_username = 'Свободно'
new_action = Actions(user_id=get_user_by_username(current_user.username).id,
action_type='release_vm',
vm=get_vm_by_vms(vm.hyper, vm.name).id)
db.session.add(new_action)
db.session.commit()
return redirect(url_for('vm.dashboard'))
return redirect(url_for('index'))
def disable_ssl_verification():
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS)
ssl_context.verify_mode = ssl.CERT_NONE
return ssl_context
def find_vm_by_name(content, vm_name):
vm = None
for child in content.rootFolder.childEntity:
if hasattr(child, 'vmFolder'):
vm_folder = child.vmFolder
vm_list = vm_folder.childEntity
for vm_obj in vm_list:
if vm_obj.name == vm_name:
vm = vm_obj
break
if vm:
break
return vm
def read_content(hyper):
if hyper in hypervisors:
user = hypervisors[hyper]["user"]
password = hypervisors[hyper]["password"]
host = hypervisors[hyper]["host"]
else:
flash(f'Узел {hyper} не найден', 'danger')
return redirect(url_for('vm.manage'))
service_instance = connect.SmartConnect(
host=host,
user=user,
pwd=password,
sslContext=disable_ssl_verification()
)
content = service_instance.RetrieveContent()
return content
@vm_blueprint.route('/start/<string:name>/<string:hyper>', methods=['GET', 'POST'])
@login_required
def start_virtual_machine(name, hyper):
vm = find_vm_by_name(read_content(hyper), name)
try:
if vm:
vm.PowerOnVM_Task()
new_action = Actions(user_id=get_user_by_username(current_user.username).id,
action_type='start_vm',
vm=get_vm_by_vms(hyper, name).id)
db.session.add(new_action)
db.session.commit()
flash(f'Запущена виртуальная машина: {name} на гипервизоре {hyper}.', 'success')
return redirect(url_for('vm.manage'))
else:
flash(f'Ошибка запуска виртуальной машины: {name} на гипервизоре {hyper}.', 'danger')
return redirect(url_for('vm.manage'))
except Exception as e:
flash(f'При выполнении операции запуска возникла ошибка {e}', 'danger')
return redirect(url_for('vm.manage'))
@vm_blueprint.route('/stop/<string:name>/<string:hyper>', methods=['GET', 'POST'])
@login_required
def stop_virtual_machine(name, hyper):
vm = find_vm_by_name(read_content(hyper), name)
try:
if vm:
vm.PowerOffVM_Task()
new_action = Actions(user_id=get_user_by_username(current_user.username).id,
action_type='stop_vm',
vm=get_vm_by_vms(hyper, name).id)
db.session.add(new_action)
db.session.commit()
flash(f'Остановлена виртуальная машина: {name} на гипервизоре {hyper}.', 'success')
return redirect(url_for('vm.manage'))
else:
flash(f'Ошибка остановки виртуальной машины: {name} на гипервизоре {hyper}.', 'danger')
return redirect(url_for('vm.manage'))
except Exception as e:
flash(f'При выполнении операции остановки возникла ошибка {e}', 'danger')
return redirect(url_for('vm.manage'))
@vm_blueprint.route('/restart/<string:name>/<string:hyper>', methods=['GET', 'POST'])
@login_required
def restart_virtual_machine(name, hyper):
vm = find_vm_by_name(read_content(hyper), name)
try:
if vm:
vm.ResetVM_Task()
new_action = Actions(user_id=get_user_by_username(current_user.username).id,
action_type='restart_vm',
vm=get_vm_by_vms(hyper, name).id)
db.session.add(new_action)
db.session.commit()
flash(f'Перезагружена виртуальная машина: {name} на гипервизоре {hyper}.', 'success')
return redirect(url_for('vm.manage'))
else:
flash(f'Ошибка перезагрузки виртуальной машины: {name} на гипервизоре {hyper}.', 'danger')
return redirect(url_for('vm.manage'))
except Exception as e:
flash(f'При выполнении операции перезагрузки возникла ошибка {e}', 'danger')
return redirect(url_for('vm.manage'))
@vm_blueprint.route('/vm_info/<string:name>/<string:hyper>', methods=['GET', 'POST'])
@login_required
def vm_info(name, hyper):
all_data_vm = VirtualMachine.query.filter_by(hyper=hyper, name=name).first()
if not all_data_vm:
flash('Виртуальная машина не найдена.', 'danger')
return redirect(url_for('vm.dashboard'))
form = UpdateVmInfo(request.form)
if request.method == 'POST' and form.validate():
all_data_vm.information = form.information.data
db.session.commit()
flash('Комментарий к виртуальной машине успешно обновлена.', 'success')
return redirect(url_for('vm.dashboard'))
elif request.method == 'POST':
flash('Ошибка при обновлении комментария к виртуальной машине.', 'danger')
return render_template('vm_info.html', form=form, all_data_vm=all_data_vm, information=all_data_vm.information)
@vm_blueprint.route('/statistics/', methods=['GET', 'POST'])
@login_required
def vm_statistics():
try:
most_used_vm = db.session.query(Actions.vm, func.count(Actions.vm).label('vm_count')).group_by(Actions.vm).order_by(
func.count(Actions.vm).desc()).first()
action_counts = db.session.query(Actions.action_type,
func.count(Actions.action_type).label('action_count')).group_by(
Actions.action_type).order_by(func.count(Actions.action_type).desc()).all()
data_vm = VirtualMachine.query.filter_by(id=most_used_vm[0]).first()
return render_template('statistics.html', data_vm=data_vm, action_counts=action_counts)
except Exception as e:
logging.error(f"Ошибка отображения статистики {e}")
return render_template('statistics.html', data_vm=None, action_counts=None)

71
start.sh Executable file
View File

@ -0,0 +1,71 @@
#!/bin/bash
if [[ -z $1 ]]; then
start_port=5000
else
start_port=$1
fi
rebase=false
develop=false
for arg in "$@"
do
if [ "$arg" == "-rebase" ]; then
rebase=true
fi
if [ "$arg" == "-dev" ]; then
develop=true
fi
done
if [ "$1" == "-rebase" ]; then
rebase=true
else
rebase=false
fi
if [ ! -d "logs" ]; then
mkdir -p "logs"
fi
if [ "$rebase" == true ]; then
touch ./.create
touch ./.init
echo "The setup is complete. Start in normal mode."
exit 1
else
if [ ! -d "migrations" ]; then
if [ "$develop" != true ]; then
python3 -m venv venv
source ./venv/bin/activate
fi
pip install --no-cache-dir -r requirements.txt
sed -i 's/DISABLING_TASK=.*/DISABLING_TASK=True/g' .env
flask db init
flask db migrate
flask db upgrade
touch ./.create
sed -i 's/DISABLING_TASK=.*/DISABLING_TASK=False/g' .env
if [ "$develop" != true ]; then
deactivate
fi
fi
fi
if [ "$develop" != true ]; then
source ./venv/bin/activate
nohup flask run --host=0.0.0.0 --port="$start_port" > logs/flask.log 2>&1 &
sed -i "s/processes=\$(lsof -ti :.*)/processes=\$(lsof -ti :$start_port)/g" stop.sh
echo $! > flask.pid
else
python app.py > logs/flask.log 2>&1 &
echo $! > flask.pid
fi

BIN
static/image/bg-01.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
static/image/fav.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
static/image/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

BIN
static/image/stop.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

BIN
static/off.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 607 KiB

BIN
static/on.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 602 KiB

BIN
static/sotp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

18
stop.sh Executable file
View File

@ -0,0 +1,18 @@
#!/bin/bash
PID=$(cat flask.pid)
kill $PID
processes=$(lsof -ti :5000)
if [ -z "$processes" ]; then
echo "No processes"
else
echo "$processes"
for pid in $processes; do
kill $pid
done
fi
rm "flask.pid"

8
templates/about.html Normal file
View File

@ -0,0 +1,8 @@
{% extends 'layout.html' %}
{% block tytle %} Информация {% endblock%}
{% block body %}
<h1>Информация</h1>
<p>BlogIt is a blog web app built by Sophia Iroegbu to expand her knowledge on flask.</p>
{% endblock %}

74
templates/add.html Normal file
View File

@ -0,0 +1,74 @@
{% extends 'layout.html' %}
{% block tytle %} Добавить ВМ {% endblock%}
{% block style %}
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f0f0f0;
}
h1 {
color: #333;
}
form {
background-color: #fff;
padding: 20px;
border-radius: 5px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
width: 300px;
margin: 20px auto;
}
input[type="text"] {
width: calc(100% - 22px);
padding: 10px;
margin-bottom: 10px;
border: 1px solid #ccc;
border-radius: 5px;
}
button[type="submit"] {
background-color: #007bff;
color: #fff;
padding: 10px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
}
button[type="submit"]:hover {
background-color: #0056b3;
}
{% endblock %}
{% block body %}
<form method="post" action="">
{% from "includes/_formhelpers.html" import render_field%}
<div class="form-group">
{{ render_field(form.hyper, class_='form-control') }}
</div>
<div class="form-group">
{{ render_field(form.ip_addres, class_='form-control') }}
</div>
<div class="form-group">
{{ render_field(form.name, class_='form-control') }}
</div>
<div class="form-group">
{{ render_field(form.os, class_='form-control') }}
</div>
<div class="form-group">
{{ render_field(form.memory, class_='form-control') }}
</div>
<div class="form-group">
{{ render_field(form.cpu, class_='form-control') }}
</div>
<div class="form-group">
{{ render_field(form.technical, class_='form-control') }}
</div>
<p><input type="submit" class="btn btn-primary" value="Сохранить"></p>
</form>
{% endblock %}

101
templates/admin.html Normal file
View File

@ -0,0 +1,101 @@
{% extends 'layout.html' %}
{% block title %} Панель администратора {% endblock %}
{% block style %}
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
box-sizing: border-box;
}
.jumbotron {
background-color: #f8f9fa;
padding: 20px;
margin: 20px;
border-radius: 5px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.jumbotron h1 {
color: #ff7077;
}
.jumbotron h2 {
color: #007bff;
}
.info-block {
margin: 10px;
padding: 10px;
background-color: #f1f1f1;
border-radius: 5px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.tile-container {
display: flex;
flex-wrap: nowrap; /* Ensure tiles stay in a single line */
}
.info-block.stable-version {
flex: 1; /* Distribute space equally among tiles */
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 8px;
text-align: left;
border-bottom: 1px solid #ddd;
}
th {
background-color: #f2f2f2;
color: #333;
}
{% endblock %}
{% block body %}
<table>
<thead>
<tr>
<th>Имя</th>
<th>UserName</th>
<th>Email</th>
<th>Дата регистрации</th>
<th>Последний успешный вход</th>
<th>Последний IP</th>
<th>Админ?</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
{% for user in all_user %}
<tr>
<td><a href="/user_info/{{ user.username }}">{{ user.name }}</a></td>
<td>{{ user.username }}</td>
<td>{{ user.email }}</td>
<td>{{ user.registration_date }}</td>
<td>{{ user.last_successful_entry }}</td>
<td>{{ user.last_address }}</td>
<td>{{ user.is_admin }}</td>
{% if user.is_admin %}
<td>Админа не удалять!</td>
{% else %}
<td><button class="action-button restart" onclick="confirmAction('{{ url_for('user.delete_user', username=user.username) }}', 'Вы уверены, что хотите удалить пользователя {{ user.username }}?')">Удалить</button></td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
<script>
function confirmAction(url, message) {
if (confirm(message)) {
var form = document.createElement('form');
form.setAttribute('method', 'post');
form.setAttribute('action', url);
document.body.appendChild(form);
form.submit();
}
}
</script>
{% endblock %}

376
templates/dashboard.html Normal file
View File

@ -0,0 +1,376 @@
{% extends 'layout.html' %}
{% block title %} Дашборд {% endblock %}
{% block style %}
h1 {
color: #333;
text-align: center;
margin-top: 20px;
}
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f0f0f0;
}
.table-container {
overflow-x: hidden;
}
table {
width: 100%;
margin-left: 0;
padding: 0;
border-collapse: collapse;
border-radius: 5px;
overflow: hidden;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
max-width: 100%;
}
th, td {
padding: 8px;
text-align: center;
border-bottom: 1px solid #ddd;
font-size: 11px;
width: 10%;
}
th {
background-color: #007bff;
color: #fff;
}
tr:hover {
background-color: #f5f5f5;
}
button {
border: none;
background-color: #007bff;
color: #fff;
padding: 2px 5px;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #0056b3;
}
a.add {
display: inline-block;
background-color: #007bff;
color: #ff1;
padding: 5px 10px;
border-radius: 5px;
text-align: center;
text-decoration: none;
margin-right: 10px;
}
a.add:hover {
background-color: #0056b3;
}
.search-container {
margin-bottom: 20px;
}
.button-container {
margin-bottom: 20px;
}
#searchInput {
width: 50%;
padding: 8px;
margin-bottom: 10px;
box-sizing: border-box;
border: 1px solid #ccc;
border-radius: 4px;
}
{% endblock %}
{% block body %}
<div class="search-container">
<input type="text" id="searchInput" onkeyup="searchTable()" placeholder="Поиск ...">
</div>
<div class="button-container">
<a href="{{ url_for('vm.add_virtual_machine') }}" class="add">Добавить ВМ</a>
<a href="/vms" class="add">Редактировать</a>
<a href="/manage" class="add">Управление</a>
</div>
<div class="table-container">
<table>
<thead>
<tr>
<th>#</th>
<th>Адрес</th>
<th>Название</th>
<th>Назначение</th>
<th>ОC</th>
<th>Состояние</th>
<th>Кто занял</th>
<th>Под задачей</th>
<th>Когда занято</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
{% for vm in virtual_machines %}
{% if vm.status != 'Свободно' %}
<tr style="background-color: #ffcccc;">
<td><a href="/vm_info/{{ vm.name }}/{{ vm.hyper }}">{{ loop.index }}</a></td>
<td>
{% if vm.ip_addres == None %}
Данных нет
{% else %}
{{ vm.ip_addres }}
{% endif %}
</td>
<td>{{ vm.name }}</td>
<td>
{% if vm.appointment == None %}
Данных нет
{% else %}
{{ vm.appointment }}
{% endif %}
</td>
<td>{{ vm.os }}</td>
<td>
{% if vm.power_status == 'poweredOn' %}
<img src="{{ url_for('static', filename='on.png') }}" width="20" height="20">
{% elif vm.power_status == 'poweredOff' %}
<img src="{{ url_for('static', filename='off.png') }}" width="20" height="20">
{% else %}
Не определено
{% endif %}
</td>
<td>
{% if vm.technical != True %}
{% if vm.who_borrowed == None %}
Данных нет
{% else %}
{{ vm.who_borrowed }}
{% endif %}
{% else %}
<img src="{{ url_for('static', filename='sotp.png') }}" width="20" height="20">
{% endif %}
</td>
<td>
{% if vm.technical != True %}
{{ vm.task }}
{% else %}
<img src="{{ url_for('static', filename='sotp.png') }}" width="20" height="20">
{% endif %}
</td>
<td>
{% if vm.technical != True %}
{{ vm.busy_date }}
{% else %}
<img src="{{ url_for('static', filename='sotp.png') }}" width="20" height="20">
{% endif %}
</td>
<td>
{% if vm.technical != True %}
{% if vm.status == 'Свободно' %}
<button onclick="openOccupationForm({{ vm.id }})">ЗАНЯТЬ</button>
{% else %}
<button onclick="releaseOccupation({{ vm.id }})">ОСВОБОДИТЬ</button>
{% endif %}
{% else %}
<img src="{{ url_for('static', filename='sotp.png') }}" width="20" height="20">
{% endif %}
</td>
</tr>
{% endif %}
{% endfor %}
{% for vm in virtual_machines %}
{% if vm.status == 'Свободно' and vm.technical != True %}
<tr>
<td><a href="/vm_info/{{ vm.name }}/{{ vm.hyper }}">{{ loop.index }}</a></td>
<td>
{% if vm.ip_addres == None %}
Данных нет
{% else %}
{{ vm.ip_addres }}
{% endif %}
</td>
<td>{{ vm.name }}</td>
<td>
{% if vm.appointment == None %}
Данных нет
{% else %}
{{ vm.appointment }}
{% endif %}
</td>
<td>{{ vm.os }}</td>
<td>
{% if vm.power_status == 'poweredOn' %}
<img src="{{ url_for('static', filename='on.png') }}" width="20" height="20">
{% elif vm.power_status == 'poweredOff' %}
<img src="{{ url_for('static', filename='off.png') }}" width="20" height="20">
{% else %}
Не определено
{% endif %}
</td>
<td>
{% if vm.technical != True %}
{% if vm.who_borrowed == None %}
Данных нет
{% else %}
{{ vm.who_borrowed }}
{% endif %}
{% else %}
<img src="{{ url_for('static', filename='sotp.png') }}" width="20" height="20">
{% endif %}
</td>
<td>
{% if vm.technical != True %}
{{ vm.task }}
{% else %}
<img src="{{ url_for('static', filename='sotp.png') }}" width="20" height="20">
{% endif %}
</td>
<td>
{% if vm.technical != True %}
{{ vm.busy_date }}
{% else %}
<img src="{{ url_for('static', filename='sotp.png') }}" width="20" height="20">
{% endif %}
</td>
<td>
{% if vm.technical != True %}
{% if vm.status == 'Свободно' %}
<button onclick="openOccupationForm({{ vm.id }})">ЗАНЯТЬ</button>
{% else %}
<button onclick="releaseOccupation({{ vm.id }})">ОСВОБОДИТЬ</button>
{% endif %}
{% else %}
<img src="{{ url_for('static', filename='sotp.png') }}" width="20" height="20">
{% endif %}
</td>
</tr>
{% endif %}
{% endfor %}
{% for vm in virtual_machines %}
{% if vm.technical == True %}
<tr style="background-color: #ffff99;">
<td><a href="/vm_info/{{ vm.name }}/{{ vm.hyper }}">{{ loop.index }}</a></td>
<td>
{% if vm.ip_addres == None %}
Данных нет
{% else %}
{{ vm.ip_addres }}
{% endif %}
</td>
<td>{{ vm.name }}</td>
<td>
{% if vm.appointment == None %}
Данных нет
{% else %}
{{ vm.appointment }}
{% endif %}
</td>
<td>{{ vm.os }}</td>
<td>
{% if vm.power_status == 'poweredOn' %}
<img src="{{ url_for('static', filename='on.png') }}" width="20" height="20">
{% elif vm.power_status == 'poweredOff' %}
<img src="{{ url_for('static', filename='off.png') }}" width="20" height="20">
{% else %}
Не определено
{% endif %}
</td>
<td>х</td>
<td>х</td>
<td>х</td>
<td>
<img src="{{ url_for('static', filename='sotp.png') }}" width="20" height="20">
</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
</div>
<script>
function openOccupationForm(vmId) {
var task = prompt("Введите задачу:");
if (task !== null) {
var currentDate = new Date();
var busyDate = currentDate.toLocaleString();
var form = document.createElement('form');
form.method = 'post';
form.action = '/occupy/' + vmId;
var taskInput = document.createElement('input');
taskInput.type = 'hidden';
taskInput.name = 'task';
taskInput.value = task;
form.appendChild(taskInput);
var busyDateInput = document.createElement('input');
busyDateInput.type = 'hidden';
busyDateInput.name = 'busy_date';
busyDateInput.value = busyDate;
form.appendChild(busyDateInput);
document.body.appendChild(form);
form.submit();
}
}
function releaseOccupation(vmId) {
var confirmation = confirm("Вы уверены, что хотите освободить эту ВМ?");
if (confirmation) {
var form = document.createElement('form');
form.method = 'post';
form.action = '/release/' + vmId;
var statusInput = document.createElement('input');
statusInput.type = 'hidden';
statusInput.name = 'status';
statusInput.value = 'Свободно';
form.appendChild(statusInput);
document.body.appendChild(form);
form.submit();
}
}
function searchTable() {
var input, filter, table, tr, td, i, txtValue;
input = document.getElementById("searchInput");
filter = input.value.toUpperCase();
table = document.getElementsByTagName("table")[0];
tr = table.getElementsByTagName("tr");
for (i = 0; i < tr.length; i++) {
td = tr[i].getElementsByTagName("td");
for (var j = 0; j < td.length; j++) {
if (td[j]) {
txtValue = td[j].textContent || td[j].innerText;
if (txtValue.toUpperCase().indexOf(filter) > -1) {
tr[i].style.display = "";
break;
} else {
tr[i].style.display = "none";
}
}
}
}
}
</script>
{% endblock %}

81
templates/edit.html Normal file
View File

@ -0,0 +1,81 @@
{% extends 'layout.html' %}
{% block tytle %} Редактирование {{virtual_machine.name}} {% endblock%}
{% block style %}
body {
font-family: Arial, sans-serif;
background-color: #f0f0f0;
padding: 20px;
margin: 0;
}
h1 {
color: #333;
}
form {
background-color: #fff;
padding: 20px;
border-radius: 5px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
width: 300px;
margin: 20px auto;
}
input[type="text"] {
width: calc(100% - 22px);
padding: 10px;
margin-bottom: 10px;
border: 1px solid #ccc;
border-radius: 5px;
}
input[type="text"]#appointment {
border: 1px solid red; /* обводка красным */
}
button[type="submit"] {
background-color: #007bff;
color: #fff;
padding: 10px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
}
button[type="submit"]:hover {
background-color: #0056b3;
}
{% endblock %}
{% block body %}
<h1>Редактирование {{virtual_machine.name}}</h1>
<form action="/edit/{{virtual_machine.id}}" method="post">
<label for="hyper">Гипервизор:</label>
<input type="text" id="hyper" name="hyper" value="{{virtual_machine.hyper}}" placeholder="Гипервизор">
<label for="ip_addres">Адрес:</label>
<input type="text" id="ip_addres" name="ip_addres" value="{{virtual_machine.ip_addres}}" placeholder="ID на гипере">
<label for="name">Название:</label>
<input type="text" id="name" name="name" value="{{virtual_machine.name}}" placeholder="Название">
<label for="name" style="color: red;">Назначение (запрещено редактировать без согласования с Дмитрием Двойниковым!):</label>
<input type="text" id="appointment" name="appointment" value="{{virtual_machine.appointment}}" placeholder="Назначение" style="border: 1px solid red;">
<label for="os">Операционная система:</label>
<input type="text" id="os" name="os" value="{{virtual_machine.os}}" placeholder="Операционная система">
<label for="memory">ОЗУ:</label>
<input type="text" id="memory" name="memory" value="{{virtual_machine.memory}}" placeholder="Память (Гб)">
<label for="cpu">ЦПУ:</label>
<input type="text" id="cpu" name="cpu" value="{{virtual_machine.cpu}}" placeholder="ЦПУ">
<label for="technical">Техническая машина</label>
<input type="checkbox" id="technical" name="technical" {% if virtual_machine.technical %}checked{% endif %}>
<button type="submit">Сохранить</button>
</form>
{% endblock %}

126
templates/edit_info.html Normal file
View File

@ -0,0 +1,126 @@
{% extends 'layout.html' %}
{% block title %} Редактирование профиля {% endblock %}
{% block style %}
.profile-heading {
background-color: #3498db;
padding: 25px;
border-radius: 20px 20px 20px 20px;
color: white;
text-align: center;
margin: 20px;
}
.profile-section {
padding: 20px;
border: 1px solid #e0e0e0;
border-radius: 20px 20px 20px 20px;
box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.1);
background-color: #ffffff;
margin-bottom: 20px;
}
.profile-section-nav {
padding: 20px;
border: 1px solid #e0e0e0;
border-radius: 20px;
box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.1);
background-color: #ffffff;
margin-bottom: 20px;
display: flex;
justify-content: center;
align-items: center;
}
.profile-section h3 {
font-size: 1.5rem;
margin-bottom: 20px;
text-align: center;
color: #3498db;
text-transform: uppercase;
}
.profile-form-group {
margin-bottom: 20px;
}
.profile-label {
font-weight: bold;
}
.profile-input {
width: 100%;
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
}
.profile-submit-button {
background-color: #3498db;
border: none;
border-radius: 5px;
padding: 10px 20px;
color: #ffffff;
font-size: 1rem;
text-decoration: none;
transition: background-color 0.2s;
}
.profile-input-info {
width: 100%;
height: 200px;
resize: vertical;
}
{% endblock %}
{% block body %}
<section class="profile-section">
<h3>Изменение данных профиля</h3>
<form method="POST" action="{{ url_for('user.update_user_info') }}" id="user-info-form">
<div class="profile-form-group">
<label for="name" class="profile-label">Имя:</label>
<input type="text" id="name" class="profile-input" name="name" value="{{ current_user.name }}" required>
</div>
<div class="profile-form-group">
<label for="user_information" class="profile-label">Заметки:</label>
<textarea id="user_information" class="profile-input-info" name="user_information" required>{{ current_user.user_information }}</textarea>
<span id="character-count">0</span>/3000 символов
</div>
<button type="submit" class="profile-submit-button">Сохранить</button>
</form>
</section>
<script>
const textareaElement = document.getElementById("user_information");
const characterCountElement = document.getElementById("character-count");
function initializeCharacterCount() {
const currentText = textareaElement.value;
const currentLength = currentText.length;
characterCountElement.textContent = currentLength;
if (currentLength > 300) {
characterCountElement.classList.add("character-limit-exceeded");
} else {
characterCountElement.classList.remove("character-limit-exceeded");
}
}
window.addEventListener("load", initializeCharacterCount);
textareaElement.addEventListener("input", function() {
const currentText = textareaElement.value;
const currentLength = currentText.length;
characterCountElement.textContent = currentLength;
if (currentLength > 300) {
characterCountElement.classList.add("character-limit-exceeded");
} else {
characterCountElement.classList.remove("character-limit-exceeded");
}
});
</script>
{% endblock %}

79
templates/edit_pass.html Normal file
View File

@ -0,0 +1,79 @@
{% extends 'layout.html' %}
{% block title %} Изменение пароля {% endblock %}
{% block style %}
.profile-heading {
background-color: #3498db;
padding: 25px;
border-radius: 20px 20px 20px 20px;
color: white;
text-align: center;
margin: 20px;
}
.profile-section {
padding: 20px;
border: 1px solid #e0e0e0;
border-radius: 20px 20px 20px 20px;
box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.1);
background-color: #ffffff;
margin-bottom: 20px;
}
.profile-section h3 {
font-size: 1.5rem;
margin-bottom: 20px;
text-align: center;
color: #3498db;
text-transform: uppercase;
}
.profile-form-group {
margin-bottom: 20px;
}
.profile-label {
font-weight: bold;
}
.profile-input {
width: 100%;
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
}
.profile-submit-button {
background-color: #3498db;
border: none;
border-radius: 5px;
padding: 10px 20px;
color: #ffffff;
font-size: 1rem;
text-decoration: none;
transition: background-color 0.2s;
}
.profile-submit-button:hover {
background-color: #6500db;
}
{% endblock %}
{% block body %}
<section class="profile-section">
<h3>Изменение пароля пользователя</h3>
<form method="POST" action="{{ url_for('user.update_pass') }}" id="user-info-form">
<div class="profile-form-group">
<label for="new_password" class="profile-label">Новый пароль:</label>
<input type="password" id="new_password" class="profile-input" name="new_password" required>
</div>
<div class="profile-form-group">
<label for="confirm_password" class="profile-label">Подтвердите новый пароль:</label>
<input type="password" id="confirm_password" class="profile-input" name="confirm_password" required>
</div>
<button type="submit" class="profile-submit-button">Сохранить</button>
</form>
</section>
{% endblock %}

83
templates/home.html Normal file
View File

@ -0,0 +1,83 @@
{% extends 'layout.html' %}
{% block title %} Домашняя страница {% endblock %}
{% block style %}
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
box-sizing: border-box;
}
.jumbotron {
background-color: #f8f9fa;
padding: 20px;
margin: 20px;
border-radius: 5px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.jumbotron h1 {
color: #ff7077;
}
.jumbotron h2 {
color: #007bff;
}
.info-block {
margin: 10px;
padding: 10px;
background-color: #f1f1f1;
border-radius: 5px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.tile-container {
display: flex;
flex-wrap: nowrap; /* Ensure tiles stay in a single line */
}
.info-block.stable-version {
flex: 1; /* Distribute space equally among tiles */
}
{% endblock %}
{% block body %}
<div class="jumbotron text-center">
{% if session.logged_in %}
{% for stb in stables_version %}
<div class="tile-container">
<div class="info-block stable-version">
<h2>Мониторинг: {{ stb.monitoring }}</h2>
</div>
<div class="info-block stable-version">
<h2>Управление печатью: {{ stb.printmanager }}</h2>
</div>
</div>
{% endfor %}
<div class="info-block">
<h3>Количество виртуальных машин: {{ total_vm }}</h3>
</div>
<div class="info-block">
<h3>Количество занятых виртуальных машин: {{ number_of_employees }}</h3>
</div>
<div class="info-block">
<h3>Количество технических виртуальных машин: {{ number_of_technical }}</h3>
</div>
<div class="info-block">
<h3>Количество виртуальных машин доступных для тестов: {{ quantity_for_tests }}</h3>
</div>
{% else %}
<div class="info-block">
<h1>Для пользования данным сервисом необходимо пройти регистрацию и авторизацию!</h1>
</div>
{% for stb in stables_version %}
<div class="tile-container">
<div class="info-block stable-version">
<h2>Мониторинг: {{ stb.monitoring }}</h2>
</div>
<div class="info-block stable-version">
<h2>Управление печатью: {{ stb.printmanager }}</h2>
</div>
</div>
{% endfor %}
{% endif %}
</div>
{% endblock %}

View File

@ -0,0 +1,9 @@
{% macro render_field(field) %}
{{ field.label }}
{{ field(**kwargs)|safe }}
{% if field.errors %}
{% for error in field.errors %}
<span class="help-inline">{{ error }}</span>
{% endfor %}
{% endif %}
{% endmacro %}

View File

@ -0,0 +1,9 @@
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">
<p style="text-align: center; font-weight: bold;">{{ message }}</p>
</div>
{% endfor %}
{% endif %}
{% endwith %}

View File

@ -0,0 +1,38 @@
<nav class="navbar navbar-default">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
<span class="sr-only">Navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">
<img src="/static/image/logo.png" height="20">
</a>
</div>
<div id="navbar" class="collapse navbar-collapse">
<ul class="nav navbar-nav">
{% if session.logged_in %}
<li><a href="/">Home</a></li>
<li><a href="/dashboard">Дашборд</a></li>
<li><a href="/statistics">Статистика</a></li>
{% if session.is_admin %}
<li><a href="/admin">Админка</a></li>
{% endif %}
{% endif %}
</ul>
<ul class="nav navbar-nav navbar-right">
{% if session.logged_in %}
<li><a href="/update_user_info">Изменить информацию профиля</a></li>
<li><a href="/update_pass">Изменить пароль</a></li>
<li><a href="/logout">Выйти</a></li>
{% else %}
<li><a href="/login">Войти</a></li>
{% endif %}
</ul>
</div>
</div>
</nav>

28
templates/layout.html Normal file
View File

@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{% block tytle %}{% endblock%}</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
<style>
{% block style%}{% endblock%}
body {
background-image: url("/static/image/bg-01.png");
background-position: center;
}
.navbar {
background-color: #b6c7d3;
}
</style>
</head>
<body>
{% include 'includes/_navbar.html' %}
<div class="container">
{% include 'includes/_messages.html' %}
{% block body%}{% endblock%}
</div>
{% block scp %}{% endblock%}
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
</body>
</html>

60
templates/login.html Normal file
View File

@ -0,0 +1,60 @@
{% extends 'layout.html' %}
{% block title %} Авторизация {% endblock %}
{% block style %}
.login-form {
max-width: 400px;
margin: auto;
padding: 20px;
background-color: #f7f7f7;
border: 1px solid #e0e0e0;
border-radius: 5px;
box-shadow: 0px 2px 6px rgba(0, 0, 0, 0.1);
}
.login-form h2 {
text-align: center;
margin-bottom: 20px;
}
.login-form .form-group {
margin-bottom: 15px;
}
.login-form input[type="text"],
.login-form input[type="password"] {
width: 100%;
padding: 10px;
border: 1px solid #e0e0e0;
border-radius: 5px;
font-size: 1rem;
}
.login-form .btn-primary {
width: 100%;
padding: 10px;
font-size: 1rem;
background-color: #3498db;
border: none;
border-radius: 12px;
color: #ffffff;
cursor: pointer;
margin-bottom: 5px;
}
{% endblock %}
{% block body %}
<div class="login-form">
<h2>Войти</h2>
<form method="POST" action="{{ url_for('user.login') }}">
<div class="form-group">
<label for="username">Имя пользователя</label>
<input type="text" id="username" name="username" class="form-control" required>
</div>
<div class="form-group">
<label for="password">Пароль</label>
<input type="password" id="password" name="password" class="form-control" required>
</div>
<button type="submit" class="btn btn-primary">Войти</button>
<a href="/register" class="btn btn-primary">Регистрация</a>
</form>
</div>
{% endblock %}

154
templates/manage.html Normal file
View File

@ -0,0 +1,154 @@
{% extends 'layout.html' %}
{% block title %} Управление {% endblock %}
{% block style %}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 8px;
text-align: left;
border-bottom: 1px solid #ddd;
}
th {
background-color: #f2f2f2;
}
.add {
display: inline-block;
margin-right: 10px;
}
.action-buttons {
display: flex;
flex-direction: column;
}
.action-button {
padding: 8px 12px;
margin-bottom: 5px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.start {
background-color: #28a745; /* Зеленый цвет */
color: #fff;
}
.stop {
background-color: #dc3545; /* Красный цвет */
color: #fff;
}
.restart {
background-color: #007bff; /* Синий цвет */
color: #fff;
}
.search-container {
margin-bottom: 20px;
}
#searchInput {
width: 50%;
padding: 8px;
margin-bottom: 10px;
box-sizing: border-box;
border: 1px solid #ccc;
border-radius: 4px;
}
{% endblock %}
{% block body %}
<div class="search-container">
<input type="text" id="searchInput" onkeyup="searchTable()" placeholder="Поиск...">
</div>
<table>
<tr>
<th>#</th>
<th>Гипер</th>
<th>Адрес</th>
<th>Название</th>
<th>Операционная система</th>
<th>ОЗУ</th>
<th>ЦПУ</th>
<th>Состояние</th>
<th>Действия</th>
</tr>
{% for vm in virtual_machines %}
<tr>
<td>{{ loop.index }}</td>
<td>{{ vm.hyper }}</td>
<td>
{% if vm.ip_addres == None %}
Данных нет
{% else %}
{{ vm.ip_addres }}
{% endif %}
</td>
<td>{{ vm.name }}</td>
<td>{{ vm.os }}</td>
<td>
{% if vm.memory < 1024 %}
{{ vm.memory }} Мб
{% else %}
{{ (vm.memory // 1024) | int }} Гб
{% endif %}
</td>
<td>{{ vm.cpu }}</td>
<td>
{% if vm.power_status == 'poweredOn' %}
<img src="{{ url_for('static', filename='on.png') }}" width="20" height="20">
{% elif vm.power_status == 'poweredOff' %}
<img src="{{ url_for('static', filename='off.png') }}" width="20" height="20">
{% else %}
Не определено
{% endif %}
</td>
<td>
{% if vm.technical != True %}
{% if vm.power_status == 'poweredOn' %}
<div class="action-buttons">
<button class="action-button stop" onclick="confirmAction('{{ url_for('vm.stop_virtual_machine', name=vm.name, hyper=vm.hyper) }}', 'Вы уверены, что хотите остановить {{ vm.name }} {% if vm.ip_addres == None %} Не определено {% else %} {{ vm.ip_addres }} {% endif %}?')">Остановить</button>
<button class="action-button restart" onclick="confirmAction('{{ url_for('vm.restart_virtual_machine', name=vm.name, hyper=vm.hyper) }}', 'Вы уверены, что хотите перезапустить {{ vm.name }} {% if vm.ip_addres == None %} Не определено {% else %} {{ vm.ip_addres }} {% endif %}?')">Перезапустить</button>
</div>
{% elif vm.power_status == 'poweredOff' %}
<div class="action-buttons">
<button class="action-button start" onclick="confirmAction('{{ url_for('vm.start_virtual_machine', name=vm.name, hyper=vm.hyper) }}', 'Вы уверены, что хотите запустить {{ vm.name }} {% if vm.ip_addres == None %} Не определено {% else %} {{ vm.ip_addres }} {% endif %}?')">Запустить</button>
</div>
{% endif %}
{% endif %}
</td>
</tr>
{% endfor %}
</table>
<script>
function searchTable() {
var input, filter, table, tr, td, i, txtValue;
input = document.getElementById("searchInput");
filter = input.value.toUpperCase();
table = document.getElementsByTagName("table")[0];
tr = table.getElementsByTagName("tr");
for (i = 0; i < tr.length; i++) {
td = tr[i].getElementsByTagName("td");
for (var j = 0; j < td.length; j++) {
if (td[j]) {
txtValue = td[j].textContent || td[j].innerText;
if (txtValue.toUpperCase().indexOf(filter) > -1) {
tr[i].style.display = "";
break;
} else {
tr[i].style.display = "none";
}
}
}
}
}
function confirmAction(url, message) {
if (confirm(message)) {
window.location.href = url;
}
}
</script>
{% endblock %}

67
templates/register.html Normal file
View File

@ -0,0 +1,67 @@
{% extends 'layout.html' %}
{% block title %} Регистрация {% endblock %}
{% block style %}
.registration-form {
max-width: 400px;
margin: auto;
padding: 20px;
background-color: #f7f7f7;
border: 1px solid #e0e0e0;
border-radius: 5px;
box-shadow: 0px 2px 6px rgba(0, 0, 0, 0.1);
}
.registration-form h1 {
text-align: center;
margin-bottom: 20px;
}
.registration-form .form-group {
margin-bottom: 15px;
}
.registration-form input[type="text"],
.registration-form input[type="email"],
.registration-form input[type="password"] {
width: 100%;
padding: 10px;
border: 1px solid #e0e0e0;
border-radius: 5px;
font-size: 1rem;
}
.registration-form .btn-primary {
width: 100%;
padding: 10px;
font-size: 1rem;
background-color: #3498db;
border: none;
border-radius: 5px;
color: #ffffff;
cursor: pointer;
}
{% endblock %}
{% block body %}
<div class="registration-form">
<h1>Регистрация</h1>
{% from "includes/_formhelpers.html" import render_field%}
<form method="post" action="">
<div class="form-group">
{{ render_field(form.name, class_='form-control') }}
</div>
<div class="form-group">
{{ render_field(form.email, class_='form-control') }}
</div>
<div class="form-group">
{{ render_field(form.username, class_='form-control') }}
</div>
<div class="form-group">
{{ render_field(form.password, class_='form-control') }}
</div>
<div class="form-group">
{{ render_field(form.confirm, class_='form-control') }}
</div>
<p><input type="submit" class="btn btn-primary" value="Регистрация"></p>
</form>
</div>
{% endblock %}

View File

@ -0,0 +1,8 @@
{% extends 'layout.html' %}
{% block tytle %} Регистрация закрыта {% endblock%}
{% block body %}
<h4 class="text_off">Регистрация отключена! Обратитесь в тех. поддержку.</h4>
{% endblock %}

100
templates/statistics.html Normal file
View File

@ -0,0 +1,100 @@
{% extends 'layout.html' %}
{% block title %}Статистика{% endblock %}
{% block style %}
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
box-sizing: border-box;
}
.jumbotron {
background-color: #f8f9fa;
padding: 20px;
margin: 20px;
border-radius: 5px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.jumbotron h1 {
color: #ff7077;
}
.jumbotron h2 {
color: #007bff;
}
.info-block {
margin: 10px;
padding: 10px;
background-color: #f1f1f1;
border-radius: 5px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.tile-container {
display: flex;
flex-wrap: wrap; /* Allow tiles to wrap */
justify-content: center; /* Center tiles horizontally */
}
.info-block.stable-version {
flex: 1;
text-align: center;
}
.info-block h2 {
color: #333;
margin-bottom: 5px; /* Adjusted spacing */
}
.info-block h3 {
color: #555; /* Subdued color for secondary headings */
}
{% endblock %}
{% block body %}
<div class="tile-container">
<div class="info-block stable-version">
<h2 style="color: #1238d2; font-size: 24px;">Наиболее часто используемая виртуальная машина</h2>
{% if not data_vm %}
<p style="font-style: italic; color: #666;">Нет данных</p>
{% else %}
<div style="margin-top: 10px;">
<p style="font-weight: bold; color: #333;">Имя: <span style="color: #007bff;">{{ data_vm.name }}</span></p>
<p style="font-weight: bold; color: #333;">Гипервизор: <span style="color: #007bff;">{{ data_vm.hyper }}</span></p>
<p style="font-weight: bold; color: #333;">IP-адрес: <span style="color: #007bff;">{{ data_vm.ip_addres }}</span></p>
</div>
{% endif %}
</div>
</div>
<div class="tile-container">
<div class="info-block stable-version">
<h2 style="color: #1238d2; font-size: 24px;">Статистика по выполненным действиям</h2>
{% if not action_counts %}
<p style="font-style: italic; color: #666;">Нет данных</p>
{% else %}
<ul style="list-style-type: none; padding: 0;">
{% for action in action_counts %}
{% set action_name, action_count = action %}
<li style="margin-bottom: 10px;">
{% if action_name == 'add_vm' %}
<span style="color: #007bff; font-weight: bold;">Добавлено ВМ (вручную):</span> <span style="color: #007bff;">{{ action_count }}</span>
{% elif action_name == 'del_vm' %}
<span style="color: #007bff; font-weight: bold;">Удалено ВМ:</span> <span style="color: #007bff;">{{ action_count }}</span>
{% elif action_name == 'edit_vm' %}
<span style="color: #007bff; font-weight: bold;">Отредактированно ВМ:</span> <span style="color: #007bff;">{{ action_count }}</span>
{% elif action_name == 'occupy_vm' %}
<span style="color: #007bff; font-weight: bold;">Занято ВМ:</span> <span style="color: #007bff;">{{ action_count }}</span>
{% elif action_name == 'release_vm' %}
<span style="color: #007bff; font-weight: bold;">Освобождено ВМ:</span> <span style="color: #007bff;">{{ action_count }}</span>
{% elif action_name == 'start_vm' %}
<span style="color: #007bff; font-weight: bold;">Запущено ВМ:</span> <span style="color: #007bff;">{{ action_count }}</span>
{% elif action_name == 'stop_vm' %}
<span style="color: #007bff; font-weight: bold;">Остановлено ВМ:</span> <span style="color: #007bff;">{{ action_count }}</span>
{% elif action_name == 'restart_vm' %}
<span style="color: #007bff; font-weight: bold;">Перезапущено ВМ:</span> <span style="color: #007bff;">{{ action_count }}</span>
{% endif %}
</li>
{% endfor %}
</ul>
{% endif %}
</div>
</div>
{% endblock %}

116
templates/user_info.html Normal file
View File

@ -0,0 +1,116 @@
{% extends 'layout.html' %}
{% block title %} Информация о пользователе {% endblock %}
{% block style %}
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
box-sizing: border-box;
}
.jumbotron {
background-color: #f8f9fa;
padding: 20px;
margin: 20px;
border-radius: 5px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.jumbotron h1 {
color: #ff7077;
}
.jumbotron h2 {
color: #007bff;
}
.info-block {
margin: 10px;
padding: 10px;
background-color: #f1f1f1;
border-radius: 5px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.tile-container {
display: flex;
flex-wrap: nowrap; /* Ensure tiles stay in a single line */
}
.info-block.stable-version {
flex: 1; /* Distribute space equally among tiles */
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 8px;
text-align: left;
border-bottom: 1px solid #ddd;
}
th {
background-color: #f2f2f2;
color: #333;
}
{% endblock %}
{% block body %}
<table>
<thead>
<tr>
<th>ID</th>
<th>Тип действия</th>
<th>ВМ</th>
<th>Гипер</th>
<th>Информация</th>
<th>Время действия</th>
</tr>
</thead>
<tbody>
{% for user in user_data.items %}
<tr>
<td>{{ user[0].user_id }}</td>
{% if user[0].action_type == 'login_user' %}
<td>Авторизация</td>
{% elif user[0].action_type == 'release_vm' %}
<td>ВМ освобождена</td>
{% elif user[0].action_type == 'occupy_vm' %}
<td>ВМ занята</td>
{% elif user[0].action_type == 'del_vm' %}
<td>ВМ удалена</td>
{% elif user[0].action_type == 'edit_vm' %}
<td>ВМ отредактирована</td>
{% elif user[0].action_type == 'start_vm' %}
<td>ВМ запущена</td>
{% elif user[0].action_type == 'stop_vm' %}
<td>ВМ остановлена</td>
{% elif user[0].action_type == 'restart_vm' %}
<td>ВМ перезагружена</td>
{% endif %}
{% if user[0].action_type == 'login_user' %}
<td>х</td>
<td>х</td>
{% else %}
<td>{{ user[1] }}</td>
<td>{{ user[2] }}</td>
{% endif %}
{% if user[0].action_info == None %}
<td>х</td>
{% else %}
<td>{{ user[0].action_info }}</td>
{% endif %}
<td>{{ user[0].action_timestamp }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="pagination">
<span>Страницы:</span>
{% for page_num in user_data.iter_pages() %}
{% if page_num %}
<a href="{{ url_for('user.user_info', username=user_pg, page=page_num) }}">{{ page_num }}</a>
{% else %}
<span class="current">{{ page_num }}</span>
{% endif %}
{% endfor %}
</div>
{% endblock %}

105
templates/vm_info.html Normal file
View File

@ -0,0 +1,105 @@
{% extends 'layout.html' %}
{% block title %} Информация о машине {% endblock %}
{% block style %}
.vm-heading {
background-color: #3498db;
padding: 25px;
border-radius: 20px;
color: white;
text-align: center;
margin-bottom: 20px;
}
.vm-section {
padding: 20px;
border: 1px solid #e0e0e0;
border-radius: 20px;
box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.1);
background-color: #ffffff;
margin-bottom: 20px;
}
.vm-section h3 {
font-size: 1.5rem;
margin-bottom: 20px;
text-align: center;
color: #3498db;
text-transform: uppercase;
}
.vm-form-group {
margin-bottom: 20px;
}
.vm-label {
font-weight: bold;
}
.vm-input {
width: 100%;
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
}
.vm-submit-button {
background-color: #3498db;
border: none;
border-radius: 5px;
padding: 10px 20px;
color: #ffffff;
font-size: 1rem;
text-decoration: none;
transition: background-color 0.2s;
}
.vm-input-info {
width: 100%;
height: 200px;
resize: vertical;
}
.vm-status-image {
width: 30px;
vertical-align: middle;
}
{% endblock %}
{% block body %}
<div class="vm-heading">
{% if all_data_vm.technical == True %}
<h4>Данная виртуальная машина является технической</h4>
{% endif %}
<h1>{{ all_data_vm.name }}</h1>
</div>
<div class="vm-section">
<p><strong>IP-адрес:</strong> {% if all_data_vm.ip_addres == None %} Данных нет {% else %} {{ all_data_vm.ip_addres }} {% endif %}</p>
<p><strong>Гипервизор:</strong> {{ all_data_vm.hyper }}</p>
<p><strong>Операционная система:</strong> {{ all_data_vm.os }}</p>
<p><strong>Память:</strong>{% if all_data_vm.memory < 1024 %} {{ all_data_vm.memory }} МБ {% else %} {{ (all_data_vm.memory // 1024) | int }} ГБ {% endif %} </p>
<p><strong>CPU:</strong> {{ all_data_vm.cpu }}</p>
<p><strong>Статус питания:</strong>
{% if all_data_vm.power_status == 'poweredOn' %}
<img src="{{ url_for('static', filename='on.png') }}" width="20" height="20">
{% else %}
<img src="{{ url_for('static', filename='off.png') }}" width="20" height="20">
{% endif %}
</p>
{% if all_data_vm.technical != True %}
<p><strong>Статус:</strong> {% if all_data_vm.status == 'Занято' %} Виртуальная машина занята <strong>{{ all_data_vm.who_borrowed }}</strong> {% else %} Свободно {% endif %}</p>
{% endif %}
</div>
<div class="vm-section">
<form method="POST" action="{{ url_for('vm.vm_info', name=all_data_vm.name, hyper=all_data_vm.hyper) }}" id="vm-info-form">
<div class="vm-form-group">
<label for="information" class="vm-label">Добавить комментарий:</label>
<textarea id="information" class="vm-input-info" name="information" required>{{ information }}</textarea>
</div>
<button type="submit" class="vm-submit-button">Сохранить</button>
</form>
</div>
{% endblock %}

144
templates/vms.html Normal file
View File

@ -0,0 +1,144 @@
{% extends 'layout.html' %}
{% block title %} Редактирование {% endblock %}
{% block style %}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #ddd;
}
th {
background-color: #f2f2f2;
}
.add {
display: inline-block;
margin-right: 10px;
padding: 8px 12px;
background-color: #007bff;
color: #fff;
border-radius: 4px;
text-decoration: none;
}
.add:hover {
background-color: #4da3ff;
}
.action-button {
padding: 6px 10px;
background-color: #28a745;
color: #fff;
border: none;
border-radius: 4px;
cursor: pointer;
margin-right: 5px;
}
.action-button.delete {
background-color: #dc3545;
}
.search-container {
margin-bottom: 20px;
}
#searchInput {
width: 50%;
padding: 8px;
margin-bottom: 10px;
box-sizing: border-box;
border: 1px solid #ccc;
border-radius: 4px;
}
{% endblock %}
{% block body %}
<div class="search-container">
<input type="text" id="searchInput" onkeyup="searchTable()" placeholder="Поиск...">
</div>
<a href="{{ url_for('vm.add_virtual_machine') }}" class="add">Добавить ВМ</a>
<table id="vmTable">
<tr>
<th>#</th>
<th>Гипер</th>
<th>Адрес</th>
<th>Название</th>
<th>Операционная система</th>
<th>ОЗУ</th>
<th>ЦПУ</th>
<th>Состояние</th>
<th>Действия</th>
</tr>
{% for vm in virtual_machines %}
<tr>
<td>{{ loop.index }}</td>
<td>{{ vm.hyper }}</td>
<td>
{% if vm.ip_addres == None %}
Данных нет
{% else %}
{{ vm.ip_addres }}
{% endif %}
</td>
<td>{{ vm.name }}</td>
<td>{{ vm.os }}</td>
<td>
{% if vm.memory < 1024 %}
{{ vm.memory }} МБ
{% else %}
{{ (vm.memory // 1024) | int }} ГБ
{% endif %}
</td>
<td>{{ vm.cpu }}</td>
<td>
{% if vm.power_status == 'poweredOn' %}
<img src="{{ url_for('static', filename='on.png') }}" width="20" height="20">
{% elif vm.power_status == 'poweredOff' %}
<img src="{{ url_for('static', filename='off.png') }}" width="20" height="20">
{% else %}
Не определено
{% endif %}
</td>
<td>
<div style="display: flex; align-items: center;">
<button class="action-button" onclick="window.location.href='{{ url_for('vm.edit_virtual_machine', id=vm.id) }}'">Редактировать</button>
<button class="action-button delete" onclick="confirmDelete('{{ url_for('vm.delete_virtual_machine', id=vm.id) }}')">Удалить</button>
</div>
</td>
</tr>
{% endfor %}
</table>
<script>
function searchTable() {
var input, filter, table, tr, td, i, txtValue;
input = document.getElementById("searchInput");
filter = input.value.toUpperCase();
table = document.getElementById("vmTable");
tr = table.getElementsByTagName("tr");
for (i = 0; i < tr.length; i++) {
td = tr[i].getElementsByTagName("td");
for (var j = 0; j < td.length; j++) {
if (td[j]) {
txtValue = td[j].textContent || td[j].innerText;
if (txtValue.toUpperCase().indexOf(filter) > -1) {
tr[i].style.display = "";
break;
} else {
tr[i].style.display = "none";
}
}
}
}
}
function confirmDelete(url) {
if (confirm('Вы уверены, что хотите удалить данную ВМ?')) {
window.location.href = url;
}
}
</script>
{% endblock %}

32
upd-file.txt Normal file
View File

@ -0,0 +1,32 @@
routers/user_routers.py
routers/vm_routers.py
templates/includes/_formhelpers.html
templates/includes/_messages.html
templates/includes/_navbar.html
templates/about.html
templates/add.html
templates/dashboard.html
templates/edit.html
templates/edit_info.html
templates/edit_pass.html
templates/home.html
templates/layout.html
templates/login.html
templates/manage.html
templates/register.html
templates/register_off.html
templates/vms.html
templates/vm_info.html
templates/statistics.html
templates/admin.html
templates/user_info.html
.create
.init
app.py
db_manager.py
forms.py
vms.py
wsgi.py
requirements.txt
upd-file.txt
update.sh

83
update.sh Executable file
View File

@ -0,0 +1,83 @@
#!/bin/bash
RED='\033[0;31m'
GREEN='\033[0;32m'
NC='\033[0m'
if [ "$(id -u)" != "0" ]; then
echo -e "${RED}Этот скрипт должен быть запущен с правами суперпользователя.${NC}"
exit 1
fi
project_directory="/opt/dashboard"
update_directory="/tmp/update-dashboard"
log_directory="$project_directory/logs"
file_list="$update_directory/upd-file.txt"
# shellcheck disable=SC2164
cd $project_directory
source $project_directory/stop.sh
if [ ! -d "$log_directory" ]; then
mkdir -p "$log_directory"
fi
if [ "$1" == "-all" ]; then
all_updates=true
else
all_updates=false
fi
function update_files {
local current_dir="$1"
while IFS= read -r file; do
if [ -e "$current_dir/$file" ]; then
if [ -d "$current_dir/$file" ]; then
mkdir -p "$project_directory/${current_dir/$update_directory/}"
update_files "$current_dir/$file"
elif [ -f "$current_dir/$file" ]; then
project_file="$project_directory/${current_dir/$update_directory/}/$file"
project_file_dir=$(dirname "$project_file")
if [ ! -d "$project_file_dir" ]; then
mkdir -p "$project_file_dir"
fi
if [ -e "$project_file" ]; then
cp "$current_dir/$file" "$project_file" >> "$log_directory/update.log"
echo -e "${GREEN}Файл $file успешно обновлен.${NC}"
else
if [ "$all_updates" == true ]; then
cp "$current_dir/$file" "$project_file" >> "$log_directory/update.log"
echo -e "${GREEN}Файл $file добавлен в проект.${NC}"
else
read -p "Файл $file не найден в проекте. Хотите добавить его? (y/n): " choice
if [ "$choice" == "y" ]; then
cp "$current_dir/$file" "$project_file" >> "$log_directory/update.log"
echo -e "${GREEN}Файл $file добавлен в проект.${NC}"
else
echo -e "${RED}Файл $file пропущен.${NC}"
fi
fi
fi
fi
else
echo -e "${RED}Файл $file не существует.${NC}"
fi
done < "$file_list"
}
update_files "$update_directory"
sed -i 's/DISABLING_TASK=.*/DISABLING_TASK=True/g' .env
flask db migrate
flask db upgrade
sed -i 's/DISABLING_TASK=.*/DISABLING_TASK=False/g' .env
source $project_directory/start.sh 80
cd ~
echo -e "${GREEN}Обновление завершено.${NC}"

95
vms.py Normal file
View File

@ -0,0 +1,95 @@
from db_manager import db, VirtualMachine
from dotenv import load_dotenv
from pyVim import connect
from pyVmomi import vim
import logging
import ssl
import os
dotenv_path = os.path.join(os.path.dirname(__file__), '.env')
load_dotenv(dotenv_path)
def get_database_uri():
db_type = os.environ.get('DB_TYPE').lower()
if db_type == "sqlite":
return f"sqlite:///{os.environ.get('NAME_DB_SQLITE')}.db"
elif db_type == "postgresql":
return (f"postgresql://{os.environ.get('DB_USER')}:{os.environ.get('DB_PASS')}@"
f"{os.environ.get('DB_HOST')}:{os.environ.get('DB_PORT')}/"
f"{os.environ.get('DB_NAME')}")
elif db_type == "mysql":
return (f"mysql+mysqlconnector://{os.environ.get('DB_USER')}:"
f"{os.environ.get('DB_PASS')}@{os.environ.get('DB_HOST')}:"
f"{os.environ.get('DB_PORT')}/{os.environ.get('DB_NAME')}")
else:
logging.error('Неправильные настройки базы данных')
def disable_ssl_verification():
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS)
ssl_context.verify_mode = ssl.CERT_NONE
return ssl_context
def connect_to_vcenter(host, user, password):
try:
service_instance = connect.SmartConnect(host=host, user=user, pwd=password,
sslContext=disable_ssl_verification())
return service_instance.RetrieveContent()
except Exception as e:
logging.error(f"Ошибка подключения к {host}: {e}")
return None
def get_vm_info_and_save_to_db(content, hyper):
try:
container = content.viewManager.CreateContainerView(content.rootFolder, [vim.VirtualMachine], True)
vms = container.view
for vm in vms:
vm_config = vm.config
existing_vm = VirtualMachine.query.filter_by(id_vm=vm._moId, hyper=hyper).first()
if existing_vm:
existing_vm.name = vm.name
existing_vm.os = vm_config.guestFullName
existing_vm.memory = vm_config.hardware.memoryMB
existing_vm.cpu = vm_config.hardware.numCPU
existing_vm.power_status = vm.runtime.powerState
logging.info(f"Виртуальная машина {vm.name} обновлена.")
else:
new_vm = VirtualMachine(
hyper=hyper,
ip_addres=vm.summary.guest.ipAddress,
id_vm=vm._moId,
name=vm.name,
os=vm_config.guestFullName,
memory=vm_config.hardware.memoryMB,
cpu=vm_config.hardware.numCPU,
power_status=vm.runtime.powerState,
status='Свободно',
task='Свободно',
busy_date='Свободно'
)
logging.info(f"Новая виртуальная машина {vm.name} создана.")
db.session.add(new_vm)
db.session.commit()
except Exception as e:
logging.error(f"Ошибка при полном обновлении данных виртуальной машины: {e}")
def update_vm_power_status(content, hyper):
try:
container = content.viewManager.CreateContainerView(content.rootFolder, [vim.VirtualMachine], True)
vms = container.view
for vm in vms:
existing_vm = VirtualMachine.query.filter_by(id_vm=vm._moId, hyper=hyper).first()
if existing_vm:
existing_vm.power_status = vm.runtime.powerState
if existing_vm.ip_addres is None:
existing_vm.ip_addres = vm.summary.guest.ipAddress
db.session.commit()
except Exception as e:
logging.error(f"Ошибка при обновлении статусов виртуальной машине: {e}")

5
wsgi.py Normal file
View File

@ -0,0 +1,5 @@
from app import app, socketio
# Start WSGI
if __name__ == "__main__":
socketio.run(app, allow_unsafe_werkzeug=True)