Skip to content

Commit a3d5d14

Browse files
authored
feat(instance): add support set and get user_data (scaleway#827)
1 parent f96b37e commit a3d5d14

File tree

6 files changed

+285
-6
lines changed

6 files changed

+285
-6
lines changed

.github/workflows/checks.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ jobs:
104104
# - name: Set up Python
105105
# uses: actions/setup-python@v5
106106
# with:
107-
# python-version: 3.8
107+
# python-version: "3.10"
108108
# - name: Install poetry
109109
# run: |
110110
# pip install poetry

scaleway-core/scaleway_core/api.py

+10-5
Original file line numberDiff line numberDiff line change
@@ -122,10 +122,14 @@ def _request(
122122
if method == "POST" or method == "PUT" or method == "PATCH":
123123
additional_headers["Content-Type"] = "application/json; charset=utf-8"
124124

125-
if body is None:
126-
body = {}
125+
if body is None:
126+
body = {}
127127

128-
raw_body = json.dumps(body) if body is not None else None
128+
raw_body: Union[bytes, str]
129+
if isinstance(body, bytes):
130+
raw_body = body
131+
else:
132+
raw_body = json.dumps(body) if body is not None else None
129133

130134
request_params: List[Tuple[str, Any]] = []
131135
for k, v in params.items():
@@ -155,9 +159,10 @@ def _request(
155159
url=url,
156160
params=request_params,
157161
headers=headers,
158-
body=raw_body,
162+
body=raw_body.decode("utf-8", errors="replace")
163+
if isinstance(raw_body, bytes)
164+
else raw_body,
159165
)
160-
161166
response = requests.request(
162167
method=method,
163168
url=url,
+130
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
from typing import Optional, Dict
2+
3+
from requests import Response
4+
5+
from scaleway_core.bridge import Zone as ScwZone
6+
from scaleway_core.utils import validate_path_param
7+
from .api import InstanceV1API
8+
from .custom_marshalling import marshal_GetServerUserDataRequest
9+
from .custom_types import GetServerUserDataRequest, GetAllServerUserDataResponse
10+
11+
12+
class InstanceUtilsV1API(InstanceV1API):
13+
"""
14+
This API extends InstanceV1API by adding utility methods for managing Instance resources,
15+
such as getting and setting server user data, while inheriting all methods of InstanceV1API.
16+
"""
17+
18+
def get_server_user_data(
19+
self, server_id: str, key: str, zone: Optional[ScwZone] = None
20+
) -> Response:
21+
"""
22+
GetServerUserData gets the content of a user data on a server for the given key.
23+
:param zone: Zone to target. If none is passed will use default zone from the config.
24+
:param server_id:
25+
:param key:
26+
:return: A plain text response with data user information
27+
28+
Usage:
29+
::
30+
31+
result = api.get_server_user_data(
32+
server_id="example",
33+
key="example",
34+
)
35+
"""
36+
param_zone = validate_path_param("zone", zone or self.client.default_zone)
37+
param_server_id = validate_path_param("server_id", server_id)
38+
39+
res = self._request(
40+
"GET",
41+
f"/instance/v1/zones/{param_zone}/servers/{param_server_id}/user_data/{key}",
42+
body=marshal_GetServerUserDataRequest(
43+
GetServerUserDataRequest(
44+
zone=zone,
45+
server_id=server_id,
46+
key=key,
47+
),
48+
self.client,
49+
),
50+
)
51+
self._throw_on_error(res)
52+
return res
53+
54+
def set_server_user_data(
55+
self, server_id: str, key: str, content: bytes, zone: Optional[ScwZone] = None
56+
) -> Response:
57+
"""
58+
Sets the content of a user data on a server for the given key.
59+
:param zone: Zone to target. If none is passed, it will use the default zone from the config.
60+
:param server_id: The ID of the server.
61+
:param key: The user data key.
62+
:param content: The content to set as user data in bytes.
63+
:return: A plain text response confirming the operation.
64+
"""
65+
param_zone = validate_path_param("zone", zone or self.client.default_zone)
66+
param_server_id = validate_path_param("server_id", server_id)
67+
headers = {
68+
"Content-Type": "text/plain",
69+
}
70+
res = self._request(
71+
"PATCH",
72+
f"/instance/v1/zones/{param_zone}/servers/{param_server_id}/user_data/{key}",
73+
body=content,
74+
headers=headers,
75+
)
76+
77+
self._throw_on_error(res)
78+
return res
79+
80+
def get_all_server_user_data(
81+
self, server_id: str, zone: Optional[ScwZone] = None
82+
) -> GetAllServerUserDataResponse:
83+
param_zone = validate_path_param("zone", zone or self.client.default_zone)
84+
param_server_id = validate_path_param("server_id", server_id)
85+
86+
all_user_data_res = InstanceUtilsV1API.list_server_user_data(
87+
self, server_id=param_server_id, zone=param_zone
88+
)
89+
90+
user_data: Dict[str, bytes] = {}
91+
for key in all_user_data_res.user_data:
92+
value = InstanceUtilsV1API.get_server_user_data(
93+
self, server_id=param_server_id, key=key
94+
)
95+
print("value: ", value)
96+
user_data[key] = value.content
97+
98+
res = GetAllServerUserDataResponse(user_data=user_data)
99+
100+
return res
101+
102+
def set_all_server_user_data(
103+
self,
104+
server_id: str,
105+
user_data: Dict[str, bytes],
106+
zone: Optional[ScwZone] = None,
107+
) -> Optional[None]:
108+
param_zone = validate_path_param("zone", zone or self.client.default_zone)
109+
param_server_id = validate_path_param("server_id", server_id)
110+
111+
all_user_data_res = InstanceUtilsV1API.list_server_user_data(
112+
self, server_id=param_server_id, zone=param_zone
113+
)
114+
for key in all_user_data_res.user_data:
115+
if user_data.get(key) is not None:
116+
continue
117+
InstanceUtilsV1API.delete_server_user_data(
118+
self, server_id=param_server_id, key=key
119+
)
120+
121+
for key in user_data:
122+
InstanceUtilsV1API.set_server_user_data(
123+
self,
124+
server_id=param_server_id,
125+
zone=param_zone,
126+
key=key,
127+
content=user_data[key],
128+
)
129+
130+
return None
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from typing import Dict, Any
2+
3+
from scaleway.instance.v1.custom_types import (
4+
GetServerUserDataRequest,
5+
GetAllServerUserDataRequest,
6+
)
7+
from scaleway_core.profile import ProfileDefaults
8+
9+
10+
def marshal_GetServerUserDataRequest(
11+
request: GetServerUserDataRequest, defaults: ProfileDefaults
12+
) -> Dict[str, Any]:
13+
output: Dict[str, Any] = {}
14+
15+
if request.server_id is not None:
16+
output["server_id"] = request.server_id
17+
if request.key is not None:
18+
output["key"] = request.key
19+
if request.zone is not None:
20+
output["zone"] = request.zone
21+
22+
return output
23+
24+
25+
def marshal_ListServerUserDataRequest(
26+
request: GetAllServerUserDataRequest, defaults: ProfileDefaults
27+
) -> Dict[str, Any]:
28+
output: Dict[str, Any] = {}
29+
30+
if request.server_id is not None:
31+
output["server_id"] = request.server_id
32+
if request.zone is not None:
33+
output["zone"] = request.zone
34+
35+
return output
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
from dataclasses import dataclass
2+
from typing import Optional, Dict
3+
4+
from scaleway_core.bridge import Zone as ScwZone
5+
6+
7+
@dataclass
8+
class GetServerUserDataRequest:
9+
server_id: str
10+
11+
"""
12+
Key defines the user data key to get
13+
"""
14+
key: str
15+
16+
"""
17+
Zone of the user data to get
18+
"""
19+
zone: Optional[ScwZone]
20+
21+
22+
@dataclass
23+
class GetAllServerUserDataRequest:
24+
server_id: str
25+
26+
"""
27+
Zone of the user data to get
28+
"""
29+
zone: Optional[ScwZone]
30+
31+
32+
@dataclass
33+
class GetAllServerUserDataResponse:
34+
user_data: Dict[str, bytes]
35+
36+
37+
@dataclass
38+
class SetAllServerUserDataRequest:
39+
server_id: str
40+
41+
user_data: Dict[str, bytes]
42+
43+
"""
44+
Zone of the user data to set
45+
"""
46+
zone: Optional[ScwZone]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import sys
2+
import unittest
3+
import logging
4+
from typing import Dict
5+
6+
from scaleway_core.client import Client
7+
from .custom_api import InstanceUtilsV1API
8+
9+
logger = logging.getLogger()
10+
logger.level = logging.DEBUG
11+
stream_handler = logging.StreamHandler(sys.stdout)
12+
logger.addHandler(stream_handler)
13+
14+
15+
class TestServerUserData(unittest.TestCase):
16+
def setUp(self) -> None:
17+
self.client = Client()
18+
self.instance_api = InstanceUtilsV1API(self.client, bypass_validation=True)
19+
self.server = self.instance_api._create_server(
20+
commercial_type="DEV1-S",
21+
zone="fr-par-1",
22+
image="ubuntu_jammy",
23+
name="my-server-web",
24+
volumes={},
25+
)
26+
27+
@unittest.skip("API Test is not up")
28+
def test_set_and_get_server_user_data(self) -> None:
29+
if self.server is None or self.server.server is None:
30+
self.fail("Server setup failed.")
31+
key = "first key"
32+
content = b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x10"
33+
self.instance_api.set_server_user_data(
34+
server_id=self.server.server.id, key=key, content=content
35+
)
36+
user_data = self.instance_api.get_server_user_data(
37+
server_id=self.server.server.id, key=key
38+
)
39+
self.assertIsNotNone(user_data)
40+
41+
@unittest.skip("API Test is not up")
42+
def test_set_and_get_all_user_data(self) -> None:
43+
if self.server is None or self.server.server is None:
44+
self.fail("Server setup failed.")
45+
key = "first key"
46+
content = b"content first key"
47+
key_bis = "second key"
48+
content_bis = b"test content"
49+
another_key = "third key"
50+
another_content = b"another content to test"
51+
52+
user_data: Dict[str, bytes] = {
53+
key_bis: content_bis,
54+
another_key: another_content,
55+
key: content,
56+
}
57+
self.instance_api.set_all_server_user_data(
58+
server_id=self.server.server.id, user_data=user_data
59+
)
60+
response = self.instance_api.get_all_server_user_data(
61+
server_id=self.server.server.id
62+
)
63+
self.assertIsNotNone(response)

0 commit comments

Comments
 (0)