Skip to content

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à details

Implementation

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

StatusDescription
queuedImage creation queued
savingImage is being saved
activeImage is ready ✅
killedImage creation killed ❌
deletedImage deleted ❌
errorError occurred ❌
failedCreation 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

  1. Timeout hợp lý: 30 phút cho large servers
  2. Interval không quá ngắn: 5 giây để tránh spam API
  3. Fallback search: Nếu không có imageId từ Location header
  4. 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

  1. Snapshot vs Backup

    • Snapshot: createImage, manual, no rotation
    • Backup: createBackup, scheduled, with rotation
  2. Polling Pattern

    • Poll every 5 seconds
    • Timeout after 30 minutes
    • Handle all status states
  3. Metadata Management

    • Store source (portal-manual vs portal-auto)
    • Store OS info (os_distro, os_version)
    • Update via Glance PATCH API
  4. 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ề:


Last Updated: 2025-01-25
Previous: 02. Authentication
Next: 04. Schedule System

Internal documentation for iNET Portal