Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 4 additions & 24 deletions lib/routes/veeam/get.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
const xml2js = require('xml2js');
const { errors } = require('arsenal');
const metadata = require('../../metadata/wrapper');
const { respondWithData, buildHeadXML, getFileToBuild, isSystemXML } = require('./utils');
const { respondWithData, buildHeadXML, getFileToBuild, isSystemXML, fetchCapacityMetrics } = require('./utils');
const { responseXMLBody } = require('arsenal/build/lib/s3routes/routesUtils');
const UtilizationService = require('../../../lib/utilization/instance');

/**
* Returns system.xml or capacity.xml files for a given bucket.
Expand Down Expand Up @@ -34,7 +33,7 @@ function getVeeamFile(request, response, bucketMd, log) {

// Extract the last modified date, but do not include it when computing
// the file's ETag (md5)
const modified = fileToBuild.value.LastModified;
const modified = bucketMetrics?.date || new Date();
delete fileToBuild.value.LastModified;
Comment thread
DarkIsDude marked this conversation as resolved.
Outdated

// The SOSAPI metrics are dynamically computed using the SUR backend.
Expand All @@ -53,29 +52,10 @@ function getVeeamFile(request, response, bucketMd, log) {
return respondWithData(request, response, log, data,
buildHeadXML(builder.buildObject(fileToBuild.value)), modified);
};

if (!isSystemXML(request.objectKey)) {
const bucketKey = `${bucketMd._name}_${new Date(bucketMd._creationDate).getTime()}`;
return UtilizationService.getUtilizationMetrics('bucket', bucketKey, null, {}, (err, bucketMetrics) => {
return fetchCapacityMetrics(bucketMd, request, log, 'getVeeamFile', (err, bucketMetrics) => {
if (err) {
// Handle errors from UtilizationService/scubaclient
// axios errors have status in err.response.status
Comment thread
DarkIsDude marked this conversation as resolved.
const statusCode = err.response?.status || err.statusCode || err.code;
// Only handle 404 gracefully (no metrics available yet, e.g. post-install)
// For 404, continue with static capacity data (Used=0 from bucket metadata)
if (statusCode === 404) {
log.warn('UtilizationService returned 404 when fetching capacity metrics', {
method: 'getVeeamFile',
bucket: request.bucketName,
error: err.message || err.code,
});
return finalizeRequest();
}
log.error('error fetching capacity metrics from UtilizationService', {
method: 'getVeeamFile',
bucket: request.bucketName,
error: err.message || err.code,
statusCode,
});
return responseXMLBody(errors.InternalError, null, response, log);
}
return finalizeRequest(bucketMetrics);
Expand Down
51 changes: 35 additions & 16 deletions lib/routes/veeam/head.js
Comment thread
DarkIsDude marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const xml2js = require('xml2js');
const { errors } = require('arsenal');
const metadata = require('../../metadata/wrapper');
const { getResponseHeader, buildHeadXML, getFileToBuild } = require('./utils');
const { getResponseHeader, buildHeadXML, getFileToBuild, isSystemXML, fetchCapacityMetrics } = require('./utils');
const { responseXMLBody, responseContentHeaders } = require('arsenal/build/lib/s3routes/routesUtils');

/**
Expand All @@ -17,26 +17,45 @@ function headVeeamFile(request, response, bucketMd, log) {
if (!bucketMd) {
return responseXMLBody(errors.NoSuchBucket, null, response, log);
}

Comment thread
DarkIsDude marked this conversation as resolved.
return metadata.getBucket(request.bucketName, log, (err, data) => {
if (err) {
return responseXMLBody(errors.InternalError, null, response, log);
}
const fileToBuild = getFileToBuild(request, data._capabilities?.VeeamSOSApi);
if (fileToBuild.error) {
return responseXMLBody(fileToBuild.error, null, response, log);

const finalizeRequest = bucketMetrics => {
Comment thread
DarkIsDude marked this conversation as resolved.
Outdated
const fileToBuild = getFileToBuild(request, data._capabilities?.VeeamSOSApi);
if (fileToBuild.error) {
return responseXMLBody(fileToBuild.error, null, response, log);
}

// Extract the last modified date, but do not include it when computing the file's ETag (md5)
const modified = bucketMetrics?.date || new Date();
delete fileToBuild.value.LastModified;
const builder = new xml2js.Builder({
headless: true,
});
const dataBuffer = Buffer.from(buildHeadXML(builder.buildObject(fileToBuild)));

return responseContentHeaders(
null,
{},
getResponseHeader(request, data, dataBuffer, modified, log),
response,
log,
);
};

if (!isSystemXML(request.objectKey)) {
return fetchCapacityMetrics(bucketMd, request, log, 'headVeeamFile', (err, bucketMetrics) => {
if (err) {
return responseXMLBody(errors.InternalError, null, response, log);
}
return finalizeRequest(bucketMetrics);
});
}
let modified = new Date().toISOString();
// Extract the last modified date, but do not include it when computing
// the file's ETag (md5)
modified = fileToBuild.value.LastModified;
delete fileToBuild.value.LastModified;
// Recompute file content to generate appropriate content-md5 header
const builder = new xml2js.Builder({
headless: true,
});
const dataBuffer = Buffer.from(buildHeadXML(builder.buildObject(fileToBuild)));
return responseContentHeaders(null, {}, getResponseHeader(request, data,
dataBuffer, modified, log), response, log);

return finalizeRequest();
});
}

Expand Down
72 changes: 44 additions & 28 deletions lib/routes/veeam/list.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const { errors, errorInstances } = require('arsenal');
const querystring = require('querystring');
const metadata = require('../../metadata/wrapper');
const { responseXMLBody } = require('arsenal/build/lib/s3routes/routesUtils');
const { respondWithData, getResponseHeader, buildHeadXML, validPath } = require('./utils');
const { respondWithData, getResponseHeader, buildHeadXML, validPath, fetchCapacityMetrics } = require('./utils');
const { processVersions, processMasterVersions } = require('../../api/bucketGet');


Expand Down Expand Up @@ -96,37 +96,53 @@ function listVeeamFiles(request, response, bucketMd, log) {
if (err) {
return responseXMLBody(errors.InternalError, null, response, log);
}
const filesToBuild = [];
const fieldsToGenerate = [];
if (data._capabilities?.VeeamSOSApi?.SystemInfo) {
fieldsToGenerate.push({
...data._capabilities?.VeeamSOSApi?.SystemInfo,
name: `${validPath}system.xml`,

const buildAndRespond = bucketMetrics => {
Comment thread
DarkIsDude marked this conversation as resolved.
Outdated
const filesToBuild = [];
const fieldsToGenerate = [];
if (data._capabilities?.VeeamSOSApi?.SystemInfo) {
fieldsToGenerate.push({
...data._capabilities?.VeeamSOSApi?.SystemInfo,
name: `${validPath}system.xml`,
});
}
if (data._capabilities?.VeeamSOSApi?.CapacityInfo) {
fieldsToGenerate.push({
...data._capabilities?.VeeamSOSApi?.CapacityInfo,
name: `${validPath}capacity.xml`,
});
}
fieldsToGenerate.forEach(file => {
const isCapacity = file.name.endsWith('capacity.xml');
const lastModified = isCapacity
? (bucketMetrics?.date || new Date())
: file.LastModified;
// eslint-disable-next-line no-param-reassign
delete file.LastModified;
const builder = new xml2js.Builder({
headless: true,
});
const dataBuffer = Buffer.from(buildHeadXML(builder.buildObject(file)));
filesToBuild.push({
...getResponseHeader(request, data,
dataBuffer, lastModified, log),
name: file.name,
});
});
}
// When `versions` is present, listing should return a versioned list
return respondWithData(request, response, log, data,
buildXMLResponse(request, filesToBuild, 'versions' in request.query));
};

if (data._capabilities?.VeeamSOSApi?.CapacityInfo) {
fieldsToGenerate.push({
...data._capabilities?.VeeamSOSApi?.CapacityInfo,
name: `${validPath}capacity.xml`,
return fetchCapacityMetrics(bucketMd, request, log, 'listVeeamFiles', (err, bucketMetrics) => {
Comment thread
DarkIsDude marked this conversation as resolved.
Outdated
if (err) {
return responseXMLBody(errors.InternalError, null, response, log);
}
return buildAndRespond(bucketMetrics);
});
}
fieldsToGenerate.forEach(file => {
const lastModified = file.LastModified;
// eslint-disable-next-line no-param-reassign
delete file.LastModified;
const builder = new xml2js.Builder({
headless: true,
});
const dataBuffer = Buffer.from(buildHeadXML(builder.buildObject(file)));
filesToBuild.push({
...getResponseHeader(request, data,
dataBuffer, lastModified, log),
name: file.name,
});
});
// When `versions` is present, listing should return a versioned list
return respondWithData(request, response, log, data,
buildXMLResponse(request, filesToBuild, 'versions' in request.query));
return buildAndRespond();
});
}

Expand Down
45 changes: 43 additions & 2 deletions lib/routes/veeam/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const collectResponseHeaders = require('../../utilities/collectResponseHeaders')
const collectCorsHeaders = require('../../utilities/collectCorsHeaders');
const crypto = require('crypto');
const { prepareStream } = require('arsenal/build/lib/s3middleware/prepareStream');
const UtilizationService = require('../../utilization/instance');

/**
* Decodes an URI and return the result.
Expand Down Expand Up @@ -172,13 +173,15 @@ function isSystemXML(objectKey) {
*/
function getFileToBuild(request, data, inlineLastModified = false) {
const _isSystemXML = isSystemXML(request.objectKey);
const fileToBuild = _isSystemXML ? data?.SystemInfo
: data?.CapacityInfo;
const fileToBuild = _isSystemXML ? data?.SystemInfo : data?.CapacityInfo;

if (!fileToBuild) {
return { error: errors.NoSuchKey };
}

const modified = fileToBuild.LastModified || (new Date()).toISOString();
const fieldName = _isSystemXML ? 'SystemInfo' : 'CapacityInfo';

if (inlineLastModified) {
fileToBuild.LastModified = modified;
return {
Expand All @@ -199,6 +202,43 @@ function getFileToBuild(request, data, inlineLastModified = false) {
}
}

/**
* Fetches capacity metrics from UtilizationService for a bucket.
* Handles 404 gracefully (no metrics available yet, e.g. post-install).
*
* @param {object} bucketMd - bucket metadata
* @param {object} request - incoming request
* @param {object} log - logger object
* @param {string} method - calling method name for log context
* @param {function} callback - (err, bucketMetrics) where bucketMetrics
* is undefined when metrics are not available (404)
* @returns {undefined}
*/
function fetchCapacityMetrics(bucketMd, request, log, method, callback) {
const bucketKey = `${bucketMd._name}_${new Date(bucketMd._creationDate).getTime()}`;
return UtilizationService.getUtilizationMetrics('bucket', bucketKey, null, {}, (err, bucketMetrics) => {
if (err) {
const statusCode = err.response?.status || err.statusCode || err.code;
if (statusCode === 404) {
log.warn('UtilizationService returned 404 when fetching capacity metrics', {
method,
bucket: request.bucketName,
error: err.message || err.code,
});
return callback(null);
}
log.error('error fetching capacity metrics from UtilizationService', {
method,
bucket: request.bucketName,
error: err.message || err.code,
statusCode,
});
return callback(err);
}
return callback(null, bucketMetrics);
});
}

module.exports = {
_decodeURI,
receiveData,
Expand All @@ -208,4 +248,5 @@ module.exports = {
validPath,
isSystemXML,
getFileToBuild,
fetchCapacityMetrics,
};
2 changes: 1 addition & 1 deletion tests/unit/api/apiUtils/objectLockHelpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ describe('objectLockHelpers: calculateRetainUntilDate', () => {
};
const date = moment();
const expectedRetainUntilDate
= date.add(mockConfigWithYears.years * 365, 'days');
= date.add(mockConfigWithYears.years * 365 * 86400000, 'ms');
Comment thread
francoisferrand marked this conversation as resolved.
const retainUntilDate = calculateRetainUntilDate(mockConfigWithYears);
assert.strictEqual(retainUntilDate.slice(0, 16),
expectedRetainUntilDate.toISOString().slice(0, 16));
Expand Down
Loading
Loading