Добавлен проект
11
.gitignore
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
venv
|
||||
.idea
|
||||
/flask_session/
|
||||
/instance/
|
||||
/migrations/
|
||||
/logs/
|
||||
/.init
|
||||
/.create
|
||||
/flask.pid
|
||||
/.env
|
||||
/test.py
|
155
README.md
Normal 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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
172
routers/user_routers.py
Normal 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
@ -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
@ -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
After Width: | Height: | Size: 1.4 KiB |
BIN
static/image/fav.ico
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
static/image/logo.png
Normal file
After Width: | Height: | Size: 7.4 KiB |
BIN
static/image/stop.png
Normal file
After Width: | Height: | Size: 99 KiB |
BIN
static/off.png
Normal file
After Width: | Height: | Size: 607 KiB |
BIN
static/on.png
Normal file
After Width: | Height: | Size: 602 KiB |
BIN
static/sotp.png
Normal file
After Width: | Height: | Size: 44 KiB |
18
stop.sh
Executable 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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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 %}
|
9
templates/includes/_formhelpers.html
Normal 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 %}
|
9
templates/includes/_messages.html
Normal 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 %}
|
38
templates/includes/_navbar.html
Normal 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
@ -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
@ -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
@ -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
@ -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 %}
|
8
templates/register_off.html
Normal 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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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}")
|
||||
|