Skip to content

Commit 7a4cd8f

Browse files
committed
Introduce (un)mounting hosts via sshfs on client
1 parent 17cd450 commit 7a4cd8f

File tree

1 file changed

+141
-2
lines changed

1 file changed

+141
-2
lines changed

hyperion/lib/networking/clientInterface.py

Lines changed: 141 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@
22
import socket
33
import time
44
import logging
5+
import os
56
import sys
67
import struct
78
import threading
9+
import hyperion.lib.util.config as config
810
import hyperion.lib.util.actionSerializer as actionSerializer
911
import hyperion.lib.util.exception as exceptions
1012
from 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

1317
is_py2 = sys.version[0] == '2'
1418
if 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

Comments
 (0)