Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"""add REDEPLOYING to deploymentinstancestatus enum

Revision ID: e2a91d05c7b8
Revises: f9c2a14e7b80
Create Date: 2026-06-30 09:00:00.000000

"""
from typing import Sequence, Union

from alembic import op


# revision identifiers, used by Alembic.
revision: str = 'e2a91d05c7b8'
down_revision: Union[str, Sequence[str], None] = 'f9c2a14e7b80'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
"""Upgrade schema.

Adds a new ``REDEPLOYING`` value to the ``deploymentinstancestatus``
Postgres enum so the ``redeploy_instance`` Celery task can mark a single
DeploymentInstance row as transient while it tears down + recreates its
Heat stack, without flipping the parent Deployment to RESTARTING (which
would block the user from running other ops on its siblings).

``IF NOT EXISTS`` keeps the migration idempotent — safe to re-run if a
manual ALTER got there first. Same approach as
``034d40e1dad3_add_activation_link_to_access_type``.
"""
op.execute(
"ALTER TYPE deploymentinstancestatus "
"ADD VALUE IF NOT EXISTS 'REDEPLOYING'"
)


def downgrade() -> None:
"""Downgrade schema.

Postgres does not support removing values from an enum type without
rebuilding the type and all dependent columns. Rolling back the
application code is the correct response; the unused enum label is
harmless. Left intentionally empty (mirrors
``034d40e1dad3_add_activation_link_to_access_type``).
"""
pass
160 changes: 160 additions & 0 deletions bruno/Deployments/Redeploy Deployment.bru
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
meta {
name: Redeploy Deployment
type: http
seq: 6
}

post {
url: {{base_url}}/api/v1/deployments/{{deployment_id}}/redeploy?openstack_project_id={{openstack_project_local_id}}
body: json
auth: bearer
}

auth:bearer {
token: {{access_token}}
}

params:query {
openstack_project_id: {{openstack_project_local_id}}
}

params:path {
deployment_id:
}

body:json {
{
"deployment_parameter_overrides": {
"include_example_notebooks": true
},
"instance_parameter_overrides": {
"instance-uuid-of-group-3": {
"flavor": "gp1.medium"
}
},
"preserve_credentials": false
}
}

docs {
# Redeploy Deployment (all VMs)

Destroy-and-recreate **every VM** (DeploymentInstance) in this deployment, one
after another, optionally with overridden parameters (template / config values).

Unlike `POST /deployments/{id}/restart` (which only triggers a Heat
`update_stack` on the existing stack), this fully rebuilds each VM from
scratch:

1. Heat stack deleted
2. Heat stack recreated with merged parameters
3. Ansible re-run
4. Credentials regenerated (unless `preserve_credentials=true`)

Use it when a config / template parameter changed and you want the change to
actually take effect.

## Authorization
- **Owner (Lecturer)** or **Admin** only
- Lecturers can only redeploy their own deployments
- Admins can redeploy any deployment

## Body

All fields are optional. Empty body = redeploy with the stored
`deployment_parameters` unchanged and fresh credentials.

| Field | Type | Description |
|----------------------------------|-------------------------------------|-------------|
| `deployment_parameter_overrides` | `dict[str, Any]` | Merged ON TOP of the deployment's stored parameters for every redeployed VM. |
| `instance_parameter_overrides` | `dict[instance_id, dict[str, Any]]` | Per-VM overrides, keyed by `DeploymentInstance.id`. Merged ON TOP of `deployment_parameter_overrides` for that one VM. |
| `preserve_credentials` | `bool` (default `false`) | Keep the existing per-VM credentials instead of regenerating them. Useful to avoid breaking student logins during a quick config change. |

### Merge order

Later layers win:

template defaults
deployment.deployment_parameters
deployment_parameter_overrides
instance_parameter_overrides[<instance_id>]

## Process

1. API validates permissions and deployment state.
2. A single Celery task `redeploy_deployment` is enqueued.
3. The task iterates **sequentially** over every DeploymentInstance row and
reuses the `redeploy_instance` task per VM (to avoid quota exhaustion in
parallel mode).
4. For each VM: status flips to `REDEPLOYING` → Heat stack deleted → Heat
stack recreated with merged params → Ansible run → credentials persisted.
5. The parent deployment stays in `RUNNING` between instances; siblings
remain reachable for students.

## Response

Returns 202 Accepted; the actual redeploy runs asynchronously.

## Example Response

```json
{
"success": true,
"data": {
"deployment_id": "deploy-123",
"instance_count": 5,
"status": "redeploy_queued",
"preserve_credentials": false
},
"message": "Redeploy requested for 5 instance(s); operation in progress",
"request_id": "req-456"
}
```

## Error Responses

**404 Not Found**
```json
{"success": false, "message": "Deployment with ID xyz not found"}
```

**403 Forbidden** (lecturer trying to redeploy another lecturer's deployment)
```json
{"success": false, "message": "You do not have permission to access this deployment"}
```

**400 Bad Request** (deployment in transitional state)
```json
{"success": false, "message": "Cannot redeploy deployment in creating state. Please wait for the current operation to complete."}
```

**400 Bad Request** (deployment has no instances)
```json
{"success": false, "message": "Deployment has no instances to redeploy"}
```

**500 Internal Server Error** (task enqueue failure)
```json
{"success": false, "message": "Failed to enqueue redeploy task"}
```

## Tracking Progress

- Per-VM status visible on each `DeploymentInstance.status` (flips through
`REDEPLOYING` → `CREATING` → `RUNNING|FAILED`).
- Live log stream: `GET /deployments/{id}/logs/stream`.
- The deployment's `openstack_stack_id` JSON array is rewritten incrementally
as old stack IDs are dropped and new ones are added — so a follow-up DELETE
still cleans up correctly.

## Caveats

- `preserve_credentials=true` can only carry over credentials that are
persisted in `DeploymentInstanceAccess`. Passwords generated ad-hoc inside
Ansible playbooks (without a credential spec entry) are still regenerated.
- The Heat stack name format stays `<deployment-slug>-s<idx>-<dep4>`, so
OpenStack tags / dashboards keep working across redeploys.
}
141 changes: 141 additions & 0 deletions bruno/Deployments/Redeploy Instance.bru
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
meta {
name: Redeploy Instance
type: http
seq: 7
}

post {
url: {{base_url}}/api/v1/deployments/{{deployment_id}}/instances/{{instance_id}}/redeploy?openstack_project_id={{openstack_project_local_id}}
body: json
auth: bearer
}

auth:bearer {
token: {{access_token}}
}

params:query {
openstack_project_id: {{openstack_project_local_id}}
}

params:path {
deployment_id:
instance_id:
}

body:json {
{
"deployment_parameter_overrides": {
"include_example_notebooks": true,
"flavor": "gp1.medium"
},
"preserve_credentials": false
}
}

docs {
# Redeploy Instance (single VM)

Destroy-and-recreate **exactly one VM** (DeploymentInstance) inside an
existing deployment.

Use this when one VM is wedged, or to apply a config change to a single
group without touching its siblings. The parent deployment stays in
`RUNNING` for the duration — only the targeted instance flips to
`REDEPLOYING`.

## Authorization
- **Owner (Lecturer)** or **Admin** only
- Lecturers can only redeploy instances of their own deployments
- Admins can redeploy any instance

## Body

All fields are optional. Empty body = redeploy with the deployment's stored
parameters unchanged and fresh credentials.

| Field | Type | Description |
|----------------------------------|--------------------------|-------------|
| `deployment_parameter_overrides` | `dict[str, Any]` | Merged ON TOP of the deployment's stored parameters for this VM. Since there's only one VM in scope, treat this as the full override map for this instance. |
| `preserve_credentials` | `bool` (default `false`) | Keep the existing access rows (passwords / SSH keys / activation links) on the freshly-created instance instead of regenerating them. |

> **Note:** The `instance_parameter_overrides` field accepted by the
> deployment-wide endpoint is **ignored here**. Pass per-VM parameters
> directly in `deployment_parameter_overrides`.

## Process

1. API validates permissions, deployment state, and instance existence.
2. Celery task `redeploy_instance` is enqueued.
3. Task flips instance status to `REDEPLOYING`.
4. If `preserve_credentials=true`: snapshots the current access rows.
5. Old Heat stack deleted, old `DeploymentInstance` + access rows wiped.
6. New Heat stack created with merged parameters (same `-s<idx>-<dep4>` slug
so OpenStack tags stay consistent).
7. Ansible runs; credentials are persisted (or restored from snapshot).
8. The deployment's `openstack_stack_id` JSON array is rewritten so the old
ID is dropped and the new one is added — a future DELETE walks the right
list.

## Response

Returns 202 Accepted; the actual redeploy runs asynchronously.

## Example Response

```json
{
"success": true,
"data": {
"deployment_id": "deploy-123",
"instance_id": "inst-A",
"status": "redeploy_queued",
"preserve_credentials": false
},
"message": "Redeploy requested for instance; operation in progress",
"request_id": "req-456"
}
```

## Error Responses

**404 Not Found** (deployment missing)
```json
{"success": false, "message": "Deployment with ID xyz not found"}
```

**404 Not Found** (instance missing or belongs to a different deployment)
```json
{"success": false, "message": "Instance inst-A not found in deployment deploy-123"}
```

**403 Forbidden** (lecturer not the owner)
```json
{"success": false, "message": "You do not have permission to access this deployment"}
```

**400 Bad Request** (deployment in transitional state)
```json
{"success": false, "message": "Cannot redeploy instance while deployment is in deleting state."}
```

**500 Internal Server Error** (task enqueue failure)
```json
{"success": false, "message": "Failed to enqueue redeploy task"}
```

## Tracking Progress

- Watch this single instance's `.status` (REDEPLOYING → CREATING → RUNNING|FAILED).
- Live log stream: `GET /deployments/{id}/logs/stream`.
- Sibling VMs remain reachable throughout — no class-wide outage.

## Caveats

- The instance lookup uses the row's stored `vm_name` (`-s<idx>-<dep4>` suffix)
to recover which `stack_assignment` produced it. Manually-renamed VMs may
fail with `Cannot recover stack_assignment for instance ...`.
- `preserve_credentials=true` only restores credentials present in
`DeploymentInstanceAccess`. Passwords generated ad-hoc inside Ansible
playbooks (no credential spec entry) are still regenerated.
}
Loading
Loading