Skip to content
Snippets Groups Projects
Unverified Commit bcc3b01e authored by Théo Zimmermann's avatar Théo Zimmermann
Browse files

Add a start all workspaces function.

parent abbd8eed
No related branches found
No related tags found
No related merge requests found
%% Cell type:markdown id:6d924ae8-e462-4c47-86d9-8560ecedf78f tags:
# Coder management
%% Cell type:code id:bb33cb2d-8256-4d65-9442-153a5eb920a6 tags:
``` python
import csv
import json
import requests
```
%% Cell type:code id:991fc1f2-61d2-443f-901b-df6acf094287 tags:
``` python
cluster_name='inf110'
proxyJump='ubuntu@137.194.210.143'
```
%% Cell type:code id:ae9a0955-2a17-499a-827b-8dfe8be56d65 tags:
``` python
token,=!source openrc && echo $CODER_TOKEN
```
%% Cell type:code id:904c792f-2229-44cb-a98b-7714af05e163 tags:
``` python
response = requests.get("https://tp-inf110.r2.enst.fr/api/v2/buildinfo")
response.raise_for_status()
buildinfo = response.json()
print(f"Version: {buildinfo['version']}")
print(f"URL: {buildinfo['dashboard_url']}")
```
%% Cell type:markdown id:463b7432-7861-44f6-8a42-36f453bf0a24 tags:
To retrieve the ID of the default organization, we can list the organizations of the admin user.
%% Cell type:code id:3d2d2fe7-c20b-4e7d-a5da-e0c8021cb6f8 tags:
``` python
user="Zimmi48"
response = requests.get(f"https://tp-inf110.r2.enst.fr/api/v2/users/{user}/organizations", headers={"Coder-Session-Token": token})
response.raise_for_status()
organizations = response.json()
print(f"Organizations: {organizations}")
organization_id = organizations[0]['id']
```
%% Cell type:markdown id:19a552b0-8bf8-4cd8-86a1-135b7e2253ed tags:
## Create workspace template
If it doesn't exist yet. Otherwise, retrieve the template and template version IDs.
%% Cell type:code id:b4db687e-3e52-448d-b4d7-8c812cdaa2f7 tags:
``` python
# Test if template exists
response = requests.get(f"https://tp-inf110.r2.enst.fr/api/v2/organizations/{organization_id}/templates", headers={"Coder-Session-Token": token})
response.raise_for_status()
templates = response.json()
if len(templates) == 1:
template_id = templates[0]['id']
template_version = templates[0]['active_version_id']
print(f"Template found: {template_id} (version {template_version})")
elif len(templates) == 0:
print("No template found. Creating one...")
# Create zip file to upload
!cd inf110-workspace && zip "template.zip" "main.tf" "README.md"
# Upload template files
with open("inf110-workspace/template.zip", "rb") as f:
response = requests.post(
"https://tp-inf110.r2.enst.fr/api/v2/files",
headers={
"Coder-Session-Token": token,
"Content-Type": "application/zip"
},
data=f
)
response.raise_for_status()
file_hash = response.json()['hash']
# Create template version
version_name='INF110-2024'
response = requests.post(
f"https://tp-inf110.r2.enst.fr/api/v2/organizations/{organization_id}/templateversions",
headers={"Coder-Session-Token": token},
json={
"name": version_name,
"file_id": file_hash,
"storage_method": "file",
"provisioner": "terraform"
}
)
response.raise_for_status()
template_version = response.json()['id']
# Create template
template_name='INF110'
display_name='TP INF110'
description="Espace de travail pour les TP d'INF110"
response = requests.post(
f"https://tp-inf110.r2.enst.fr/api/v2/organizations/{organization_id}/templates",
headers={"Coder-Session-Token": token},
json={
"name": template_name,
"display_name": display_name,
"description": description,
"icon": "/emojis/1f4d0.png",
"template_version_id": template_version
}
)
response.raise_for_status()
template_id = response.json()['id']
print(f"Template created: {template_id} (version {template_version})")
else:
raise ValueError("Multiple templates found")
```
%% Cell type:markdown id:e20dfefb-0359-421a-92c6-ab403de529dc tags:
The query below can be used to check that the template creation was successful.
%% Cell type:code id:1ae74b43-8065-4e5f-9976-e00a879d7c99 tags:
``` python
response = requests.get(f"https://tp-inf110.r2.enst.fr/api/v2/templateversions/{template_version}", headers={"Coder-Session-Token": token})
response.raise_for_status()
template_version = response.json()
print(f"Template version: {template_version['job']['status']}")
```
%% Cell type:markdown id:5d900272-a3ee-461c-9cdd-df08b17b1d2a tags:
## Create users
%% Cell type:code id:02384bfa tags:
``` python
def create_users():
with open("users.csv") as f:
reader = csv.reader(f)
for email, username, password in reader:
response = requests.post(
"https://tp-inf110.r2.enst.fr/api/v2/users",
headers={"Coder-Session-Token": token},
json={
"email": email,
"login_type": "password",
"password": password,
"username": username
}
)
response.raise_for_status()
print(f"User {username} created")
#create_users()
```
%% Cell type:code id:9f05d75f-c300-49ca-9faf-19a1a24dd6d8 tags:
``` python
def create_user(email,username):
passwords=!pwgen -s 10 -1
password=passwords[0]
!echo "{email},{username},{password}" >> users.csv && \
https POST "tp-inf110.r2.enst.fr/api/v2/users" "Coder-Session-Token:{token}" "email={email}" "login_type=password" "password={password}" "username={username}"
#create_user("", "")
```
%% Cell type:code id:96595327-81a0-4a50-b090-a79d75a3134d tags:
``` python
def create_student_accounts(csv_filename):
with open("students_2024-2025.csv") as f:
reader = csv.DictReader(f, delimiter=';')
for student in reader:
create_user(student['mail'], student['login'].split('@')[0])
#create_student_accounts('students_2024-2025.csv')
```
%% Cell type:markdown id:9868d906-ca8c-4fda-ad96-ec00bea5fadc tags:
## Delete users
(Requires having deleted their workspaces first.)
%% Cell type:code id:30a56dca-7f8f-4839-94e7-36340f5f199f tags:
``` python
def delete_user(username):
!https DELETE "tp-inf110.r2.enst.fr/api/v2/users/{username}" "Coder-Session-Token:{token}"
#delete_user("bbinder")
```
%% Cell type:markdown id:d6d20c1f-e231-488c-90ef-2daa811e89a2 tags:
## Create workspaces
%% Cell type:code id:80ff8f4e-fa5f-4308-a885-e3d382e2a171 tags:
``` python
def create_workspace(username, name='tp'):
!https POST "tp-inf110.r2.enst.fr/api/v2/organizations/{organization_id}/members/{username}/workspaces" "Coder-Session-Token:{token}" "name={name}" "template_id={template_id}"
#create_workspace("Zimmi48")
```
%% Cell type:markdown id:d2cea793-4b57-4718-bd17-7cbda610d5bd tags:
Test auto-scaling by creating many workspaces at once.
%% Cell type:code id:e3d06f0c-3183-47e4-a63e-254a6c08612a tags:
``` python
#for i in range(0,99):
# create_workspace('Zimmi48', 'autoscaling-test-%d' % i)
```
%% Cell type:code id:11d58691-691b-41a8-bdbf-51d3b313b64f tags:
``` python
def create_student_workspaces(csv_filename):
with open(csv_filename, mode='r') as csv_file:
csv_reader = csv.DictReader(csv_file)
for student in csv_reader:
create_workspace(student['login'])
#create_student_workspaces('students_2024-2025.csv')
```
%% Cell type:code id:f26e86ea tags:
``` python
def create_user_workspaces():
with open("users.csv") as f:
reader = csv.reader(f)
for _, username, _ in reader:
create_workspace(username)
#create_user_workspaces()
```
%% Cell type:markdown id:1b405071-7dd5-4082-80f2-dbd60ea46262 tags:
## Start / stop all workspaces from a student group
%% Cell type:code id:e9be7a12-c858-4baf-9ac7-53827330798a tags:
``` python
def get_student_username_list(csv_filename):
students = []
with open(csv_filename, mode='r') as csv_file:
csv_reader = csv.DictReader(csv_file)
for student in csv_reader:
students += [student['login']]
return students
#groupB = get_student_username_list('inf110-2023-B.csv')
#groupD = get_student_username_list('inf110-2023-D.csv')
```
%% Cell type:code id:1640ad41-d82c-4519-88d3-ef2c2cfc2950 tags:
``` python
len(groupB)
```
%% Cell type:code id:6b0b126a-b7e4-43c5-9aab-2c27931ebf91 tags:
``` python
len(groupD)
```
%% Cell type:code id:b495de0f-d085-4db6-a8a4-9a022f345251 tags:
``` python
def stop_workspaces_from_group(group):
workspaces = !https GET "tp-inf110.r2.enst.fr/api/v2/workspaces" "Coder-Session-Token:{token}" "limit==150" "q==status:running" -b | jq -cr '.workspaces | .[]'
workspaces = list(map(json.loads, workspaces))
for workspace in [workspace['id'] for workspace in workspaces if workspace['owner_name'] in group]:
!https POST "tp-inf110.r2.enst.fr/api/v2/workspaces/{workspace}/builds" "Coder-Session-Token:{token}" "transition=stop"
stop_workspaces_from_group(['Zimmi48'])
```
%% Cell type:code id:13f05a6e-f591-4bcd-9857-63f5a21f1028 tags:
``` python
def start_workspaces_from_group(group):
workspaces = !https GET "tp-inf110.r2.enst.fr/api/v2/workspaces" "Coder-Session-Token:{token}" "limit==150" "q==status:stopped" -b | jq -cr '.workspaces | .[]'
workspaces = list(map(json.loads, workspaces))
for workspace in [workspace['id'] for workspace in workspaces if workspace['owner_name'] in group]:
!https POST "tp-inf110.r2.enst.fr/api/v2/workspaces/{workspace}/builds" "Coder-Session-Token:{token}" "transition=start" "template_version_id={template_version}"
start_workspaces_from_group(mitro)
```
%% Cell type:code id:fd82a694-ba4b-4dba-aab1-08d1d606f625 tags:
``` python
def start_all_workspaces():
workspaces = !https GET "tp-inf110.r2.enst.fr/api/v2/workspaces" "Coder-Session-Token:{token}" "limit==150" "q==status:stopped" -b | jq -cr '.workspaces | .[]'
workspaces = list(map(json.loads, workspaces))
for workspace in [workspace['id'] for workspace in workspaces]:
!https POST "tp-inf110.r2.enst.fr/api/v2/workspaces/{workspace}/builds" "Coder-Session-Token:{token}" "transition=start" "template_version_id={template_version}"
start_all_workspaces()
```
%% Cell type:markdown id:e8e88940-8c7c-4b3c-8923-aaf0b813f524 tags:
## Delete all workspaces and users from a student group
%% Cell type:code id:5c22d1b3-923f-45fd-ac04-9f404e5002b6 tags:
``` python
def delete_workspaces_from_group(group):
workspaces = !https GET "tp-inf110.r2.enst.fr/api/v2/workspaces" "Coder-Session-Token:{token}" "limit==150" "q==status:stopped" -b | jq -cr '.workspaces | .[]'
workspaces = list(map(json.loads, workspaces))
for workspace in [workspace['id'] for workspace in workspaces if workspace['owner_name'] in group]:
!https POST "tp-inf110.r2.enst.fr/api/v2/workspaces/{workspace}/builds" "Coder-Session-Token:{token}" "transition=delete"
delete_workspaces_from_group(['Zimmi48'])
```
%% Cell type:code id:eafaba21-fdf9-46e9-ba8f-bc1f25de96a4 tags:
``` python
def delete_students(group):
for student in group:
delete_user(student)
delete_students(groupD)
```
%% Cell type:markdown id:12c57f21-23fe-4752-ad28-b05bd33ff08f tags:
## Stop all unhealthy workspaces
%% Cell type:code id:73ce34f9-b0ad-4434-b448-6e2fdcfed6bd tags:
``` python
def stop_unhealthy_workspaces():
workspaces = !https GET "tp-inf110.r2.enst.fr/api/v2/workspaces" "Coder-Session-Token:{token}" "limit==150" "q==status:running" -b | jq -cr '.workspaces | .[]'
workspaces = list(map(json.loads, workspaces))
for workspace in [workspace for workspace in workspaces if not workspace['health']['healthy']]:
!https POST "tp-inf110.r2.enst.fr/api/v2/workspaces/{workspace['id']}/builds" "Coder-Session-Token:{token}" "transition=stop"
stop_unhealthy_workspaces()
```
%% Cell type:markdown id:acd9cb0e-9596-4091-ae35-82a34ff9f0b4 tags:
## Check health of running workspaces
%% Cell type:code id:6c4beb11-717a-4d89-b9a4-e9b033b3e6a4 tags:
``` python
servers = {}
```
%% Cell type:code id:9bcdc633-0b28-46e4-aa54-97dc20f7d091 tags:
``` python
healthy_workspaces = []
unhealthy_workspaces = []
workspaces = !https GET "tp-inf110.r2.enst.fr/api/v2/workspaces" "Coder-Session-Token:{token}" "limit==150" "q==status:running" -b | jq -cr '.workspaces | .[]'
workspaces = list(map(json.loads, workspaces))
for workspace in workspaces:
if workspace['health']['healthy']:
healthy_workspaces += [workspace['owner_name'].lower()]
else:
unhealthy_workspaces += [workspace['owner_name'].lower()]
```
%% Cell type:code id:ffb92e49-aa8b-4371-bade-a06104020e32 tags:
``` python
print("Healthy workspaces: %d" % len(healthy_workspaces))
print("Unhealthy workspaces: %d" % len(unhealthy_workspaces))
```
%% Cell type:code id:08d53bc7-fd70-4a7f-97d3-be5e737cdbaa tags:
``` python
volumes=!source openrc && openstack volume list --long -f json | jq -c '.[] | { id: .Name, name: .Properties."csi.storage.k8s.io/pvc/name", status: .Status, location: .Name, attached_to: ."Attached to" } | select(.name != null) | select(.name | startswith("coder-pvc-") and contains("-tp-inf110")) | { name: .id, username: .name | split("coder-pvc-") | .[1] | split("-tp-inf110") | .[0], status: .status, server_id: .attached_to[0].server_id, location: ("/var/lib/kubelet/plugins/kubernetes.io/csi/pv/" + .location + "/globalmount") }'
volumes = list(map(json.loads, volumes))
```
%% Cell type:code id:c183542f-6abf-4fbe-8b64-386da87a2c95 tags:
``` python
[volume for volume in volumes if volume['server_id'] is not None and not volume['username'] in healthy_workspaces]
```
%% Cell type:code id:db003235-11d9-4386-b8fe-944955e5e257 tags:
``` python
for server in servers:
servers[server]['users'] = []
for volume in volumes:
if volume['server_id'] is not None:
if not volume['server_id'] in servers:
ip,=!source openrc && openstack server show "{volume['server_id']}" -f json | jq -r '.addresses."{cluster_name}"[0]'
servers[volume['server_id']] = { 'ip': ip, 'users': [volume['username']] }
else:
servers[volume['server_id']]['users'] += [volume['username']]
```
%% Cell type:code id:8cee7a31-4e22-4ff4-9b06-5c6b014ddea8 tags:
``` python
for server in servers:
print("Server: " + servers[server]['ip'])
for user in servers[server]['users']:
print('User: ' + user, end='')
if user in healthy_workspaces:
print(' (healthy)')
elif user in unhealthy_workspaces:
print(' (unhealthy)')
else:
print(' (unknown)')
print()
print()
```
%% Cell type:markdown id:79725809-695a-4ab4-9e58-79bb747b8b05 tags:
## Clean workspaces
%% Cell type:markdown id:a5032d9b-aff8-459d-91fb-1b49709c232b tags:
Note that the `StrictHostKeyChecking=no` below does not apply to the proxy jump. Make sure you connected at least once to the proxy jump before running the following cells.
%% Cell type:code id:1a29e39d-2abb-4b2a-b663-3455f956b46d tags:
``` python
def remove_lost_found():
for volume in volumes:
if volume['server_id'] is not None:
machine_ip = servers[volume['server_id']]['ip']
lost_found = volume['location'] + "/lost+found"
command = "set -xe; "
command += "if [ -d " + lost_found + " ]; then rmdir -v " + lost_found + "; else echo 'No lost+found to remove'; fi; "
!ssh -o StrictHostKeyChecking=no -J {proxyJump} core@{machine_ip} sudo sh -c "\"{command}\""
remove_lost_found()
```
%% Cell type:markdown id:75661986-3bb2-42b0-abe6-1a5f18ea9615 tags:
## Copy / update files in workspace
%% Cell type:code id:13d7c970-4863-4fac-bb58-722bb9c27f34 tags:
``` python
past_versions = {
'tp1.ipynb': [
'e6f11284e2ba7d4a01350a1f2c0ab208',
'c208201a6aaf72f102fbd2cf46b62e5b',
],
'tp2.mv': [
'b3b2a3e7a8dde6ed8848300158d89e5d',
'60a26b6a7d07579f63f6054c24b87ea2',
'190845731e26037dcbc7e3a74d239b1b',
'd010ae9ab0188b4cc77a3d5a45df2f1a',
],
'tp3.mv': [
'91621d47a84e460e567dc51dfe1fcf45',
],
'tp4.mv': [
'a819a3d31373750cfcb8473a27cd5c3f',
],
'tp5.mv': [],
'settings.json': [
'aca81e70023acea605dd44e829d04339', # Autogenerated by coq-lsp
'7378b13908523bfde3692518f61939cf',
'2c2178abd6661d10ae492bf6fdc0454b',
'350ac51d8fea466958d9986c96c84842',
'ded5f984f385aa61ef30e01fef43f3e4',
'8f3c325edd4f635fdaee9175701216df',
],
"_CoqProject": [],
}
location = "inf110-workspace-contents/"
```
%% Cell type:code id:876c1a46-9dd4-4f4c-8a4e-185dc361d5a1 tags:
``` python
current_version = {
'tp1.ipynb': '009c42561f1853af966f95e9dcee9c24',
'tp2.mv': '78139922e6b6a531e7e70acc98c1019c',
'tp3.mv': '0a0253e4825146da6f08eff073b10444',
'tp4.mv': '203ce16afa53d60cfd8390fb3fb7d898',
'tp5.mv': '3ec8626aa544cb075af1f78719a1fb48',
'settings.json': '3ddc1036917034cec13503b74d3b051e',
'_CoqProject': 'b1d60da8dd98106e00006d99428d1403',
}
```
%% Cell type:code id:a8f0c5e5-f995-43bd-9cb8-08099e6ea8fa tags:
``` python
def copy_update_file(filename):
file_hash, = !md5sum {location + filename} | cut -f1 -d' '
if file_hash != current_version[filename]:
print("Hash of " + filename + " has changed. Please update past_versions and current_version. New hash is " + file_hash)
return
for server in servers:
!scp -o StrictHostKeyChecking=no -J {proxyJump} {location + filename} core@{servers[server]['ip']}:/tmp
for volume in volumes:
if volume['server_id'] is not None:
machine_ip = servers[volume['server_id']]['ip']
!ssh -J {proxyJump} core@{machine_ip} sudo cp -nv /tmp/{filename} {volume['location']}
file_hash, = !ssh -J {proxyJump} core@{machine_ip} sudo md5sum {volume['location'] + "/" + filename} | cut -f1 -d' '
if file_hash in past_versions[filename]:
!ssh -J {proxyJump} core@{machine_ip} sudo cp -v /tmp/{filename} {volume['location']}
!ssh -J {proxyJump} core@{machine_ip} sudo chmod go+rw {volume['location'] + "/" + filename}
copy_update_file("tp1.ipynb")
copy_update_file("_CoqProject")
copy_update_file("tp2.mv")
copy_update_file("tp3.mv")
copy_update_file("tp4.mv")
copy_update_file("tp5.mv")
```
%% Cell type:code id:13608dc8-78f6-41d3-9cc3-f96c603a9437 tags:
``` python
def copy_read_only_file(filename):
for server in servers:
!scp -J {proxyJump} {location + filename} core@{servers[server]['ip']}:/tmp
for volume in volumes:
if volume['server_id'] is not None:
machine_ip = servers[volume['server_id']]['ip']
!ssh -J {proxyJump} core@{machine_ip} sudo cp -v /tmp/{filename} {volume['location']}
copy_read_only_file('README.md')
copy_read_only_file("tp1-mysterious-tm.png")
```
%% Cell type:code id:4cc929db-08fe-4b7b-9293-d86b7124f7cf tags:
``` python
def copy_vscode_settings():
file_hash, = !md5sum {location + "settings.json"} | cut -f1 -d' '
if file_hash != current_version['settings.json']:
print("Hash of settings.json has changed. Please update past_versions and current_version. New hash is " + file_hash)
return
for server in servers:
!scp -J {proxyJump} -o StrictHostKeyChecking=no {location + "settings.json"} core@{servers[server]['ip']}:/tmp
for volume in volumes:
if volume['server_id'] is not None:
machine_ip = servers[volume['server_id']]['ip']
vscode = volume['location'] + "/.vscode"
command = "set -xe; "
command += "if [ ! -d " + vscode + " ]; then mkdir " + vscode + "; fi; "
command += "cp -nv /tmp/settings.json " + vscode + "/settings.json; "
!ssh -J {proxyJump} core@{machine_ip} sudo sh -c "\"{command}\""
file_hash, = !ssh -J {proxyJump} core@{machine_ip} sudo md5sum {vscode + "/settings.json"} | cut -f1 -d' '
if file_hash in past_versions['settings.json']:
!ssh -J {proxyJump} core@{machine_ip} sudo cp -v /tmp/settings.json {vscode}
!ssh -J {proxyJump} core@{machine_ip} sudo chmod go+rw {vscode + "/settings.json"}
copy_vscode_settings()
```
%% Cell type:markdown id:55bd39c3-794e-43f2-8bf0-342361d61b64 tags:
## Save snapshots of modified files
%% Cell type:code id:db436f12-abb7-4b48-9e66-2f6ada19bdff tags:
``` python
def snapshot(filename, snapshot_name):
!mkdir -p {snapshot_name}
for volume in volumes:
if volume['server_id'] is not None:
machine_ip = servers[volume['server_id']]['ip']
file_hash, = !ssh -o StrictHostKeyChecking=no -J {proxyJump} core@{machine_ip} sudo md5sum {volume['location'] + "/" + filename} | cut -f1 -d' '
if not file_hash == current_version[filename] and not file_hash in past_versions[filename]:
snapshot_filename = "/tmp/" + volume['username'] + "-" + snapshot_name + "-" + filename
!ssh -J {proxyJump} core@{machine_ip} sudo cp {volume['location'] + "/" + filename} {snapshot_filename}
!scp -J {proxyJump} core@{machine_ip}:{snapshot_filename} {snapshot_name + "/" + volume['username'] + "_" + filename}
#snapshot('tp1.ipynb', '2023-12-04-post-tp')
#snapshot('tp2.mv', '2023-12-13-post-tp')
#snapshot('tp3.mv', '2023-12-13-post-tp')
#snapshot('tp4.mv', '2024-01-12-post-tp')
#snapshot('tp5.mv', '2024-01-22-post-tp')
```
%% Cell type:code id:35af9e82-e79c-4ad9-a843-3ebaab9f4d62 tags:
``` python
```
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment