OvhKvm/mainwindow.cpp

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;
}