2020
2121import org .apache .cloudstack .managed .context .ManagedContextRunnable ;
2222import org .apache .cloudstack .poll .BackgroundPollTask ;
23+ import org .apache .cloudstack .utils .identity .ManagementServerNode ;
2324import org .apache .logging .log4j .Logger ;
2425import org .apache .logging .log4j .LogManager ;
2526
3233import java .io .InputStream ;
3334import java .io .OutputStream ;
3435import java .io .Writer ;
36+ import java .net .InetAddress ;
37+ import java .net .UnknownHostException ;
3538import java .nio .charset .StandardCharsets ;
3639import java .nio .file .FileVisitResult ;
3740import java .nio .file .Files ;
@@ -73,12 +76,6 @@ public class InfrastructureBackupTask extends ManagedContextRunnable implements
7376 /** 24 hours in milliseconds */
7477 private static final long DAILY_INTERVAL_MS = 86400L * 1000L ;
7578
76- private final NASBackupProvider provider ;
77-
78- public InfrastructureBackupTask (NASBackupProvider provider ) {
79- this .provider = provider ;
80- }
81-
8279 @ Override
8380 public Long getDelay () {
8481 return DAILY_INTERVAL_MS ;
@@ -123,7 +120,8 @@ protected void runInContext() {
123120 boolean includeUsageDb = isUsageDbIncluded ();
124121
125122 String timestamp = LocalDateTime .now ().format (DateTimeFormatter .ofPattern ("yyyyMMdd-HHmmss" ));
126- String backupDir = nasBackupPath + "/infra-backup/" + timestamp ;
123+ String infraBackupRoot = nasBackupPath + "/infra-backup/" + getManagementServerLabel ();
124+ String backupDir = infraBackupRoot + "/" + timestamp ;
127125
128126 LOG .info ("Starting infrastructure backup to {} (database included: {})" , backupDir , includeDatabase );
129127
@@ -143,49 +141,11 @@ protected void runInContext() {
143141 return ;
144142 }
145143
146- // 1 & 2. Database backup — opt-in via nas.infra.backup.include.database.
147- // Production deployments typically run their own mysqldump cron jobs and disable this;
148- // it exists for small/edge deployments wanting unified DR on the same NAS as VM backups.
149- if (includeDatabase ) {
150- Properties dbProps = loadDbProperties ();
151- if (dbProps == null ) {
152- LOG .error ("Database backup requested but failed to load properties from {} — skipping DB component" , DB_PROPERTIES_PATH );
153- } else {
154- String dbHost = dbProps .getProperty ("db.cloud.host" , "localhost" );
155- String dbUser = dbProps .getProperty ("db.cloud.username" , "cloud" );
156- String dbPassword = dbProps .getProperty ("db.cloud.password" , "" );
157-
158- backupDatabase ("cloud" , backupDir , timestamp , dbHost , dbUser , dbPassword );
159-
160- if (includeUsageDb ) {
161- String usageHost = dbProps .getProperty ("db.usage.host" , dbHost );
162- String usageUser = dbProps .getProperty ("db.usage.username" , dbUser );
163- String usagePassword = dbProps .getProperty ("db.usage.password" , dbPassword );
164- backupDatabase ("cloud_usage" , backupDir , timestamp , usageHost , usageUser , usagePassword );
165- }
166- }
167- } else {
168- LOG .debug ("Database backup skipped (nas.infra.backup.include.database=false). " +
169- "Manage DB backups externally for production deployments." );
170- }
171-
172- // 3. Backup management server configs
144+ backupDatabases (backupDir , timestamp , includeDatabase , includeUsageDb );
173145 backupDirectory (MANAGEMENT_CONFIG_PATH , backupDir , "management-config" );
174-
175- // 4. Backup agent configs (if present on this host)
176- File agentDir = new File (AGENT_CONFIG_PATH );
177- if (agentDir .exists ()) {
178- backupDirectory (AGENT_CONFIG_PATH , backupDir , "agent-config" );
179- }
180-
181- // 5. Backup SSL certificates
182- File sslDir = new File (SSL_CERT_PATH );
183- if (sslDir .exists ()) {
184- backupDirectory (SSL_CERT_PATH , backupDir , "ssl-certs" );
185- }
186-
187- // 6. Cleanup old backups based on retention policy
188- cleanupOldBackups (nasBackupPath , retentionCount );
146+ backupDirectoryIfPresent (AGENT_CONFIG_PATH , backupDir , "agent-config" );
147+ backupDirectoryIfPresent (SSL_CERT_PATH , backupDir , "ssl-certs" );
148+ cleanupOldBackups (infraBackupRoot , retentionCount );
189149
190150 LOG .info ("Infrastructure backup completed successfully: {}" , backupDir );
191151
@@ -196,6 +156,63 @@ protected void runInContext() {
196156 }
197157 }
198158
159+ /**
160+ * Name of the sub-directory that keeps this management server's backups apart from those of the
161+ * other servers in the cluster. Management configs and certificates are per server, so they must
162+ * not overwrite each other, and the retention count applies per server. Uses the host name and
163+ * falls back to the management server id.
164+ */
165+ protected String getManagementServerLabel () {
166+ String label = null ;
167+ try {
168+ label = InetAddress .getLocalHost ().getHostName ();
169+ } catch (UnknownHostException e ) {
170+ LOG .debug ("Could not determine the local host name for the infrastructure backup directory: {}" , e .getMessage ());
171+ }
172+ if (label == null || label .isBlank ()) {
173+ label = "ms-" + ManagementServerNode .getManagementServerId ();
174+ }
175+ return label .replaceAll ("[^A-Za-z0-9._-]" , "_" );
176+ }
177+
178+ /**
179+ * Dumps the cloud database, and the usage database when requested, into {@code backupDir}.
180+ * The database component is opt-in ({@code nas.infra.backup.include.database}): production
181+ * deployments typically run their own mysqldump jobs and leave it off; it exists for small and
182+ * edge deployments that want unified disaster recovery on the same NAS as their VM backups.
183+ */
184+ protected void backupDatabases (String backupDir , String timestamp , boolean includeDatabase , boolean includeUsageDb ) {
185+ if (!includeDatabase ) {
186+ LOG .debug ("Database backup skipped (nas.infra.backup.include.database=false). " +
187+ "Manage DB backups externally for production deployments." );
188+ return ;
189+ }
190+ Properties dbProps = loadDbProperties ();
191+ if (dbProps == null ) {
192+ LOG .error ("Database backup requested but failed to load properties from {}, skipping DB component" , DB_PROPERTIES_PATH );
193+ return ;
194+ }
195+ String dbHost = dbProps .getProperty ("db.cloud.host" , "localhost" );
196+ String dbUser = dbProps .getProperty ("db.cloud.username" , "cloud" );
197+ String dbPassword = dbProps .getProperty ("db.cloud.password" , "" );
198+
199+ backupDatabase ("cloud" , backupDir , timestamp , dbHost , dbUser , dbPassword );
200+
201+ if (includeUsageDb ) {
202+ String usageHost = dbProps .getProperty ("db.usage.host" , dbHost );
203+ String usageUser = dbProps .getProperty ("db.usage.username" , dbUser );
204+ String usagePassword = dbProps .getProperty ("db.usage.password" , dbPassword );
205+ backupDatabase ("cloud_usage" , backupDir , timestamp , usageHost , usageUser , usagePassword );
206+ }
207+ }
208+
209+ /** Archives {@code sourcePath} like {@link #backupDirectory} but silently skips it when it does not exist on this server. */
210+ protected void backupDirectoryIfPresent (String sourcePath , String backupDir , String archiveName ) {
211+ if (new File (sourcePath ).exists ()) {
212+ backupDirectory (sourcePath , backupDir , archiveName );
213+ }
214+ }
215+
199216 /**
200217 * Acquire the cluster-wide run lock so only one management server performs the infrastructure
201218 * backup at a time. Returns null if the lock can't be taken (another MS holds it). Overridable
@@ -345,13 +362,14 @@ protected void backupDirectory(String sourcePath, String backupDir, String archi
345362 }
346363 }
347364
348- protected void cleanupOldBackups (String nasBackupPath , int retentionCount ) {
365+ /** Keeps the newest {@code retentionCount} backups under {@code infraBackupRoot} (this server's directory) and deletes the rest. */
366+ protected void cleanupOldBackups (String infraBackupRoot , int retentionCount ) {
349367 // A negative retention (misconfiguration) would make toDelete exceed backups.length below and
350368 // throw ArrayIndexOutOfBoundsException; clamp it so we never compute a delete count > available.
351369 if (retentionCount < 0 ) {
352370 retentionCount = 0 ;
353371 }
354- File infraDir = new File (nasBackupPath + "/infra-backup" );
372+ File infraDir = new File (infraBackupRoot );
355373 if (!infraDir .exists ()) {
356374 return ;
357375 }
0 commit comments