Skip to content

Commit 1993e19

Browse files
author
Elias Nygren
committed
add convenience functions for starting/stopping+destroying servers
1 parent 8178912 commit 1993e19

File tree

1 file changed

+127
-16
lines changed

1 file changed

+127
-16
lines changed

upcloud/server.py

Lines changed: 127 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66
from builtins import str
77
from future import standard_library
88
standard_library.install_aliases()
9+
910
from .base import BaseAPI
11+
from time import sleep
12+
1013

1114
class Server(BaseAPI):
1215
"""
@@ -16,7 +19,11 @@ class Server(BaseAPI):
1619
See __setattr__ override. Any field can still be set with object.__setattr__(self, field, val) syntax.
1720
"""
1821

19-
updateable_fields = [ "boot_order", "core_number", "firewall", "hostname", "memory_amount",
22+
#
23+
# Functionality for partial immutability and repopulating the object from API.
24+
#
25+
26+
updateable_fields = [ "boot_order", "core_number", "firewall", "hostname", "memory_amount",
2027
"nic_model", "title", "timezone", "video_model", "vnc", "vnc_password" ]
2128

2229

@@ -60,6 +67,13 @@ def populate(self):
6067
populated = True)
6168
return self
6269

70+
def __str__(self):
71+
return "Server: " + self.hostname
72+
73+
#
74+
# Main functionality, 1:1 with UpCloud's API
75+
#
76+
6377
def save(self):
6478
"""
6579
Sync local changes in server's attributes to the API.
@@ -88,7 +102,7 @@ def shutdown(self):
88102
body = dict()
89103
body["stop_server"] = {
90104
"stop_type" : "soft",
91-
"timeout" : "30"
105+
"timeout" : "30"
92106
}
93107
self.cloud_manager.post_request("/server/" + self.uuid + "/stop" , body)
94108
object.__setattr__(self, "state", "maintenance") # post_request already handles any errors from API
@@ -119,8 +133,8 @@ def restart(self):
119133
body = dict()
120134
body["restart_server"] = {
121135
"stop_type" : "soft",
122-
"timeout" : "30",
123-
"timeout_action" : "destroy"
136+
"timeout" : "30",
137+
"timeout_action" : "destroy"
124138
}
125139
self.cloud_manager.post_request("/server/" + self.uuid + "/restart" , body)
126140
object.__setattr__(self, "state", "maintenance") # post_request already handles any errors from API
@@ -186,6 +200,12 @@ def get_firewall_rules(self):
186200
firewall_rule._associate_with_server(self)
187201
return firewall_rules
188202

203+
204+
#
205+
# Helper and convenience functions.
206+
# May perform several API requests and contain more complex logic.
207+
#
208+
189209
def configure_firewall(self, FirewallRules):
190210
"""
191211
Helper function for automatically adding several FirewallRules in series.
@@ -201,6 +221,11 @@ def configure_firewall(self, FirewallRules):
201221

202222

203223
def prepare_post_body(self):
224+
"""
225+
Prepares a JSON serializable dict from a Server instance with nested
226+
Storage instances.
227+
"""
228+
204229
body = dict()
205230
# mandatory
206231
body["server"] = {
@@ -211,16 +236,16 @@ def prepare_post_body(self):
211236
}
212237

213238
# optional
214-
if hasattr(self, "core_number"): body["server"]["core_number"] = self.core_number
215-
if hasattr(self, "memory_amount"): body["server"]["memory_amount"] = self.memory_amount
216-
if hasattr(self, "boot_order"): body["server"]["boot_order"] = self.boot_order
217-
if hasattr(self, "firewall"): body["server"]["firewall"] = self.firewall
218-
if hasattr(self, "nic_model"): body["server"]["nic_model"] = self.nic_model
219-
if hasattr(self, "timezone"): body["server"]["timezone"] = self.timezone
220-
if hasattr(self, "video_model"): body["server"]["video_model"] = self.video_model
221-
if hasattr(self, "vnc_password"): body["server"]["vnc_password"] = self.vnc_password
222-
if hasattr(self, "password_delivery"): body["server"]["password_delivery"] = self.password_delivery
223-
else: body["server"]["password_delivery"] = "none"
239+
if hasattr(self, "core_number"): body["server"]["core_number"] = self.core_number
240+
if hasattr(self, "memory_amount"): body["server"]["memory_amount"] = self.memory_amount
241+
if hasattr(self, "boot_order"): body["server"]["boot_order"] = self.boot_order
242+
if hasattr(self, "firewall"): body["server"]["firewall"] = self.firewall
243+
if hasattr(self, "nic_model"): body["server"]["nic_model"] = self.nic_model
244+
if hasattr(self, "timezone"): body["server"]["timezone"] = self.timezone
245+
if hasattr(self, "video_model"): body["server"]["video_model"] = self.video_model
246+
if hasattr(self, "vnc_password"): body["server"]["vnc_password"] = self.vnc_password
247+
if hasattr(self, "password_delivery"): body["server"]["password_delivery"] = self.password_delivery
248+
else: body["server"]["password_delivery"] = "none"
224249

225250

226251
body["server"]["storage_devices"] = {
@@ -238,6 +263,10 @@ def prepare_post_body(self):
238263

239264

240265
def to_dict(self):
266+
"""
267+
Prepares a JSON serializable dict for read-only purposes. Includes storages and IP-addresses.
268+
Use prepare_post_body for POST and .save() for PUT.
269+
"""
241270
fields = dict(vars(self).items())
242271

243272
if self.populated:
@@ -265,5 +294,87 @@ def to_dict(self):
265294
return fields
266295

267296

268-
def __str__(self):
269-
return "Server: " + self.hostname
297+
def get_public_ip(self):
298+
"""Returns a server's public IP. Prioritizes IPv4 over IPv6."""
299+
300+
if not hasattr(self, 'ip_addresses'):
301+
self.populate()
302+
303+
# server can have several public IPs
304+
public_ip_addrs = []
305+
for ip_addr in self.ip_addresses:
306+
if ip_addr.access == 'public':
307+
public_ip_addrs.append(ip_addr)
308+
309+
if not public_ip_addrs:
310+
return None
311+
312+
# prefer IPv4
313+
for ip_addr in public_ip_addrs:
314+
if ip_addr.family == 'IPv4':
315+
return ip_addr.address
316+
317+
# ...but accept IPv6 too
318+
return public_ip_addrs[0].address
319+
320+
321+
def _wait_for_state_change(self, target_states, update_interval=10):
322+
"""
323+
Blocking wait until target_state reached. update_interval is in seconds.
324+
Warning: state change must begin before calling this method.
325+
"""
326+
327+
while self.state not in target_states:
328+
if self.state == 'error':
329+
raise Exception('server is in error state')
330+
331+
# update server state every 10s
332+
sleep(update_interval)
333+
self.populate()
334+
335+
336+
def ensure_started(self):
337+
"""Starts a server and waits (blocking wait) until a it is fully started."""
338+
339+
# server is either starting or stopping (or error)
340+
if self.state in ['maintenance', 'error']:
341+
self._wait_for_state_change(['stopped', 'started'])
342+
343+
if self.state == 'stopped':
344+
self.start()
345+
self._wait_for_state_change(['started'])
346+
347+
if self.state == 'started':
348+
return True
349+
else:
350+
# something went wrong, fail explicitly
351+
raise Exception('unknown server state: ' + self.state)
352+
353+
354+
def stop_and_destroy(self):
355+
"""Destroy a server and its storages. Stops the server (blocking wait) before destroying."""
356+
357+
def destroy_storages():
358+
# list view does not return all server info, populate if necessary
359+
if not hasattr(self, 'storage_devices'):
360+
self.populate()
361+
362+
# destroy the server and all storages attached to it
363+
self.destroy()
364+
for storage in self.storage_devices:
365+
storage.destroy()
366+
367+
# server is either starting or stopping (or error)
368+
if self.state in ['maintenance', 'error']:
369+
self._wait_for_state_change(['stopped', 'started'])
370+
371+
# server is started
372+
if self.state != 'stopped':
373+
self.stop()
374+
self._wait_for_state_change(['stopped'])
375+
376+
if self.state == 'stopped':
377+
destroy_storages()
378+
else:
379+
# something went wrong, fail explicitly
380+
raise Exception('unknown server state: ' + self.state)

0 commit comments

Comments
 (0)