518 lines
19 KiB
C++
518 lines
19 KiB
C++
#include "mainwindow.h"
|
|
#include "./ui_mainwindow.h"
|
|
#include "ovhapi.h"
|
|
#include <QJsonObject>
|
|
#include <qcorotimer.h>
|
|
#include <QDebug>
|
|
#include <QDesktopServices>
|
|
#include <QMessageBox>
|
|
#include <QTimer>
|
|
#include <QJsonDocument>
|
|
#include <QJsonArray>
|
|
#include <QProgressDialog>
|
|
#include <QWebEngineView>
|
|
#include <QSignalMapper>
|
|
#include <QSettings>
|
|
#include <QTemporaryFile>
|
|
#include <QDir>
|
|
#include <QChart>
|
|
#include <QChartView>
|
|
#include <QLineSeries>
|
|
#include <QDateTimeAxis>
|
|
#include <QValueAxis>
|
|
#include <QFileDialog>
|
|
#include <KArchive>
|
|
#include <KZip>
|
|
#include "servicesview.h"
|
|
#include "dedicatedservergroupwidget.h"
|
|
#include "dedicatedserverinfowidget.h"
|
|
#include "ovhwebauthentication.h"
|
|
#include "pendingtasks.h"
|
|
|
|
using namespace std::chrono_literals;
|
|
|
|
const auto ServerGroup = Qt::UserRole + 1;
|
|
const auto ServerName = Qt::UserRole + 2;
|
|
|
|
const auto ten_years = 87600h;
|
|
|
|
MainWindow::MainWindow(QWidget *parent)
|
|
: QMainWindow(parent)
|
|
, ui(new Ui::MainWindow)
|
|
{
|
|
ui->setupUi(this);
|
|
|
|
api = new OvhApi{"https://eu.api.ovh.com/1.0", this};
|
|
|
|
serverKvmMapper = new QSignalMapper(this);
|
|
connect(serverKvmMapper, &QSignalMapper::mappedString, this, &MainWindow::startKVMRequest);
|
|
|
|
QSettings settings;
|
|
QAction *loginAction = nullptr;
|
|
if (settings.childGroups().contains("logins")) {
|
|
settings.beginGroup("logins");
|
|
for (auto ck: settings.childKeys()) {
|
|
// Add a menu item...
|
|
loginAction = addLoginAction(ck, settings.value(ck).toString());
|
|
loginActions.append(loginAction);
|
|
}
|
|
}
|
|
if (loginActions.size() == 1) {
|
|
// singleShot so it jumps back to the main event loop
|
|
QTimer::singleShot(0, loginAction, &QAction::trigger);
|
|
}
|
|
}
|
|
|
|
QAction *MainWindow::addLoginAction(const QString &ck, const QString &display) {
|
|
auto loginAction = new QAction(this);
|
|
loginAction->setText(display);
|
|
loginAction->setData(ck);
|
|
QObject::connect(loginAction, &QAction::triggered, this, [this, loginAction]() {
|
|
login(loginAction->data().toString());
|
|
});
|
|
ui->menuLogin->addAction(loginAction);
|
|
return loginAction;
|
|
}
|
|
|
|
MainWindow::~MainWindow()
|
|
{
|
|
delete ui;
|
|
}
|
|
|
|
QCoro::Task<> MainWindow::login(const QString &ck) {
|
|
qDebug() << "cleaning";
|
|
// First step : clean current tabs and tree
|
|
ui->serverList->clear();
|
|
ui->tabWidget->clear();
|
|
topLevelNodes.clear();
|
|
// Now switch to the given ck
|
|
qDebug() << "ck";
|
|
api->setConsumerKey(ck);
|
|
|
|
// Check it works...
|
|
auto meCheck = (co_await api->get("/me")).object();
|
|
if (meCheck.keys().contains("class") && meCheck["class"].toString() == "Client::Forbidden" ) {
|
|
QMessageBox::warning(this, tr("Expired token"), tr("It seems this token is expired. Please login again to continue."));
|
|
co_return;
|
|
}
|
|
|
|
// And fetch all infos...
|
|
qDebug() << "fetch";
|
|
auto servers = co_await api->get("/dedicated/server");
|
|
qDebug() << servers;
|
|
auto serverList = servers.array();
|
|
for (auto server: serverList) {
|
|
QString serverName = server.toString();
|
|
/* singleShot again, so we will keep the main event loop busy
|
|
* this way, when a query is submitted, the next loadServerInfo is called
|
|
* and so on, thus making parallel queries to OVH API... */
|
|
QTimer::singleShot(0, [this, serverName]() -> QCoro::Task<> {
|
|
return loadServerInfo(serverName);
|
|
});
|
|
}
|
|
}
|
|
|
|
QCoro::Task<> MainWindow::loadServerInfo(QString server) {
|
|
qDebug() << "Loading for " << server;
|
|
auto serverInfo = (co_await api->get(QString("/dedicated/server/") + server, 1h)).object();
|
|
auto displayName = serverInfo["reverse"].toString();
|
|
if (displayName == server) {
|
|
qDebug() << serverInfo;
|
|
displayName = serverInfo["name"].toString();
|
|
}
|
|
auto serverDomain = displayName.mid(displayName.indexOf('.') + 1);
|
|
QTreeWidgetItem *topDomain = nullptr;
|
|
if (!topLevelNodes.contains(serverDomain)) {
|
|
topDomain = new QTreeWidgetItem(ui->serverList);
|
|
topDomain->setText(0, serverDomain);
|
|
topDomain->setData(0, ServerGroup, serverDomain);
|
|
ui->serverList->addTopLevelItem(topDomain);
|
|
topLevelNodes.insert(serverDomain, topDomain);
|
|
} else {
|
|
topDomain = topLevelNodes[serverDomain];
|
|
}
|
|
auto serverItem = new QTreeWidgetItem(topDomain);
|
|
serverItem->setText(0, serverInfo["reverse"].toString());
|
|
serverItem->setData(0, ServerName, server);
|
|
topDomain->addChild(serverItem);
|
|
ui->serverList->sortItems(0, Qt::AscendingOrder);
|
|
}
|
|
|
|
void MainWindow::on_actionQuit_triggered()
|
|
{
|
|
qApp->exit();
|
|
}
|
|
|
|
|
|
QCoro::Task<> MainWindow::on_actionLogin_triggered()
|
|
{
|
|
auto ovh = new OvhApi{"https://eu.api.ovh.com/1.0", this};
|
|
auto [key, url] = co_await ovh->requestCredentials(QUrl("http://localhost/"));
|
|
qDebug() << "Got key " << key;
|
|
qDebug() << "Goto " << url;
|
|
QDesktopServices::openUrl(url);
|
|
auto answer = QMessageBox::question(this, tr("Validate credential"), tr("Have you validated the OVH credential request?"));
|
|
if (answer == QMessageBox::Yes) {
|
|
// 0) fetch consumer info...
|
|
ovh->setConsumerKey(key);
|
|
auto meInfo = (co_await ovh->get("/me/")).object();
|
|
qDebug() << "/me gives " << meInfo;
|
|
|
|
// 1) Store credential
|
|
QSettings settings;
|
|
settings.beginGroup("logins");
|
|
auto displayName = QString("%1 %2 (%3)")
|
|
.arg(meInfo["firstname"].toString())
|
|
.arg(meInfo["name"].toString())
|
|
.arg(meInfo["nichandle"].toString());
|
|
|
|
settings.setValue(key, displayName);
|
|
// 2) use it...
|
|
addLoginAction(key, displayName);
|
|
login(key);
|
|
} else {
|
|
qDebug() << "User answered " << answer << "and not" << QMessageBox::Yes;
|
|
}
|
|
ovh->deleteLater();
|
|
}
|
|
|
|
|
|
QCoro::Task<> MainWindow::on_serverList_itemClicked(QTreeWidgetItem *item, [[maybe_unused]] int column)
|
|
{
|
|
auto serverName = item->data(0, ServerName).toString();
|
|
if (serverName.isEmpty()) {
|
|
// Maybe it's a server group
|
|
auto groupName = item->data(0, ServerGroup).toString();
|
|
if (groupName.isEmpty())
|
|
co_return;
|
|
|
|
// Find existing tab
|
|
for (int i = 0 ; i < ui->tabWidget->count() ; i++) {
|
|
auto existingTab = ui->tabWidget->widget(i);
|
|
auto existingGroupInfo = qobject_cast<DedicatedServerGroupWidget*>(existingTab);
|
|
if (existingGroupInfo && existingGroupInfo->groupName() == groupName) {
|
|
ui->tabWidget->setCurrentIndex(i);
|
|
co_return;
|
|
}
|
|
}
|
|
|
|
// Build and fill a server group widget
|
|
QStringList serverNames;
|
|
for (int i = 0 ; i < item->childCount() ; i++) {
|
|
auto node = item->child(i);
|
|
serverName = node->data(0, ServerName).toString();
|
|
if (!serverName.isEmpty())
|
|
serverNames.append(serverName);
|
|
}
|
|
qDebug() << serverNames;
|
|
|
|
auto groupView = new DedicatedServerGroupWidget(api, groupName, serverNames, this);
|
|
int tabId = ui->tabWidget->addTab(groupView, tr("Group %1").arg(groupName));
|
|
ui->tabWidget->setCurrentIndex(tabId);
|
|
|
|
co_return;
|
|
}
|
|
auto serverReverse = item->text(0);
|
|
|
|
for (int i = 0 ; i < ui->tabWidget->count() ; i++) {
|
|
auto existingTab = ui->tabWidget->widget(i);
|
|
auto existingServerInfo = qobject_cast<DedicatedServerInfoWidget*>(existingTab);
|
|
if (existingServerInfo && existingServerInfo->serverName() == serverName) {
|
|
ui->tabWidget->setCurrentIndex(i);
|
|
co_return;
|
|
}
|
|
}
|
|
auto serverInfoView = new DedicatedServerInfoWidget(api, serverName, this);
|
|
int tabId = ui->tabWidget->addTab(serverInfoView, tr("Info %1").arg(serverReverse));
|
|
ui->tabWidget->setCurrentIndex(tabId);
|
|
serverKvmMapper->setMapping(serverInfoView, serverName);
|
|
|
|
connect(serverInfoView, &DedicatedServerInfoWidget::kvmRequested, serverKvmMapper, qOverload<>(&QSignalMapper::map));
|
|
}
|
|
|
|
QCoro::Task<> MainWindow::startKVMRequest(const QString &serverName)
|
|
{
|
|
auto serverInfo = (co_await api->get(QString("/dedicated/server/%1").arg(serverName), 1h)).object();
|
|
auto serverReverse = serverInfo["reverse"].toString();
|
|
|
|
auto ipmiInfo = (co_await api->get(QString("/dedicated/server/%1/features/ipmi").arg(serverName), 24h)).object();
|
|
qDebug() << ipmiInfo;
|
|
if (!ipmiInfo["activated"].toBool()) {
|
|
QMessageBox::warning(this, tr("IPMI not activated"), tr("IPMI is not activated on this server"));
|
|
co_return;
|
|
}
|
|
QString kvmType;
|
|
if (ipmiInfo["supportedFeatures"].toObject()["kvmipHtml5URL"].toBool())
|
|
kvmType = "Html5URL";
|
|
else if (ipmiInfo["supportedFeatures"].toObject()["kvmipJnlp"].toBool())
|
|
kvmType = "Jnlp";
|
|
else {
|
|
QMessageBox::warning(this, tr("IPMI KVM not available"), tr("IPMI KVM is not available on this server"));
|
|
co_return;
|
|
}
|
|
auto ipmiAccess = (co_await api->get(QString("/dedicated/server/%1/features/ipmi/access?type=kvmip%2").arg(serverName).arg(kvmType))).object();
|
|
qDebug() << ipmiAccess;
|
|
if (ipmiAccess.contains("value")) {
|
|
qDebug() << "Found a valid ipmi, go to " << ipmiAccess["value"].toString();
|
|
loadKvm(serverReverse, kvmType, ipmiAccess["value"].toString());
|
|
} else {
|
|
auto kvmAccessRequestData = QJsonObject{
|
|
{"ttl", 15}, // 15 minutes, the biggest available value in the API... SIGH
|
|
{"type", "kvmip" + kvmType}
|
|
};
|
|
auto kvmAccess = (co_await api->post(QString("/dedicated/server/%1/features/ipmi/access").arg(serverName), QJsonDocument(kvmAccessRequestData).toJson()));
|
|
qDebug() << "Initial task:" << kvmAccess; // This is a silly task object, have to wait on it... because the api will not wait for us, ho no, the heresy...
|
|
int kvmTaskId = kvmAccess["taskId"].toInt();
|
|
qDebug() << "Task is " << kvmTaskId;
|
|
|
|
auto taskView = new PendingTasks(api, QString{"/dedicated/server/%1/task"}.arg(serverName), this);
|
|
taskView->setWindowTitle(tr("Tasks for server %1").arg(serverReverse));
|
|
taskView->showNormal();
|
|
|
|
auto taskUrlString = QString{"/dedicated/server/%1/task/%2"}.arg(serverName).arg(kvmTaskId);
|
|
int progress = 1;
|
|
while (kvmAccess["status"].toString() != "done") {
|
|
// TODO: check for errors !
|
|
QTimer timer;
|
|
timer.start(1s);
|
|
co_await timer;
|
|
qDebug() << "Going to request an update on the task...";
|
|
kvmAccess = (co_await api->get(taskUrlString));
|
|
qDebug() << "task progress:" << kvmAccess;
|
|
progress++;
|
|
}
|
|
|
|
// Now we can get the url from ipmi...
|
|
ipmiAccess = (co_await api->get(QString("/dedicated/server/%1/features/ipmi/access?type=kvmip%2").arg(serverName).arg(kvmType))).object();
|
|
qDebug() << ipmiAccess;
|
|
loadKvm(serverReverse, kvmType, ipmiAccess["value"].toString());
|
|
}
|
|
}
|
|
|
|
void MainWindow::loadKvm(const QString &name, const QString &kvmType, const QString &data) {
|
|
qDebug() << "Loading from " << data;
|
|
if (kvmType == "Html5URL") {
|
|
auto webView = new QWebEngineView(ui->tabWidget);
|
|
webView->setUrl(QUrl{data});
|
|
int tabId = ui->tabWidget->addTab(webView, tr("KVM %1").arg(name));
|
|
ui->tabWidget->setCurrentIndex(tabId);
|
|
} else {
|
|
// Save the JNLP to a temporary file
|
|
QTemporaryFile jnlpFile(QString("%1%2%3-XXXXXXX.jnlp").arg(QDir::tempPath()).arg(QDir::separator()).arg(name));
|
|
jnlpFile.open();
|
|
jnlpFile.write(data.toUtf8());
|
|
jnlpFile.close();
|
|
if (QDesktopServices::openUrl(QUrl::fromLocalFile(jnlpFile.fileName()))) {
|
|
QMessageBox::information(this, tr("JNLP KVM"), tr("This server only supports a Java WebStart KVM, it will thus not be embedded in this window.\nJava WebStart should be starting right now, wait until it finishes loading to close this dialog."));
|
|
} else {
|
|
QMessageBox::warning(this, tr("JNLP KVM"), tr("Failed to launch javaws. Make sure it is properly installed."));
|
|
}
|
|
}
|
|
}
|
|
|
|
void MainWindow::on_tabWidget_tabCloseRequested(int index)
|
|
{
|
|
ui->tabWidget->widget(index)->deleteLater();
|
|
}
|
|
|
|
|
|
QCoro::Task<> MainWindow::on_actionGraphPerMonth_triggered()
|
|
{
|
|
auto progressDialog = new QProgressDialog(this);
|
|
progressDialog->setMaximum(-1);
|
|
progressDialog->setMinimum(-1);
|
|
progressDialog->show();
|
|
|
|
// Now fetch the bill list
|
|
QDate firstDayOfMonth = QDate::currentDate();
|
|
firstDayOfMonth = firstDayOfMonth.addDays(1 - firstDayOfMonth.day());
|
|
QDate oneYearAgo = firstDayOfMonth.addYears(-3);
|
|
|
|
QString path = QString("/me/bill?date.to=%1&date.from=%2").arg(firstDayOfMonth.toString(Qt::ISODate))
|
|
.arg(oneYearAgo.toString(Qt::ISODate));
|
|
double totalCost = 0;
|
|
QString currency;
|
|
auto billList = (co_await api->get(path, ten_years)).array();
|
|
|
|
// Ok, we can set the progress based on that list
|
|
progressDialog->setMinimum(0);
|
|
progressDialog->setMaximum(billList.size());
|
|
progressDialog->setValue(0);
|
|
QMap<QDate, double> amountPerMonth;
|
|
for (const auto &&bill: billList) {
|
|
auto billId = bill.toString();
|
|
// Get the bill info
|
|
auto billInfo = (co_await api->get(QString("/me/bill/%1").arg(billId), ten_years)).object();
|
|
|
|
QDate billDate = QDate::fromString(billInfo["date"].toString(), Qt::ISODate);
|
|
billDate = billDate.addDays(16 - billDate.day()); // Set to middle of the month, nicer graph
|
|
auto billPrice = billInfo["priceWithTax"].toObject();
|
|
|
|
if (!amountPerMonth.contains(billDate))
|
|
amountPerMonth.insert(billDate, 0);
|
|
|
|
|
|
if (billPrice["currencyCode"].toString() != "EUR")
|
|
qDebug() << "Invalid currency " << billPrice;
|
|
else
|
|
amountPerMonth[billDate] += billPrice["value"].toDouble();
|
|
|
|
qDebug() << billId << billDate << billPrice;
|
|
|
|
// update progress dialog...
|
|
progressDialog->setValue(progressDialog->value() + 1);
|
|
}
|
|
qDebug() << amountPerMonth;
|
|
|
|
// Build graph data
|
|
auto graphData = new QtCharts::QLineSeries(this);
|
|
auto i = amountPerMonth.constBegin();
|
|
while (i != amountPerMonth.constEnd()) {
|
|
graphData->append(i.key().startOfDay().toMSecsSinceEpoch(), i.value());
|
|
++i;
|
|
}
|
|
|
|
// Now we can display it
|
|
auto chart = new QtCharts::QChart();
|
|
chart->addSeries(graphData);
|
|
chart->legend()->hide();
|
|
chart->setTitle("Bills per month");
|
|
|
|
auto axisX = new QtCharts::QDateTimeAxis;
|
|
axisX->setTickCount(10);
|
|
axisX->setFormat("MMM yyyy");
|
|
axisX->setTitleText("Date");
|
|
chart->addAxis(axisX, Qt::AlignBottom);
|
|
graphData->attachAxis(axisX);
|
|
|
|
auto axisY = new QtCharts::QValueAxis;
|
|
axisY->setLabelFormat("%i");
|
|
axisY->setTitleText("Amount per month");
|
|
chart->addAxis(axisY, Qt::AlignLeft);
|
|
graphData->attachAxis(axisY);
|
|
|
|
auto chartView = new QtCharts::QChartView(chart);
|
|
chartView->setRenderHint(QPainter::Antialiasing);
|
|
chartView->resize(800, 600);
|
|
chartView->show();
|
|
|
|
progressDialog->close();
|
|
progressDialog->deleteLater();
|
|
}
|
|
|
|
|
|
QCoro::Task<> MainWindow::on_actionBillsOfLastMonth_triggered()
|
|
{
|
|
// todo filters
|
|
auto fileName = QFileDialog::getSaveFileName(this, tr("Target archive"), QString(), tr("Zip files (*.zip)"));
|
|
if (fileName.isEmpty())
|
|
co_return;
|
|
|
|
QMessageBox::information(this, tr("OVH..."), tr("OVH decided that getting a bill require a 'true' login. I'm going to show you a web browser for that."));
|
|
auto ovhAuth = new OvhWebAuthentication(this);
|
|
if (ovhAuth->exec() != QDialog::Accepted) {
|
|
qDebug() << "Rejected login, ok, bye bye";
|
|
co_return;
|
|
}
|
|
|
|
auto start = QDateTime::currentMSecsSinceEpoch();
|
|
|
|
KZip targetArchive{fileName};
|
|
if (!targetArchive.open(QIODevice::WriteOnly)) {
|
|
QMessageBox::warning(this, tr("Failed to open archive"), tr("Failed to open archive..."));
|
|
co_return;
|
|
}
|
|
|
|
auto progressDialog = new QProgressDialog(this);
|
|
progressDialog->setMaximum(-1);
|
|
progressDialog->setMinimum(-1);
|
|
progressDialog->show();
|
|
|
|
// Now fetch the bill list
|
|
QDate firstDayOfMonth = QDate::currentDate();
|
|
firstDayOfMonth = firstDayOfMonth.addDays(1 - firstDayOfMonth.day());
|
|
QDate oneMonthAgo = firstDayOfMonth.addMonths(-1);
|
|
|
|
QString path = QString("/me/bill?date.to=%1&date.from=%2").arg(firstDayOfMonth.toString(Qt::ISODate))
|
|
.arg(oneMonthAgo.toString(Qt::ISODate));
|
|
auto billList = (co_await api->get(path, ten_years)).array();
|
|
|
|
// Ok, we can set the progress based on that list
|
|
progressDialog->setMinimum(0);
|
|
progressDialog->setMaximum(billList.size());
|
|
progressDialog->setValue(0);
|
|
for (const auto &&bill: billList) {
|
|
auto billId = bill.toString();
|
|
// Get the bill info
|
|
auto billInfo = (co_await api->get(QString("/me/bill/%1").arg(billId), ten_years)).object();
|
|
QString billPdfUrl = billInfo["pdfUrl"].toString();
|
|
|
|
auto billTime = QDateTime::fromString(billInfo["date"].toString(), Qt::ISODate);
|
|
|
|
qDebug() << "Downloading bill at " << billPdfUrl;
|
|
QByteArray pdfData = co_await ovhAuth->download(QUrl{billPdfUrl});
|
|
targetArchive.writeFile(QString("%1.pdf").arg(billId), pdfData, 0100644, QString(), QString(), billTime, billTime, billTime);
|
|
|
|
// update progress dialog...
|
|
progressDialog->setValue(progressDialog->value() + 1);
|
|
}
|
|
|
|
targetArchive.close();
|
|
|
|
progressDialog->close();
|
|
progressDialog->deleteLater();
|
|
|
|
auto end = QDateTime::currentMSecsSinceEpoch();
|
|
qDebug() << "Took ..." << end - start;
|
|
|
|
ovhAuth->deleteLater();
|
|
QDesktopServices::openUrl(QUrl::fromLocalFile(fileName));
|
|
}
|
|
|
|
|
|
QCoro::Task<> MainWindow::on_actionFidelity_triggered()
|
|
{
|
|
QString path = QString("/me/fidelityAccount/movements?date.from=2012-11-21");
|
|
auto movementList = (co_await api->get(path)).array();
|
|
for (const auto &&movementId: movementList) {
|
|
QString movementPath = QString("/me/fidelityAccount/movements/%1").arg(movementId.toInt());
|
|
auto movement = (co_await api->get(movementPath, ten_years)).object();
|
|
qDebug() << movement;
|
|
}
|
|
}
|
|
|
|
|
|
void MainWindow::on_actionServices_triggered()
|
|
{
|
|
auto view = new ServicesView(api, this);
|
|
view->exec();
|
|
}
|
|
|
|
QCoro::Task<> MainWindow::on_actionPurgeLogins_triggered()
|
|
{
|
|
QList<QAction *> validLoginActions;
|
|
auto ovh = new OvhApi{"https://eu.api.ovh.com/1.0", this};
|
|
for (auto loginAction: loginActions) {
|
|
qDebug() << "checking for " << loginAction;
|
|
ovh->setConsumerKey(loginAction->data().toString());
|
|
auto meForCk = co_await ovh->get("/me");
|
|
qDebug() << meForCk;
|
|
QJsonObject me = meForCk.object();
|
|
if (me.keys().size() == 2 && me.keys().contains("class") && me["class"].toString() == "Client::Forbidden" ) {
|
|
// This is an invalid token
|
|
// Delete from settings
|
|
QSettings settings;
|
|
settings.beginGroup("logins");
|
|
settings.remove(loginAction->data().toString());
|
|
// Delete action, will purge menu entry and so on
|
|
loginAction->deleteLater();
|
|
} else {
|
|
validLoginActions.append(loginAction);
|
|
}
|
|
}
|
|
// TODO : purge cache db
|
|
QMessageBox::information(this, tr("Logins cleaned up"), tr("%n login(s) purged", nullptr, loginActions.size() - validLoginActions.size()));
|
|
loginActions = validLoginActions;
|
|
}
|
|
|