22import socket
33import time
44import logging
5+ import os
56import sys
67import struct
78import threading
9+ import hyperion .lib .util .config as config
810import hyperion .lib .util .actionSerializer as actionSerializer
911import hyperion .lib .util .exception as exceptions
1012from hyperion .manager import AbstractController
11- from hyperion .lib .util .events import ServerDisconnectEvent
13+ from hyperion .lib .util .events import ServerDisconnectEvent , DisconnectEvent , ReconnectEvent
14+ from signal import *
15+ from subprocess import Popen , PIPE
1216
1317is_py2 = sys .version [0 ] == '2'
1418if is_py2 :
@@ -51,6 +55,9 @@ def __init__(self, host, port):
5155 self .mysel = selectors .DefaultSelector ()
5256 self .keep_running = True
5357 self .ui_event_queue = None
58+ self .mounted_hosts = []
59+
60+ signal (SIGINT , self ._handle_sigint )
5461
5562 self .function_mapping = {
5663 'get_conf_response' : self ._set_config ,
@@ -84,6 +91,10 @@ def __init__(self, host, port):
8491 self .logger .debug ("Waiting for config" )
8592 time .sleep (0.5 )
8693
94+ for host in self .host_list :
95+ if not self .is_localhost (host ):
96+ self ._mount_host (host )
97+
8798 def request_config (self ):
8899 action = 'get_conf'
89100 payload = []
@@ -94,14 +105,21 @@ def request_config(self):
94105 message = actionSerializer .serialize_request (action , payload )
95106 self .send_queue .put (message )
96107
97- def cleanup (self , full = False ):
108+ def cleanup (self , full = False , exit_code = 0 ):
98109 if full :
99110 action = 'quit'
100111 message = actionSerializer .serialize_request (action , [full ])
112+ self .logger .debug ("Sending quit to server" )
101113 else :
102114 action = 'unsubscribe'
103115 message = actionSerializer .serialize_request (action , [])
116+ self .logger .debug ("Sending unsubscribe to server" )
104117 self .send_queue .put (message )
118+
119+ for host in self .mounted_hosts :
120+ self .logger .debug ("Unmounting host %s" % host )
121+ self ._unmount_host (host )
122+
105123 self .keep_running = False
106124
107125 def get_component_by_id (self , comp_id ):
@@ -170,6 +188,14 @@ def _forward_event(self, event):
170188 if self .ui_event_queue :
171189 self .ui_event_queue .put (event )
172190
191+ # Special events handling
192+ if isinstance (event , DisconnectEvent ):
193+ self .host_list [event .host_name ] = None
194+ self ._unmount_host (event .host_name )
195+ elif isinstance (event , ReconnectEvent ):
196+ self .host_list [event .host_name ] = True
197+ self ._mount_host (event .host_name )
198+
173199 def loop (self ):
174200 # Keep alive until shutdown is requested and no messages are left to send
175201 while self .keep_running or not self .send_queue .empty ():
@@ -209,3 +235,116 @@ def add_subscriber(self, subscriber_queue):
209235 :return: None
210236 """
211237 self .ui_event_queue = subscriber_queue
238+
239+ ###################
240+ # Host related
241+ ###################
242+ def _mount_host (self , hostname ):
243+ """Mount remote host log directory via sshfs.
244+
245+ :param hostname: Remote host name
246+ :type hostname: str
247+ :return: None
248+ """
249+ directory = "%s/%s" % (config .TMP_LOG_PATH , hostname )
250+ # First unmount to prevent unknown permissions issue on disconnected mountpoint
251+ self ._unmount_host (hostname )
252+
253+ if not self .host_list [hostname ]:
254+ self .logger .error ("'%s' seems not to be connected. Aborting mount! Logs will not be available" % hostname )
255+ return
256+ try :
257+ os .makedirs (directory )
258+ except OSError as err :
259+ if err .errno == 17 :
260+ # Dir already exists
261+ pass
262+ else :
263+ self .logger .error ("Error while trying to create directory '%s'" % directory )
264+
265+ cmd = 'sshfs %s:%s %s -F %s' % (hostname ,
266+ config .TMP_LOG_PATH ,
267+ directory ,
268+ config .SSH_CONFIG_PATH
269+ )
270+ self .logger .debug ("running command: %s" % cmd )
271+ p = Popen (cmd , shell = True , stdin = PIPE , stdout = PIPE , stderr = PIPE )
272+
273+ while p .poll () is None :
274+ time .sleep (.5 )
275+
276+ if p .returncode == 0 :
277+ self .logger .debug ("Successfully mounted remote '%s' with sshfs" % hostname )
278+ self .mounted_hosts .append (hostname )
279+ else :
280+ self .logger .error ("Could not mount remote '%s' with sshfs - logs will not be accessible!" % hostname )
281+ self .logger .debug ("sshfs exited with error: %s (code: %s)" % (p .stderr .readlines (), p .returncode ))
282+
283+ self .logger .debug ("mounted hosts: %s" % self .mounted_hosts )
284+
285+ def _unmount_host (self , hostname ):
286+ """Unmount fuse mounted remote log directory.
287+
288+ :param hostname: Remote host name.
289+ :type hostname: str
290+ :return: None
291+ """
292+ directory = "%s/%s" % (config .TMP_LOG_PATH , hostname )
293+
294+ cmd = 'fusermount -u %s' % directory
295+ self .logger .debug ("running command: %s" % cmd )
296+ p = Popen (cmd , shell = True , stdin = PIPE , stdout = PIPE , stderr = PIPE )
297+
298+ if hostname in self .mounted_hosts :
299+ self .mounted_hosts .remove (hostname )
300+
301+ while p .poll () is None :
302+ time .sleep (.5 )
303+
304+ self .logger .debug ("mounted hosts: %s" % self .mounted_hosts )
305+
306+ def reconnect_with_host (self , hostname ):
307+ action = 'reconnect_with_host'
308+ payload = [hostname ]
309+
310+ message = actionSerializer .serialize_request (action , payload )
311+ self .send_queue .put (message )
312+
313+ def is_localhost (self , hostname ):
314+ """Check if 'hostname' resolves to localhost.
315+
316+ :param hostname: Name of host to check
317+ :type hostname: str
318+ :return: Whether 'host' resolves to localhost or not
319+ :rtype: bool
320+ """
321+
322+ if hostname == 'localhost' :
323+ hostname = self .host
324+
325+ try :
326+ hn_out = socket .gethostbyname ('%s' % hostname )
327+ if hn_out == '127.0.0.1' or hn_out == '127.0.1.1' or hn_out == '::1' :
328+ self .logger .debug ("Host '%s' is localhost" % hostname )
329+ return True
330+ else :
331+ self .logger .debug ("Host '%s' is not localhost" % hostname )
332+ return False
333+ except socket .gaierror :
334+ raise exceptions .HostUnknownException ("Host '%s' is unknown! Update your /etc/hosts file!" % hostname )
335+
336+ def run_on_localhost (self , comp ):
337+ """Check if component 'comp' is run on localhost or not.
338+
339+ :param comp: Component to check
340+ :type comp: dict
341+ :return: Whether component is run on localhost or not
342+ :rtype: bool
343+ """
344+ try :
345+ return self .is_localhost (comp ['host' ])
346+ except exceptions .HostUnknownException as ex :
347+ raise ex
348+
349+ def _handle_sigint (self , signum , frame ):
350+ self .cleanup (False )
0 commit comments