feat: implement QEMU clone and local migration
This commit is contained in:
@@ -322,6 +322,69 @@ def register_qemu_handlers(registry: HandlerRegistry) -> None:
|
||||
async def snapshot_rollback(request: Request, inputs: dict[str, Any]) -> str:
|
||||
return await snapshot_task("snapshot-rollback", request, inputs)
|
||||
|
||||
async def clone(request: Request, inputs: dict[str, Any]) -> str:
|
||||
values = _values(inputs)
|
||||
node, vmid, newid = str(values["node"]), str(values["vmid"]), str(values["newid"])
|
||||
source = await _qemu_resource(request, node, vmid)
|
||||
if await _database(request).pool.fetchval(
|
||||
"""SELECT EXISTS(SELECT 1 FROM resources
|
||||
WHERE external_id=$1 AND kind IN ('qemu','lxc'))""",
|
||||
newid,
|
||||
):
|
||||
raise ApiError(409, "VMID already exists")
|
||||
target = str(values.get("target") or node)
|
||||
if not await _database(request).pool.fetchval(
|
||||
"SELECT EXISTS(SELECT 1 FROM nodes WHERE name=$1)", target
|
||||
):
|
||||
raise ApiError(404, "target node does not exist")
|
||||
return await _create_task(
|
||||
request,
|
||||
node=target,
|
||||
vmid=newid,
|
||||
task_type="qemu-clone",
|
||||
payload={
|
||||
"source_resource_id": str(source["id"]),
|
||||
"source_vmid": vmid,
|
||||
"node": target,
|
||||
"vmid": int(newid),
|
||||
"name": values.get("name"),
|
||||
"description": values.get("description"),
|
||||
"full": bool(values.get("full", False)),
|
||||
},
|
||||
)
|
||||
|
||||
async def migrate_preconditions(request: Request, inputs: dict[str, Any]) -> dict[str, Any]:
|
||||
values = _values(inputs)
|
||||
await _qemu_resource(request, str(values["node"]), str(values["vmid"]))
|
||||
target = str(values["target"])
|
||||
exists = await _database(request).pool.fetchval(
|
||||
"SELECT EXISTS(SELECT 1 FROM nodes WHERE name=$1)", target
|
||||
)
|
||||
if not exists:
|
||||
raise ApiError(404, "target node does not exist")
|
||||
return {"local_disks": [], "local_resources": [], "running": False}
|
||||
|
||||
async def migrate(request: Request, inputs: dict[str, Any]) -> str:
|
||||
values = _values(inputs)
|
||||
node, vmid, target = str(values["node"]), str(values["vmid"]), str(values["target"])
|
||||
resource = await _qemu_resource(request, node, vmid)
|
||||
if target == node:
|
||||
raise ApiError(400, "target node is the same as source node")
|
||||
await migrate_preconditions(request, inputs)
|
||||
return await _create_task(
|
||||
request,
|
||||
node=node,
|
||||
vmid=vmid,
|
||||
task_type="qemu-migrate",
|
||||
payload={
|
||||
"resource_id": str(resource["id"]),
|
||||
"node": node,
|
||||
"target": target,
|
||||
"vmid": vmid,
|
||||
"online": bool(values.get("online", False)),
|
||||
},
|
||||
)
|
||||
|
||||
async def task_list(request: Request, inputs: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
tasks = await TaskRepository(_database(request).pool).list_for_node(
|
||||
str(_values(inputs)["node"])
|
||||
@@ -382,6 +445,9 @@ def register_qemu_handlers(registry: HandlerRegistry) -> None:
|
||||
registry.register(
|
||||
"/nodes/{node}/qemu/{vmid}/snapshot/{snapname}/rollback", "POST", snapshot_rollback
|
||||
)
|
||||
registry.register("/nodes/{node}/qemu/{vmid}/clone", "POST", clone)
|
||||
registry.register("/nodes/{node}/qemu/{vmid}/migrate", "GET", migrate_preconditions)
|
||||
registry.register("/nodes/{node}/qemu/{vmid}/migrate", "POST", migrate)
|
||||
registry.register("/nodes/{node}/tasks", "GET", task_list)
|
||||
registry.register("/nodes/{node}/tasks/{upid}/status", "GET", task_status)
|
||||
registry.register("/nodes/{node}/tasks/{upid}/log", "GET", task_log)
|
||||
@@ -405,6 +471,8 @@ async def _create_task(
|
||||
"qemu-snapshot-create": "qmsnapshot",
|
||||
"qemu-snapshot-delete": "qmdelsnapshot",
|
||||
"qemu-snapshot-rollback": "qmrollback",
|
||||
"qemu-clone": "qmclone",
|
||||
"qemu-migrate": "qmigrate",
|
||||
}[task_type]
|
||||
upid = str(Upid(node, pid, pid, timestamp, worker_type, vmid, str(request.state.principal)))
|
||||
try:
|
||||
|
||||
@@ -44,12 +44,14 @@ def create_app(
|
||||
repository,
|
||||
"simulator-worker",
|
||||
{
|
||||
"qemu-clone": handler,
|
||||
"qemu-create": handler,
|
||||
"qemu-delete": handler,
|
||||
"qemu-reboot": handler,
|
||||
"qemu-reset": handler,
|
||||
"qemu-resume": handler,
|
||||
"qemu-shutdown": handler,
|
||||
"qemu-migrate": handler,
|
||||
"qemu-snapshot-create": handler,
|
||||
"qemu-snapshot-delete": handler,
|
||||
"qemu-snapshot-rollback": handler,
|
||||
|
||||
@@ -18,6 +18,8 @@ def qemu_handler(repository: TaskRepository, clock: Clock) -> TaskHandler:
|
||||
operation = task.task_type.removeprefix("qemu-")
|
||||
if operation == "create":
|
||||
return await _create(repository, task)
|
||||
if operation == "clone":
|
||||
return await _clone(repository, task)
|
||||
resource_id = uuid.UUID(str(task.payload["resource_id"]))
|
||||
if operation == "update":
|
||||
return await _update(repository, task, resource_id)
|
||||
@@ -27,6 +29,8 @@ def qemu_handler(repository: TaskRepository, clock: Clock) -> TaskHandler:
|
||||
return await _snapshot(
|
||||
repository, task, resource_id, operation.removeprefix("snapshot-")
|
||||
)
|
||||
if operation == "migrate":
|
||||
return await _migrate(repository, task, resource_id, clock)
|
||||
async with repository.pool.acquire() as connection:
|
||||
row = await connection.fetchrow("SELECT state FROM resources WHERE id=$1", resource_id)
|
||||
if row is None:
|
||||
@@ -201,5 +205,78 @@ async def _snapshot(
|
||||
return {"snapshot": name, "operation": operation}
|
||||
|
||||
|
||||
async def _clone(repository: TaskRepository, task: Task) -> dict[str, Any]:
|
||||
source_id = uuid.UUID(str(task.payload["source_resource_id"]))
|
||||
target_id = uuid.uuid4()
|
||||
node, vmid = str(task.payload["node"]), int(task.payload["vmid"])
|
||||
async with repository.pool.acquire() as connection:
|
||||
async with connection.transaction():
|
||||
source = await connection.fetchrow(
|
||||
"""SELECT r.state, v.config FROM resources r
|
||||
JOIN virtual_machines v ON v.resource_id=r.id WHERE r.id=$1""",
|
||||
source_id,
|
||||
)
|
||||
target = await connection.fetchrow(
|
||||
"SELECT id, cluster_id FROM nodes WHERE name=$1", node
|
||||
)
|
||||
if source is None or target is None:
|
||||
raise ValueError("clone source or target disappeared")
|
||||
config = _object(source["config"])
|
||||
if task.payload.get("name") is not None:
|
||||
config["name"] = task.payload["name"]
|
||||
state = {**_object(source["state"]), **config, "status": "stopped"}
|
||||
await connection.execute(
|
||||
"""INSERT INTO resources(id,node_id,cluster_id,kind,external_id,state,metadata)
|
||||
VALUES($1,$2,$3,'qemu',$4,$5::jsonb,'{}'::jsonb)""",
|
||||
target_id,
|
||||
target["id"],
|
||||
target["cluster_id"],
|
||||
str(vmid),
|
||||
json.dumps(state),
|
||||
)
|
||||
await connection.execute(
|
||||
"""INSERT INTO virtual_machines(resource_id,cluster_id,vmid,config)
|
||||
VALUES($1,$2,$3,$4::jsonb)""",
|
||||
target_id,
|
||||
target["cluster_id"],
|
||||
vmid,
|
||||
json.dumps(config),
|
||||
)
|
||||
await repository.append_log(task.id, f"VM cloned to {vmid}")
|
||||
return {"vmid": vmid, "node": node}
|
||||
|
||||
|
||||
async def _migrate(
|
||||
repository: TaskRepository, task: Task, resource_id: uuid.UUID, clock: Clock
|
||||
) -> dict[str, Any]:
|
||||
target = str(task.payload["target"])
|
||||
async with repository.pool.acquire() as connection:
|
||||
row = await connection.fetchrow("SELECT state FROM resources WHERE id=$1", resource_id)
|
||||
if row is None:
|
||||
raise ValueError("resource disappeared")
|
||||
state = _object(row["state"])
|
||||
transition = plan_transition(VmState(str(state["status"])), "migrate")
|
||||
state["status"] = transition.intermediate
|
||||
await connection.execute(
|
||||
"UPDATE resources SET state=$2::jsonb WHERE id=$1", resource_id, json.dumps(state)
|
||||
)
|
||||
await repository.append_log(task.id, f"migration to {target} started")
|
||||
await clock.sleep(1.0)
|
||||
async with repository.pool.acquire() as connection:
|
||||
node = await connection.fetchrow("SELECT id FROM nodes WHERE name=$1", target)
|
||||
if node is None:
|
||||
raise ValueError("target node disappeared")
|
||||
state["status"] = transition.after
|
||||
await connection.execute(
|
||||
"""UPDATE resources SET node_id=$2,state=$3::jsonb,version=version+1,
|
||||
updated_at=now() WHERE id=$1""",
|
||||
resource_id,
|
||||
node["id"],
|
||||
json.dumps(state),
|
||||
)
|
||||
await repository.append_log(task.id, f"migration to {target} completed")
|
||||
return {"node": target, "status": str(transition.after)}
|
||||
|
||||
|
||||
def _object(value: object) -> dict[str, Any]:
|
||||
return json.loads(value) if isinstance(value, str) else dict(cast(Mapping[str, Any], value))
|
||||
|
||||
Reference in New Issue
Block a user