Tạo Snapshot và Backup với OpenStack
Giới thiệu
Trong bài này, chúng ta sẽ tìm hiểu cách tạo snapshot và backup với OpenStack Nova và Glance APIs, bao gồm polling image status và metadata management.
Snapshot vs Backup trong OpenStack
Snapshot (createImage)
Nova Action: createImage
- Tạo image từ server hiện tại
- Không có rotation
- Metadata:
source: 'portal-manual' - Dùng cho: Manual snapshot từ user
Backup (createBackup)
Nova Action: createBackup
- Tạo backup với rotation support
- Tự động xóa backup cũ dựa trên rotation count
- Metadata:
source: 'portal-auto' - Dùng cho: Scheduled backup tự động
Flow Tạo Snapshot
Step-by-Step Flow
1. Get scoped token (Keystone)
↓
2. Get server details (Nova GET /servers/{id})
↓
3. Trigger snapshot (Nova POST /servers/{id}/action)
Body: { createImage: { name, metadata } }
↓
4. Extract image ID from Location header
↓
5. Poll image status (Glance GET /v2/images/{id})
- Status: queued → saving → active
- Timeout: 30 phút
- Interval: 5 giây
↓
6. Update metadata (Glance PATCH /v2/images/{id})
- Add OS metadata (os_distro, os_version, os_type)
↓
7. Return image ID và detailsImplementation
Create Server Snapshot
typescript
export const createServerSnapshotOpenStack = async function (params: {
serverId: string;
snapshotName: string;
location?: string;
osImageFromInet?: string;
}) {
const { serverId, snapshotName, location, osImageFromInet } = params;
// 1. Get scoped token
const { token } = await getScopedToken(location);
// 2. Build endpoints
const computeBase = buildEndpoint('compute', location);
const imageBase = buildEndpoint('image', location);
// 3. Get server details
const nova = createAxios(computeBase, { 'X-Auth-Token': token });
const serverRes = await nova.get(`/servers/${encodeURIComponent(serverId)}`);
const server: any = serverRes.data?.server || serverRes.data;
// 4. Prepare OS metadata
let osMetadata: any = {};
if (osImageFromInet) {
osMetadata = {
os_distro: osImageFromInet, // e.g., "Ubuntu 20.04"
};
}
// 5. Trigger snapshot via Nova
const actionUrl = `/servers/${encodeURIComponent(serverId)}/action`;
const actionBody = {
createImage: {
name: snapshotName,
metadata: {
source: 'portal-manual', // Manual snapshot
...osMetadata,
},
},
};
const actionRes = await nova.post(actionUrl, actionBody, {
validateStatus: (s) => s === 202
});
// 6. Extract image ID from Location header
const locationHeader = String(
actionRes.headers['location'] ||
actionRes.headers['Location'] ||
''
);
let imageId = '';
if (locationHeader) {
const match = locationHeader.match(/\/v2\/.+\/images\/([^/?#]+)/) ||
locationHeader.match(/images\/?([^/?#]+)/);
imageId = match ? match[1] : '';
}
// 7. Poll image status via Glance
const glance = createAxios(imageBase, { 'X-Auth-Token': token });
const startedAt = Date.now();
const timeoutMs = 30 * 60 * 1000; // 30 minutes
const intervalMs = 5000; // 5 seconds
let status = 'queued';
let details: any = null;
while (Date.now() - startedAt < timeoutMs) {
try {
if (imageId) {
const r = await glance.get(`/v2/images/${encodeURIComponent(imageId)}`);
details = r.data;
status = String(details.status || details.state || '').toLowerCase();
} else {
// Fallback: search by name
const r = await glance.get('/v2/images', {
params: { name: snapshotName }
});
const items: any[] = (r.data?.images || [])
.filter((i: any) => i.name === snapshotName);
if (items.length > 0) {
details = items[0];
imageId = String(details.id || details.image_id || '');
status = String(details.status || '').toLowerCase();
}
}
logger.info(
`OpenStack snapshot '${snapshotName}' status: ${status}` +
`${imageId ? ` (id: ${imageId})` : ''}`
);
if (status === 'active') break;
if (
status === 'killed' ||
status === 'deleted' ||
status === 'error' ||
status === 'failed'
) {
throw new Error(`Snapshot failed with status: ${status}`);
}
} catch (e: any) {
logger.warn(`Polling image status failed: ${e?.message || e}`);
}
await new Promise((r) => setTimeout(r, intervalMs));
}
if (status !== 'active') {
throw new Error(
`Timeout waiting for snapshot to become active. ` +
`Last status: ${status}`
);
}
// 8. Update metadata in Glance (optional but recommended)
if (imageId && Object.keys(osMetadata).length > 0) {
try {
const patchOps = [];
if (osMetadata.os_distro) {
patchOps.push({
op: 'add',
path: '/os_distro',
value: osMetadata.os_distro
});
}
// ... other metadata fields
if (patchOps.length > 0) {
await glance.patch(
`/v2/images/${encodeURIComponent(imageId)}`,
patchOps,
{
headers: {
'Content-Type': 'application/openstack-images-v2.1-json-patch'
}
}
);
}
} catch (e: any) {
logger.error(`Could not update snapshot metadata: ${e?.message}`);
// Don't throw - metadata update is optional
}
}
return { imageId, imageName: snapshotName, details };
};Create Server Backup
Backup với Rotation
typescript
export const createServerBackupOpenStack = async function (params: {
serverId: string;
backupName: string;
backupType?: string;
rotation?: number;
location?: string;
osImageFromInet?: string;
}) {
const { serverId, backupName, backupType = 'daily', rotation, location, osImageFromInet } = params;
// 1. Get scoped token
const { token } = await getScopedToken(location);
// 2. Build endpoints
const computeBase = buildEndpoint('compute', location);
const imageBase = buildEndpoint('image', location);
// 3. Prepare OS metadata
let osMetadata: any = {};
if (osImageFromInet) {
osMetadata = {
os_distro: osImageFromInet,
};
}
// 4. Trigger backup via Nova
const nova = createAxios(computeBase, { 'X-Auth-Token': token });
const actionUrl = `/servers/${encodeURIComponent(serverId)}/action`;
const actionBody = {
createBackup: {
name: backupName,
backup_type: backupType,
rotation: rotation,
metadata: {
source: 'portal-auto', // Scheduled backup
...osMetadata,
},
},
};
const actionRes = await nova.post(actionUrl, actionBody, {
validateStatus: (s) => s === 202
});
// 5. Extract image ID và poll status (tương tự snapshot)
// ... (same polling logic as snapshot)
return { imageId, imageName: backupName, details };
};Polling Image Status
Image Status States
| Status | Description |
|---|---|
queued | Image creation queued |
saving | Image is being saved |
active | Image is ready ✅ |
killed | Image creation killed ❌ |
deleted | Image deleted ❌ |
error | Error occurred ❌ |
failed | Creation failed ❌ |
Polling Pattern
typescript
const startedAt = Date.now();
const timeoutMs = 30 * 60 * 1000; // 30 minutes
const intervalMs = 5000; // 5 seconds
while (Date.now() - startedAt < timeoutMs) {
const image = await glance.get(`/v2/images/${imageId}`);
const status = image.status?.toLowerCase();
// Success case
if (status === "active") {
break; // Image ready
}
// Error cases
if (["killed", "deleted", "error", "failed"].includes(status)) {
throw new Error(`Image creation failed: ${status}`);
}
// Continue polling
await new Promise((r) => setTimeout(r, intervalMs));
}
// Check timeout
if (status !== "active") {
throw new Error(`Timeout waiting for image. Last status: ${status}`);
}Best Practices
- Timeout hợp lý: 30 phút cho large servers
- Interval không quá ngắn: 5 giây để tránh spam API
- Fallback search: Nếu không có imageId từ Location header
- Error handling: Catch và log polling errors, không throw ngay
Metadata Management
Snapshot Metadata Structure
typescript
{
source: 'portal-manual' | 'portal-auto',
os_distro: 'Ubuntu 20.04',
os_version: '20.04',
os_type: 'linux'
}Update Metadata trong Glance
typescript
const patchOps = [
{ op: "add", path: "/os_distro", value: "Ubuntu 20.04" },
{ op: "add", path: "/os_version", value: "20.04" },
{ op: "add", path: "/os_type", value: "linux" },
];
await glance.patch(`/v2/images/${imageId}`, patchOps, {
headers: {
"Content-Type": "application/openstack-images-v2.1-json-patch",
},
});Lý do update metadata:
- Nova metadata có thể không được lưu đầy đủ
- Glance metadata là source of truth
- Dễ dàng query và filter sau này
Error Handling
Common Errors
1. Server Not Found
typescript
try {
const serverRes = await nova.get(`/servers/${serverId}`);
} catch (error: any) {
if (error.response?.status === 404) {
throw new Error(`Server not found: ${serverId}`);
}
throw error;
}2. Image Creation Failed
typescript
if (["killed", "deleted", "error", "failed"].includes(status)) {
throw new Error(`Snapshot failed with status: ${status}`);
}3. Timeout
typescript
if (status !== "active") {
throw new Error(
`Timeout waiting for snapshot to become active. ` + `Last status: ${status}`
);
}4. Missing Image ID
typescript
if (!imageId) {
// Fallback: search by name
const r = await glance.get("/v2/images", {
params: { name: snapshotName },
});
// ...
}Testing
Unit Test Example
typescript
describe('createServerSnapshotOpenStack', () => {
it('should create snapshot successfully', async () => {
const result = await createServerSnapshotOpenStack({
serverId: 'test-server-id',
snapshotName: 'test-snapshot',
location: 'HCM',
osImageFromInet: 'Ubuntu 20.04'
});
expect(result.imageId).toBeDefined();
expect(result.imageName).toBe('test-snapshot');
});
it('should handle timeout', async () => {
// Mock long-running image creation
// ...
await expect(
createServerSnapshotOpenStack({...})
).rejects.toThrow('Timeout');
});
});Summary
Key Points
Snapshot vs Backup
- Snapshot:
createImage, manual, no rotation - Backup:
createBackup, scheduled, with rotation
- Snapshot:
Polling Pattern
- Poll every 5 seconds
- Timeout after 30 minutes
- Handle all status states
Metadata Management
- Store source (
portal-manualvsportal-auto) - Store OS info (
os_distro,os_version) - Update via Glance PATCH API
- Store source (
Error Handling
- Check server exists
- Handle image creation failures
- Handle timeout
- Fallback search by name
Next Steps
Trong bài tiếp theo, chúng ta sẽ tìm hiểu về:
- Schedule System - Cron-based backup scheduler
Last Updated: 2025-01-25
Previous: 02. Authentication
Next: 04. Schedule System