blob: 9bc3d48f10e93da4cf541e3cd8679b151657b5ba [file] [log] [blame] [edit]
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright 2020 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""GRPC server that provides host service to docker containers on debian."""
import logging
import subprocess
import sys
import netifaces
import hostservice_pb2
import host_server_base
import os
from utils import cpu_temperature_orchestrator
_LOGGER = logging.getLogger("host-server")
class HostServerDebian(host_server_base.HostServerBase):
"""Implementation for the hostservice contract on debian.
It has been specifically chosen to not implement reboot, reset or update
as on debian these should be performed outside of the moblab software.
As debian machines can serve other functions than just running moblab like
being a development machine this implementation should only implement
read only functions as providing a non authenticated way for someone to
perform write / destructive operations on such as machine as root user
is undesirable.
"""
# CpuTemperatureOrchestrator instance.
_cpu_temp = cpu_temperature_orchestrator.CpuTemperatureOrchestrator(30, 1)
def __init__(self):
"""Initialize variables."""
self.docker_dir = ""
def get_host_identifier(self, unused_request, unused_context):
"""Get a stable identifier for the machine.
This identifier should be as stable as possible, it is used in
CPCon to tie different install id's of the moblab software together
and makes it possible to give a user defined name to the moblab device
in the remote console.
Returns:
string: the value in /etc/machine id or "NoSerialNumber".
"""
response = hostservice_pb2.HostIdentifierResponse()
response.identifier = "NoSerialNumber"
# This can change on OS re-install.
cmd = ["cat /etc/machine-id"]
try:
cmd_result = subprocess.run(
cmd, stdout=subprocess.PIPE, shell=True
)
if cmd_result and cmd_result.returncode == 0 and cmd_result.stdout:
response.identifier = cmd_result.stdout.decode("utf-8").strip()
except subprocess.SubprocessError:
_LOGGER.exception("Error retrieving machine id")
return response
def get_disk_info(self, unused_request, unused_context):
"""Return a string that shows information about disk usage.
This string is displayed in mobmonitor and no guarantee of the format
is given so it should not be parsed by the client.
Returns:
string: Information about the disk usage of the host machine.
"""
result = "df -h\n"
cmd = ["df -h | sort -r -k 5 -i | grep dev"]
cmd_result = subprocess.run(cmd, stdout=subprocess.PIPE, shell=True)
if cmd_result and cmd_result.returncode == 0 and cmd_result.stdout:
result += cmd_result.stdout.decode("utf-8")
result += "\nlsblk\n"
cmd = ["lsblk"]
cmd_result = subprocess.run(cmd, stdout=subprocess.PIPE, shell=True)
if cmd_result and cmd_result.returncode == 0 and cmd_result.stdout:
result += cmd_result.stdout.decode("utf-8")
response = hostservice_pb2.DiskInfoResponse()
response.disk_info = result
return response
def get_cpu_temperature(self, unused_request, unused_context):
"""Get average CPU temperature over the last 30 minutes."""
response = hostservice_pb2.CpuTemperatureResponse()
temp = HostServerDebian._cpu_temp.get_average_cpu_temperature()
if not isinstance(temp, float):
_LOGGER.error(
"Received temperature value is %s which "
"is not of type float." % str(temp)
)
temp = float(0)
response.temperature = temp
return response
def get_ip(self, unused_request, unused_context):
"""Get the host machines IP address on the network.
Find the address the host machine connects to the internet and
return that value.
Returns:
string: network address of the host.
"""
response = hostservice_pb2.IpResponse()
ip_addr = self._get_external_interface_addresses().get("inet_addr")
response.ip_address = ip_addr if ip_addr else "localhost"
return response
def get_external_mac_address(self, unused_request, context):
"""Return the external interface's mac address.
Returns:
ExternalMacAddressResponse:
string: mac_address, the external mac address found.
"""
mac_addr = self._get_external_interface_addresses().get("link_addr")
return hostservice_pb2.ExternalMacAddressResponse(mac_address=mac_addr)
def _get_external_interface_addresses(self):
"""Get the external interface's ip address and mac address."""
MOBLAB_SUBNET_ADDR = "192.168.231.1"
if_info = {"inet_addr": None, "link_addr": None}
for iface in netifaces.interfaces():
# logic expects the physical interfaces
# to be starting with "en"
if not iface.startswith("en"):
continue
if_addresses = netifaces.ifaddresses(iface)
inet_addrs = if_addresses.get(netifaces.AF_INET)
link_addrs = if_addresses.get(netifaces.AF_LINK)
if not inet_addrs:
continue
for addr in inet_addrs:
if MOBLAB_SUBNET_ADDR != addr.get("addr"):
_LOGGER.info(f"adding interface info of {iface}: {addr}")
if_info["inet_addr"] = addr["addr"]
if_info["link_addr"] = (
link_addrs[0].get("addr") if link_addrs else None
)
return if_info
def get_system_version(self, unused_request, unused_context):
"""Get version string of the host machines OS.
Returns:
string: An information string about the version of the host os.
"""
response = hostservice_pb2.SystemVersionResponse()
response.version = "No Version Number"
try:
with open("/etc/debian_version") as f:
response.version = "".join(f.readlines()).strip()
except (FileNotFoundError, IOError):
_LOGGER.exception("Error retrieving version number")
response.track = ""
response.description = ""
try:
with open("/etc/os-release") as f:
output = f.readlines()
os_release_info = {}
for line in output:
(key, value) = line.split("=")
os_release_info[key] = value.strip('"\n')
response.track = os_release_info.get(
"VERSION_CODENAME", "No Track Found"
)
response.description = os_release_info.get(
"PRETTY_NAME", "No Name Found"
)
except (FileNotFoundError, IOError):
_LOGGER.exception("Error retrieving version information")
return response
def get_disk_usage_stats(self, request, context):
"""Get information about available disk space.
Returns:
GetDiskUsageStatsResponse:
free_MB: amount of available disk space in MB.
"""
if not self.docker_dir:
cmd = ["docker info | grep 'Root Dir'"]
cmd_result = subprocess.run(
cmd, stdout=subprocess.PIPE, shell=True
)
self.docker_dir = cmd_result.stdout.split(b": ")[1].strip()
disk_stats = [
s.split()
for s in os.popen("df -BM %s" % self.docker_dir)
.read()
.splitlines()
]
# Ex. output:
# Filesystem 1M-blocks Used Available Use% Mounted on
# /dev/mmcblk0 790388M 168009M 582163M 23% /
# ...
if len(disk_stats) != 1:
raise host_server_base.DiskSpaceException(
"%s stateful partitions found, expected one" % len(disk_stats)
)
disk_stats = disk_stats[0]
response = hostservice_pb2.GetDiskUsageStatsResponse()
response.available_MB = int(disk_stats[3].rstrip("M"))
response.total_MB = int(disk_stats[1].rstrip("M"))
return response
if __name__ == "__main__":
host_server = HostServerDebian()
host_server.serve(sys.argv[1:])