Skip to content

Commit 446be04

Browse files
committed
Resolve "Implement client for SSHKey endpoint"
1 parent d582373 commit 446be04

15 files changed

Lines changed: 590 additions & 12 deletions

File tree

hcloud/hcloud.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from hcloud.isos.client import IsosClient
88
from hcloud.servers.client import ServersClient
99
from hcloud.server_types.client import ServerTypesClient
10+
from hcloud.ssh_keys.client import SSHKeysClient
1011
from hcloud.volumes.client import VolumesClient
1112
from hcloud.images.client import ImagesClient
1213
from hcloud.locations.client import LocationsClient
@@ -37,6 +38,7 @@ def __init__(self, token):
3738
self.actions = ActionsClient(self)
3839
self.images = ImagesClient(self)
3940
self.isos = IsosClient(self)
41+
self.ssh_keys = SSHKeysClient(self)
4042

4143
def _get_user_agent(self):
4244
return "hcloud-python/" + self.version

hcloud/servers/client.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ def create(self,
146146
name, # type: str
147147
server_type, # type: str
148148
image, # type: Image
149-
ssh_keys=None, # type: Optional[List[str]]
149+
ssh_keys=None, # type: Optional[List[SSHKey]]
150150
volumes=None, # type: Optional[List[Volume]]
151151
user_data=None, # type: Optional[str]
152152
labels=None, # type: Optional[Dict[str, str]]
@@ -184,7 +184,7 @@ def create(self,
184184
data['datacenter'] = datacenter.id_or_name
185185

186186
if ssh_keys is not None:
187-
data['ssh_keys'] = ssh_keys
187+
data['ssh_keys'] = [str(ssh_key.id_or_name) for ssh_key in ssh_keys]
188188
if volumes is not None:
189189
data['volumes'] = [str(volume.id) for volume in volumes]
190190
if user_data is not None:

hcloud/ssh_keys/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# -*- coding: utf-8 -*-

hcloud/ssh_keys/client.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# -*- coding: utf-8 -*-
2+
from hcloud.core.client import ClientEntityBase, BoundModelBase
3+
4+
from hcloud.ssh_keys.domain import SSHKey
5+
6+
7+
class BoundSSHKey(BoundModelBase):
8+
model = SSHKey
9+
10+
def update(self, name=None, labels=None):
11+
# type: (Optional[str], Optional[Dict[str, str]]) -> BoundSSHKey
12+
return self._client.update(self, name, labels)
13+
14+
def delete(self):
15+
# type: () -> bool
16+
return self._client.delete(self)
17+
18+
19+
class SSHKeysClient(ClientEntityBase):
20+
21+
def get_by_id(self, id):
22+
# type: (int) -> BoundSSHKey
23+
response = self._client.request(url="/ssh_keys/{ssh_key_id}".format(ssh_key_id=id), method="GET")
24+
return BoundSSHKey(self, response['ssh_key'])
25+
26+
def get_all(self, name=None, fingerprint=None, label_selector=None):
27+
# type: (Optional[str], Optional[str], Optional[str]) -> List[BoundSSHKey]
28+
params = {}
29+
if name is not None:
30+
params['name'] = name
31+
if fingerprint is not None:
32+
params['fingerprint'] = fingerprint
33+
if label_selector is not None:
34+
params['label_selector'] = label_selector
35+
36+
response = self._client.request(url="/ssh_keys", method="GET", params=params)
37+
return [BoundSSHKey(self, location_data) for location_data in response['ssh_keys']]
38+
39+
def create(self, name, public_key, labels=None):
40+
# type: (str, str, Optional[Dict[str, str]]) -> BoundSSHKey
41+
data = {
42+
'name': name,
43+
'public_key': public_key
44+
}
45+
if labels is not None:
46+
data['labels'] = labels
47+
response = self._client.request(url="/ssh_keys", method="POST", json=data)
48+
return BoundSSHKey(self, response['ssh_key'])
49+
50+
def update(self, ssh_key, name=None, labels=None):
51+
# type: (SSHKey, Optional[str], Optional[Dict[str, str]]) -> BoundSSHKey
52+
data = {}
53+
if name is not None:
54+
data['name'] = name
55+
if labels is not None:
56+
data['labels'] = labels
57+
response = self._client.request(url="/ssh_keys/{ssh_key_id}".format(ssh_key_id=ssh_key.id), method="PUT", json=data)
58+
return BoundSSHKey(self, response['ssh_key'])
59+
60+
def delete(self, ssh_key):
61+
# type: (SSHKey) -> bool
62+
self._client.request(url="/ssh_keys/{ssh_key_id}".format(ssh_key_id=ssh_key.id), method="DELETE")
63+
# Return allays true, because the API does not return an action for it. When an error occurs a HcloudAPIException will be raised
64+
return True

hcloud/ssh_keys/domain.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# -*- coding: utf-8 -*-
2+
from hcloud.core.domain import BaseDomain
3+
4+
5+
class SSHKey(BaseDomain):
6+
7+
__slots__ = (
8+
"id",
9+
"name",
10+
"fingerprint",
11+
"public_key",
12+
"labels"
13+
)
14+
15+
def __init__(
16+
self,
17+
id=None,
18+
name=None,
19+
fingerprint=None,
20+
public_key=None,
21+
labels=None
22+
):
23+
self.id = id
24+
self.name = name
25+
self.fingerprint = fingerprint
26+
self.public_key = public_key
27+
self.labels = labels

hcloud/volumes/client.py

Lines changed: 70 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,34 @@ def __init__(self, client, data, complete=True):
1515
data['location'] = BoundLocation(client._client.locations, location)
1616
super(BoundVolume, self).__init__(client, data, complete)
1717

18-
def attach(self, server):
19-
# type: (Union[Server, BoundServer]) -> Action
20-
return self._client.attach(server, self)
18+
def get_actions(self, sort=None):
19+
# type: (Optional[List[str]]) -> List[BoundAction]
20+
return self._client.get_actions(self, sort)
21+
22+
def update(self, name=None, labels=None):
23+
# type: (Optional[str], Optional[Dict[str, str]]) -> BoundAction
24+
return self._client.update(self, name, labels)
25+
26+
def delete(self):
27+
# type: () -> BoundAction
28+
return self._client.delete(self)
29+
30+
def attach(self, server, automount=None):
31+
# type: (Union[Server, BoundServer]) -> BoundAction
32+
return self._client.attach(self, server, automount)
2133

2234
def detach(self):
2335
# type: () -> BoundAction
2436
return self._client.detach(self)
2537

38+
def resize(self, size):
39+
# type: (int) -> BoundAction
40+
return self._client.resize(self, size)
41+
42+
def change_protection(self, delete=None):
43+
# type: (Optional[bool]) -> BoundAction
44+
return self._client.change_protection(self, delete)
45+
2646

2747
class VolumesClient(ClientEntityBase):
2848

@@ -82,12 +102,55 @@ def create(self,
82102
)
83103
return result
84104

85-
def attach(self, server, volume):
86-
# type: (Union[Server, BoundServer], Union[Volume, BoundVolume]) -> Action
87-
data = self._client.request(url="/volumes/{volume_id}/actions/attach".format(volume_id=volume.id), json={'server': server.id}, method="POST")
105+
def get_actions(self, volume, sort=None):
106+
# type: (Union[Volume, BoundVolume], Optional[List[str]]) -> List[BoundAction]
107+
params = {}
108+
109+
if sort is not None:
110+
params.update({"sort": sort})
111+
response = self._client.request(url="/volumes/{volume_id}/actions".format(volume_id=volume.id), method="GET", params=params)
112+
return [BoundAction(self._client.actions, action_data) for action_data in response['actions']]
113+
114+
def update(self, volume, name=None, labels=None):
115+
# type:(Union[Volume, BoundVolume], Optional[str], Optional[Dict[str, str]]) -> BoundVolume
116+
data = {}
117+
if name is not None:
118+
data.update({"name": name})
119+
if labels is not None:
120+
data.update({"labels": labels})
121+
response = self._client.request(url="/volumes/{volume_id}".format(volume_id=volume.id), method="PUT", json=data)
122+
return BoundVolume(self, response['volume'])
123+
124+
def delete(self, volume):
125+
# type: (Union[Volume, BoundVolume]) -> BoundAction
126+
self._client.request(url="/volumes/{volume_id}".format(volume_id=volume.id), method="DELETE")
127+
return True
128+
129+
def resize(self, volume, size):
130+
# type: (Union[Volume, BoundVolume], int) -> BoundAction
131+
data = self._client.request(url="/volumes/{volume_id}/actions/resize".format(volume_id=volume.id), json={'size': size}, method="POST")
132+
return BoundAction(self._client.actions, data['action'])
133+
134+
def attach(self, volume, server, automount=None):
135+
# type: (Union[Volume, BoundVolume], Union[Server, BoundServer], Optional[bool]) -> BoundAction
136+
data = {'server': server.id}
137+
if automount is not None:
138+
data["automount"] = automount
139+
140+
data = self._client.request(url="/volumes/{volume_id}/actions/attach".format(volume_id=volume.id), json=data, method="POST")
88141
return BoundAction(self._client.actions, data['action'])
89142

90143
def detach(self, volume):
91-
# type: (Union[Volume, BoundVolume]) -> Action
144+
# type: (Union[Volume, BoundVolume]) -> BoundAction
92145
data = self._client.request(url="/volumes/{volume_id}/actions/detach".format(volume_id=volume.id), method="POST")
93146
return BoundAction(self._client.actions, data['action'])
147+
148+
def change_protection(self, volume, delete=None):
149+
# type: (Union[Volume, BoundVolume], Optional[bool], Optional[bool]) -> BoundAction
150+
data = {}
151+
if delete is not None:
152+
data.update({"delete": delete})
153+
154+
response = self._client.request(url="/volumes/{volume_id}/actions/change_protection".format(volume_id=volume.id),
155+
method="POST", json=data)
156+
return BoundAction(self._client.actions, response['action'])

tests/integration/servers/test_servers.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from hcloud.servers.client import BoundServer
55
from hcloud.servers.domain import Server
6+
from hcloud.ssh_keys.domain import SSHKey
67
from hcloud.volumes.domain import Volume
78
from hcloud.images.domain import Image
89
from hcloud.isos.domain import Iso
@@ -163,7 +164,7 @@ def test_create(self, hetzner_client):
163164
"my-server",
164165
"cx11",
165166
image=Image(name="ubuntu-16.04"),
166-
ssh_keys=["my-ssh-key"],
167+
ssh_keys=[SSHKey(name="my-ssh-key")],
167168
volumes=[Volume(id=1)],
168169
user_data="#cloud-config\\nruncmd:\\n- [touch, /root/cloud-init-worked]\\n",
169170
location=Location(name="nbg1"),

tests/integration/ssh_keys/__init__.py

Whitespace-only changes.
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import mock
2+
import pytest
3+
4+
from hcloud.ssh_keys.client import BoundSSHKey
5+
from hcloud.ssh_keys.domain import SSHKey
6+
7+
8+
class TestBoundSSHKey(object):
9+
@pytest.fixture()
10+
def bound_ssh_key(self, hetzner_client):
11+
return BoundSSHKey(client=hetzner_client.ssh_keys, data=dict(id=42))
12+
13+
def test_update(self, bound_ssh_key):
14+
ssh_key = bound_ssh_key.update(name="New name")
15+
16+
assert ssh_key.id == 2323
17+
assert ssh_key.name == "New name"
18+
19+
def test_delete(self, bound_ssh_key):
20+
delete_success = bound_ssh_key.delete()
21+
22+
assert delete_success is True
23+
24+
25+
class TestSSHKeysClient(object):
26+
def test_get_by_id(self, hetzner_client):
27+
ssh_key = hetzner_client.ssh_keys.get_by_id(1)
28+
assert ssh_key.id == 2323
29+
assert ssh_key.name == "My ssh key"
30+
assert ssh_key.fingerprint == "b7:2f:30:a0:2f:6c:58:6c:21:04:58:61:ba:06:3b:2f"
31+
assert ssh_key.public_key == "ssh-rsa AAAjjk76kgf...Xt"
32+
33+
def test_get_all(self, hetzner_client):
34+
ssh_keys = hetzner_client.ssh_keys.get_all()
35+
assert ssh_keys[0].id == 2323
36+
assert ssh_keys[0].name == "My ssh key"
37+
assert ssh_keys[0].fingerprint == "b7:2f:30:a0:2f:6c:58:6c:21:04:58:61:ba:06:3b:2f"
38+
assert ssh_keys[0].public_key == "ssh-rsa AAAjjk76kgf...Xt"
39+
40+
def test_create(self, hetzner_client):
41+
ssh_key = hetzner_client.ssh_keys.create(name="My ssh key", public_key="ssh-rsa AAAjjk76kgf...Xt")
42+
43+
assert ssh_key.id == 2323
44+
assert ssh_key.name == "My ssh key"
45+
46+
@pytest.mark.parametrize("ssh_key", [SSHKey(id=1), BoundSSHKey(mock.MagicMock(), dict(id=1))])
47+
def test_update(self, hetzner_client, ssh_key):
48+
ssh_key = hetzner_client.ssh_keys.update(ssh_key, name="New name")
49+
50+
assert ssh_key.id == 2323
51+
assert ssh_key.name == "New name"
52+
53+
@pytest.mark.parametrize("ssh_key", [SSHKey(id=1), BoundSSHKey(mock.MagicMock(), dict(id=1))])
54+
def test_delete(self, hetzner_client, ssh_key):
55+
delete_success = hetzner_client.ssh_keys.delete(ssh_key)
56+
57+
assert delete_success is True

tests/integration/volumes/test_volumes.py

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,22 @@ class TestBoundVolume(object):
1414
def bound_volume(self, hetzner_client):
1515
return BoundVolume(client=hetzner_client.volumes, data=dict(id=4711))
1616

17+
def test_get_actions(self, bound_volume):
18+
actions = bound_volume.get_actions()
19+
20+
assert len(actions) == 1
21+
assert actions[0].id == 13
22+
assert actions[0].command == "attach_volume"
23+
24+
def test_update(self, bound_volume):
25+
volume = bound_volume.update(name="new-name", labels={})
26+
assert volume.id == 4711
27+
assert volume.name == "new-name"
28+
29+
def test_delete(self, bound_volume):
30+
delete_success = bound_volume.delete()
31+
assert delete_success is True
32+
1733
@pytest.mark.parametrize("server",
1834
(Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))))
1935
def test_attach(self, hetzner_client, bound_volume, server):
@@ -28,6 +44,12 @@ def test_detach(self, hetzner_client, bound_volume):
2844
assert action.progress == 0
2945
assert action.command == "detach_volume"
3046

47+
def test_resize(self, hetzner_client, bound_volume):
48+
action = bound_volume.resize(50)
49+
assert action.id == 13
50+
assert action.progress == 0
51+
assert action.command == "resize_volume"
52+
3153

3254
class TestVolumesClient(object):
3355

@@ -68,11 +90,32 @@ def test_create(self, hetzner_client, server):
6890
assert next_actions[0].id == 13
6991
assert next_actions[0].command == "start_server"
7092

93+
@pytest.mark.parametrize("volume", [Volume(id=1), BoundVolume(mock.MagicMock(), dict(id=1))])
94+
def test_get_actions(self, hetzner_client, volume):
95+
actions = hetzner_client.volumes.get_actions(volume)
96+
97+
assert len(actions) == 1
98+
assert actions[0].id == 13
99+
assert actions[0].command == "attach_volume"
100+
101+
@pytest.mark.parametrize("volume", [Volume(id=1), BoundVolume(mock.MagicMock(), dict(id=1))])
102+
def test_update(self, hetzner_client, volume):
103+
volume = hetzner_client.volumes.update(volume, name="new-name", labels={})
104+
105+
assert volume.id == 4711
106+
assert volume.name == "new-name"
107+
108+
@pytest.mark.parametrize("volume", [Volume(id=1), BoundVolume(mock.MagicMock(), dict(id=1))])
109+
def test_delete(self, hetzner_client, volume):
110+
delete_success = hetzner_client.volumes.delete(volume)
111+
112+
assert delete_success is True
113+
71114
@pytest.mark.parametrize("server,volume",
72115
[(Server(id=43), Volume(id=4711)),
73116
(BoundServer(mock.MagicMock(), dict(id=43)), BoundVolume(mock.MagicMock(), dict(id=4711)))])
74117
def test_attach(self, hetzner_client, server, volume):
75-
action = hetzner_client.volumes.attach(server, volume)
118+
action = hetzner_client.volumes.attach(volume, server)
76119
assert action.id == 13
77120
assert action.progress == 0
78121
assert action.command == "attach_volume"
@@ -83,3 +126,10 @@ def test_detach(self, hetzner_client, volume):
83126
assert action.id == 13
84127
assert action.progress == 0
85128
assert action.command == "detach_volume"
129+
130+
@pytest.mark.parametrize("volume", [Volume(id=4711), BoundVolume(mock.MagicMock(), dict(id=4711))])
131+
def test_resize(self, hetzner_client, volume):
132+
action = hetzner_client.volumes.resize(volume, 50)
133+
assert action.id == 13
134+
assert action.progress == 0
135+
assert action.command == "resize_volume"

0 commit comments

Comments
 (0)