66from builtins import str
77from future import standard_library
88standard_library .install_aliases ()
9+
910from .base import BaseAPI
11+ from time import sleep
12+
1013
1114class 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