improved delete installation logic with S3 bucket purge and delete and reminder for manual check
This commit is contained in:
parent
584abe5b53
commit
f82190afc1
|
|
@ -264,8 +264,6 @@ public static class ExoCmd
|
|||
|
||||
public static async Task<Boolean> RevokeReadKey(this Installation installation)
|
||||
{
|
||||
//Check exoscale documentation https://openapi-v2.exoscale.com/topic/topic-api-request-signature
|
||||
|
||||
var url = $"https://api-ch-dk-2.exoscale.com/v2/access-key/{installation.S3Key}";
|
||||
var method = $"access-key/{installation.S3Key}";
|
||||
|
||||
|
|
@ -278,14 +276,28 @@ public static class ExoCmd
|
|||
client.DefaultRequestHeaders.Authorization =
|
||||
new AuthenticationHeaderValue("EXO2-HMAC-SHA256", authheader);
|
||||
|
||||
var response = await client.DeleteAsync(url);
|
||||
return response.IsSuccessStatusCode;
|
||||
try
|
||||
{
|
||||
var response = await client.DeleteAsync(url);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
Console.WriteLine($"Successfully revoked read key for installation {installation.Id}.");
|
||||
return true;
|
||||
}
|
||||
|
||||
Console.WriteLine($"Failed to revoke read key. HTTP Status: {response.StatusCode}. Response: {await response.Content.ReadAsStringAsync()}");
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error occurred while revoking read key: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<Boolean> RevokeWriteKey(this Installation installation)
|
||||
{
|
||||
//Check exoscale documentation https://openapi-v2.exoscale.com/topic/topic-api-request-signature
|
||||
|
||||
var url = $"https://api-ch-dk-2.exoscale.com/v2/access-key/{installation.S3WriteKey}";
|
||||
var method = $"access-key/{installation.S3WriteKey}";
|
||||
|
||||
|
|
@ -298,8 +310,24 @@ public static class ExoCmd
|
|||
client.DefaultRequestHeaders.Authorization =
|
||||
new AuthenticationHeaderValue("EXO2-HMAC-SHA256", authheader);
|
||||
|
||||
var response = await client.DeleteAsync(url);
|
||||
return response.IsSuccessStatusCode;
|
||||
try
|
||||
{
|
||||
var response = await client.DeleteAsync(url);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
Console.WriteLine($"Successfully revoked write key for installation {installation.Id}.");
|
||||
return true;
|
||||
}
|
||||
|
||||
Console.WriteLine($"Failed to revoke write key. HTTP Status: {response.StatusCode}. Response: {await response.Content.ReadAsStringAsync()}");
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error occurred while revoking write key: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<(String, String)> CreateWriteKey(this Installation installation)
|
||||
|
|
@ -373,10 +401,35 @@ public static class ExoCmd
|
|||
return await s3Region.PutBucket(installation.BucketName()) != null;
|
||||
}
|
||||
|
||||
public static async Task<Boolean> DeleteBucket(this Installation installation)
|
||||
public static async Task<Boolean> PurgeAndDeleteBucket(this Installation installation)
|
||||
{
|
||||
var s3Region = new S3Region($"https://{installation.S3Region}.{installation.S3Provider}", S3Credentials!);
|
||||
return await s3Region.DeleteBucket(installation.BucketName()) ;
|
||||
var bucket = s3Region.Bucket(installation.BucketName());
|
||||
|
||||
try
|
||||
{
|
||||
var purged = await bucket.PurgeBucket();
|
||||
if (!purged)
|
||||
{
|
||||
Console.WriteLine($"Failed to purge bucket {installation.BucketName()} for installation {installation.Id}.");
|
||||
return false;
|
||||
}
|
||||
|
||||
var deleted = await s3Region.DeleteBucket(installation.BucketName());
|
||||
if (!deleted)
|
||||
{
|
||||
Console.WriteLine($"Failed to delete bucket {installation.BucketName()} for installation {installation.Id}.");
|
||||
return false;
|
||||
}
|
||||
|
||||
Console.WriteLine($"Successfully purged and deleted bucket {installation.BucketName()} for installation {installation.Id}.");
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error occurred while purging/deleting bucket {installation.BucketName()}: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -324,23 +324,22 @@ public static class SessionMethods
|
|||
{
|
||||
var user = session?.User;
|
||||
|
||||
if (user is not null
|
||||
&& installation is not null
|
||||
&& user.UserType != 0)
|
||||
{
|
||||
if (user is null || installation is null || user.UserType == 0)
|
||||
return false;
|
||||
|
||||
return
|
||||
Db.Delete(installation)
|
||||
&& await installation.RevokeReadKey()
|
||||
&& await installation.RevokeWriteKey()
|
||||
&& await installation.RemoveReadRole()
|
||||
&& await installation.RemoveWriteRole()
|
||||
&& await installation.DeleteBucket();
|
||||
// Try all Exoscale operations independently (don't short-circuit)
|
||||
var readKeyOk = await installation.RevokeReadKey();
|
||||
var writeKeyOk = await installation.RevokeWriteKey();
|
||||
var readRoleOk = await installation.RemoveReadRole();
|
||||
var writeRoleOk = await installation.RemoveWriteRole();
|
||||
var bucketOk = await installation.PurgeAndDeleteBucket();
|
||||
|
||||
}
|
||||
|
||||
return false;
|
||||
if (!readKeyOk || !writeKeyOk || !readRoleOk || !writeRoleOk || !bucketOk)
|
||||
Console.WriteLine($"[Delete] Partial Exoscale cleanup for installation {installation.Id}: " +
|
||||
$"readKey={readKeyOk}, writeKey={writeKeyOk}, readRole={readRoleOk}, writeRole={writeRoleOk}, bucket={bucketOk}");
|
||||
|
||||
// Always delete from DB (best-effort — admin wants it gone)
|
||||
return Db.Delete(installation);
|
||||
}
|
||||
|
||||
public static Boolean Create(this Session? session, User newUser)
|
||||
|
|
|
|||
|
|
@ -102,10 +102,28 @@ public static partial class Db
|
|||
InstallationAccess.Delete(i => i.InstallationId == installation.Id);
|
||||
if (installation.Product == (int)ProductType.Salimax)
|
||||
{
|
||||
//For Salimax, delete the OrderNumber2Installation entries associated with this installation id.
|
||||
OrderNumber2Installation.Delete(i => i.InstallationId == installation.Id);
|
||||
}
|
||||
|
||||
// Clean up AI insight cache entries linked to this installation's reports
|
||||
var weeklyIds = WeeklyReports .Where(r => r.InstallationId == installation.Id).Select(r => r.Id).ToList();
|
||||
var monthlyIds = MonthlyReports.Where(r => r.InstallationId == installation.Id).Select(r => r.Id).ToList();
|
||||
var yearlyIds = YearlyReports .Where(r => r.InstallationId == installation.Id).Select(r => r.Id).ToList();
|
||||
|
||||
foreach (var id in weeklyIds) AiInsightCaches.Delete(c => c.ReportType == "weekly" && c.ReportId == id);
|
||||
foreach (var id in monthlyIds) AiInsightCaches.Delete(c => c.ReportType == "monthly" && c.ReportId == id);
|
||||
foreach (var id in yearlyIds) AiInsightCaches.Delete(c => c.ReportType == "yearly" && c.ReportId == id);
|
||||
|
||||
// Clean up energy records, report summaries, errors, warnings, and user actions
|
||||
DailyRecords .Delete(r => r.InstallationId == installation.Id);
|
||||
HourlyRecords .Delete(r => r.InstallationId == installation.Id);
|
||||
WeeklyReports .Delete(r => r.InstallationId == installation.Id);
|
||||
MonthlyReports.Delete(r => r.InstallationId == installation.Id);
|
||||
YearlyReports .Delete(r => r.InstallationId == installation.Id);
|
||||
Errors .Delete(e => e.InstallationId == installation.Id);
|
||||
Warnings .Delete(w => w.InstallationId == installation.Id);
|
||||
UserActions .Delete(a => a.InstallationId == installation.Id);
|
||||
|
||||
return Installations.Delete(i => i.Id == installation.Id) > 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -201,6 +201,48 @@ public static class S3
|
|||
return response.HttpStatusCode == HttpStatusCode.OK;
|
||||
}
|
||||
|
||||
public static async Task<Boolean> PurgeBucket(this S3Bucket bucket)
|
||||
{
|
||||
var client = bucket.Region.GetS3Client();
|
||||
var totalDeleted = 0;
|
||||
|
||||
Console.WriteLine($"[PurgeBucket] Starting purge of bucket {bucket.Name}");
|
||||
|
||||
while (true)
|
||||
{
|
||||
var listResponse = await client.ListObjectsV2Async(new ListObjectsV2Request
|
||||
{
|
||||
BucketName = bucket.Name,
|
||||
MaxKeys = 1000
|
||||
});
|
||||
|
||||
if (listResponse.S3Objects.Count == 0)
|
||||
{
|
||||
Console.WriteLine($"[PurgeBucket] Completed purge of bucket {bucket.Name}: {totalDeleted} objects deleted");
|
||||
return true;
|
||||
}
|
||||
|
||||
var deleteResponse = await client.DeleteObjectsAsync(new DeleteObjectsRequest
|
||||
{
|
||||
BucketName = bucket.Name,
|
||||
Objects = listResponse.S3Objects
|
||||
.Select(o => new KeyVersion { Key = o.Key })
|
||||
.ToList()
|
||||
});
|
||||
|
||||
if (deleteResponse.HttpStatusCode != HttpStatusCode.OK)
|
||||
{
|
||||
Console.WriteLine($"[PurgeBucket] Failed at batch after {totalDeleted} objects deleted from bucket {bucket.Name}");
|
||||
return false;
|
||||
}
|
||||
|
||||
totalDeleted += listResponse.S3Objects.Count;
|
||||
|
||||
if (totalDeleted % 10000 == 0)
|
||||
Console.WriteLine($"[PurgeBucket] Progress: {totalDeleted} objects deleted from bucket {bucket.Name}");
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<Boolean> DeleteBucket(this S3Region region, String bucketName)
|
||||
{
|
||||
var request = new DeleteBucketRequest { BucketName = bucketName };
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@ function Information(props: InformationProps) {
|
|||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: 350,
|
||||
width: 450,
|
||||
bgcolor: 'background.paper',
|
||||
borderRadius: 4,
|
||||
boxShadow: 24,
|
||||
|
|
@ -143,7 +143,37 @@ function Information(props: InformationProps) {
|
|||
gutterBottom
|
||||
sx={{ fontWeight: 'bold' }}
|
||||
>
|
||||
Do you want to delete this installation?
|
||||
<FormattedMessage
|
||||
id="confirmDeleteInstallation"
|
||||
defaultMessage="Do you want to delete this installation?"
|
||||
/>
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
mt: 1,
|
||||
p: 1,
|
||||
bgcolor: '#f5f5f5',
|
||||
borderRadius: 1,
|
||||
fontFamily: 'monospace',
|
||||
wordBreak: 'break-all',
|
||||
userSelect: 'all',
|
||||
width: '100%',
|
||||
textAlign: 'center'
|
||||
}}
|
||||
>
|
||||
{props.s3Credentials.s3Bucket}
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ mt: 1, fontSize: '0.75rem', color: 'text.secondary', textAlign: 'center' }}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="deleteInstallationWarning"
|
||||
defaultMessage="Please note the bucket name above. Purging S3 data may take several minutes. If deletion fails, you can manually remove the bucket from Exoscale."
|
||||
/>
|
||||
</Typography>
|
||||
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ function InformationSalidomo(props: InformationSalidomoProps) {
|
|||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: 350,
|
||||
width: 450,
|
||||
bgcolor: 'background.paper',
|
||||
borderRadius: 4,
|
||||
boxShadow: 24,
|
||||
|
|
@ -134,7 +134,37 @@ function InformationSalidomo(props: InformationSalidomoProps) {
|
|||
gutterBottom
|
||||
sx={{ fontWeight: 'bold' }}
|
||||
>
|
||||
Do you want to delete this installation?
|
||||
<FormattedMessage
|
||||
id="confirmDeleteInstallation"
|
||||
defaultMessage="Do you want to delete this installation?"
|
||||
/>
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
mt: 1,
|
||||
p: 1,
|
||||
bgcolor: '#f5f5f5',
|
||||
borderRadius: 1,
|
||||
fontFamily: 'monospace',
|
||||
wordBreak: 'break-all',
|
||||
userSelect: 'all',
|
||||
width: '100%',
|
||||
textAlign: 'center'
|
||||
}}
|
||||
>
|
||||
{props.s3Credentials.s3Bucket}
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ mt: 1, fontSize: '0.75rem', color: 'text.secondary', textAlign: 'center' }}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="deleteInstallationWarning"
|
||||
defaultMessage="Please note the bucket name above. Purging S3 data may take several minutes. If deletion fails, you can manually remove the bucket from Exoscale."
|
||||
/>
|
||||
</Typography>
|
||||
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -193,7 +193,7 @@ function InformationSodistorehome(props: InformationSodistorehomeProps) {
|
|||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: 350,
|
||||
width: 450,
|
||||
bgcolor: 'background.paper',
|
||||
borderRadius: 4,
|
||||
boxShadow: 24,
|
||||
|
|
@ -208,7 +208,37 @@ function InformationSodistorehome(props: InformationSodistorehomeProps) {
|
|||
gutterBottom
|
||||
sx={{ fontWeight: 'bold' }}
|
||||
>
|
||||
Do you want to delete this installation?
|
||||
<FormattedMessage
|
||||
id="confirmDeleteInstallation"
|
||||
defaultMessage="Do you want to delete this installation?"
|
||||
/>
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
mt: 1,
|
||||
p: 1,
|
||||
bgcolor: '#f5f5f5',
|
||||
borderRadius: 1,
|
||||
fontFamily: 'monospace',
|
||||
wordBreak: 'break-all',
|
||||
userSelect: 'all',
|
||||
width: '100%',
|
||||
textAlign: 'center'
|
||||
}}
|
||||
>
|
||||
{props.s3Credentials.s3Bucket}
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ mt: 1, fontSize: '0.75rem', color: 'text.secondary', textAlign: 'center' }}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="deleteInstallationWarning"
|
||||
defaultMessage="Please note the bucket name above. Purging S3 data may take several minutes. If deletion fails, you can manually remove the bucket from Exoscale."
|
||||
/>
|
||||
</Typography>
|
||||
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -71,6 +71,9 @@
|
|||
"status": "Status",
|
||||
"live": "Live Daten",
|
||||
"deleteInstallation": "Installation löschen",
|
||||
"confirmDeleteInstallation": "Möchten Sie diese Installation löschen?",
|
||||
"bucketLabel": "Bucket",
|
||||
"deleteInstallationWarning": "Bitte notieren Sie den Bucket-Namen oben. Das Löschen der S3-Daten kann mehrere Minuten dauern. Überprüfen Sie nach dem Löschen in Exoscale, ob der Bucket entfernt wurde. Falls nicht, leeren und löschen Sie den Bucket manuell.",
|
||||
"errorOccured": "Ein Fehler ist aufgetreten",
|
||||
"successfullyUpdated": "Erfolgreich aktualisiert",
|
||||
"grantAccess": "Zugriff gewähren",
|
||||
|
|
|
|||
|
|
@ -53,6 +53,9 @@
|
|||
"status": "Status",
|
||||
"live": "Live View",
|
||||
"deleteInstallation": "Delete Installation",
|
||||
"confirmDeleteInstallation": "Do you want to delete this installation?",
|
||||
"bucketLabel": "Bucket",
|
||||
"deleteInstallationWarning": "Please note the bucket name above. Purging S3 data may take several minutes. After deletion, verify in Exoscale that the bucket has been removed. If not, purge and delete the bucket manually.",
|
||||
"errorOccured": "An error has occurred",
|
||||
"successfullyUpdated": "Successfully updated",
|
||||
"grantAccess": "Grant Access",
|
||||
|
|
|
|||
|
|
@ -65,6 +65,9 @@
|
|||
"status": "Statut",
|
||||
"live": "Diffusion en direct",
|
||||
"deleteInstallation": "Supprimer l'installation",
|
||||
"confirmDeleteInstallation": "Voulez-vous supprimer cette installation ?",
|
||||
"bucketLabel": "Bucket",
|
||||
"deleteInstallationWarning": "Veuillez noter le nom du bucket ci-dessus. La purge des données S3 peut prendre plusieurs minutes. Après la suppression, vérifiez dans Exoscale que le bucket a bien été supprimé. Sinon, purgez et supprimez le bucket manuellement.",
|
||||
"errorOccured": "Une erreur s'est produite",
|
||||
"successfullyUpdated": "Mise à jour réussie",
|
||||
"grantAccess": "Accorder l'accès",
|
||||
|
|
|
|||
|
|
@ -53,6 +53,9 @@
|
|||
"status": "Stato",
|
||||
"live": "Vista in diretta",
|
||||
"deleteInstallation": "Elimina installazione",
|
||||
"confirmDeleteInstallation": "Vuoi eliminare questa installazione?",
|
||||
"bucketLabel": "Bucket",
|
||||
"deleteInstallationWarning": "Prendi nota del nome del bucket qui sopra. L'eliminazione dei dati S3 potrebbe richiedere diversi minuti. Dopo l'eliminazione, verifica in Exoscale che il bucket sia stato rimosso. In caso contrario, svuota ed elimina il bucket manualmente.",
|
||||
"errorOccured": "Si è verificato un errore",
|
||||
"successfullyUpdated": "Aggiornamento riuscito",
|
||||
"grantAccess": "Concedi accesso",
|
||||
|
|
|
|||
Loading…
Reference in New Issue