blob: 226225f4bc664ef864770e7c1c73a78d2ff8f5d7 [file] [log] [blame]
Pin-Yen Lin3f96b752020-04-28 15:48:49 +08001#!/usr/bin/env python3
2# Copyright 2020 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Multicast service to spawn uftp server"""
7
Pin-Yen Lin576bbca2020-06-02 15:45:45 +08008import argparse
Pin-Yen Lin58875112021-01-15 16:51:59 +08009import logging
Pin-Yen Lin57d659f2020-04-28 15:59:28 +080010import os
Pin-Yen Lina996ae92020-06-01 14:53:42 +080011import time
Pin-Yen Lin57d659f2020-04-28 15:59:28 +080012
13from cros.factory.utils import json_utils
Pin-Yen Lin58875112021-01-15 16:51:59 +080014from cros.factory.utils import process_utils
Pin-Yen Lin57d659f2020-04-28 15:59:28 +080015
16
Pin-Yen Lin58875112021-01-15 16:51:59 +080017MCAST_CONFIG_NAME = 'multicast_config.json'
18UMPIRE_CONFIG_NAME = 'active_umpire.json'
19UMPIRE_DIR = '/var/db/factory/umpire'
Pin-Yen Lin57d659f2020-04-28 15:59:28 +080020UFTP_PATH = '/usr/bin/uftp'
21
Pin-Yen Lin57d659f2020-04-28 15:59:28 +080022
Pin-Yen Lin58875112021-01-15 16:51:59 +080023class UftpProcess:
24 CC_TYPE = 'tfmcc' # TCP friendly multicast congestion control.
25 LOG_LEVEL = '0'
26 ROBUST_FACTOR = '50' # The number of announcements sent before file transfer.
27 TTL = '10'
Pin-Yen Lin57d659f2020-04-28 15:59:28 +080028
Pin-Yen Lin58875112021-01-15 16:51:59 +080029 def __init__(self, file_path, multicast_addr, status_file_path, interface):
30 """Constructor of a UFTP process wrapper.
Pin-Yen Lin57d659f2020-04-28 15:59:28 +080031
Pin-Yen Lin58875112021-01-15 16:51:59 +080032 Args:
33 file_path: The file path to be transferred.
34 multicast_addr: The multicast address for sending announcement messages.
35 status_file_path: The path for logging file transfer status.
36 interface: The network interface name or IP to send the data from.
37 """
38 self.file_path = file_path
39 self.multicast_addr = multicast_addr
40 self.status_file_path = status_file_path
41 self.interface = interface
42
43 self.Spawn()
44
45 def Spawn(self):
46 addr, port = self.multicast_addr.split(':')
47
48 # "-u" defines the the UDP source port, while "-p" defines the UDP
49 # destination port.
50 cmd = [
51 UFTP_PATH, '-M', addr, '-t', self.TTL, '-u', port, '-p', port, '-x',
52 self.LOG_LEVEL, '-S', self.status_file_path, '-C', self.CC_TYPE, '-s',
53 self.ROBUST_FACTOR
54 ]
55
56 if self.interface != '':
57 cmd += ['-I', self.interface]
58
59 cmd += [self.file_path]
60
61 self.process = process_utils.Spawn(cmd, stderr=process_utils.PIPE)
62
63 def RespawnIfDied(self):
64 ANNOUNCE_TIMED_OUT_RETCODE = 7
65 if self.process is not None and self.process.poll() is not None:
66 # Skip announce timed out message.
67 if self.process.returncode != ANNOUNCE_TIMED_OUT_RETCODE:
68 logging.error(self.process.stderr.read())
69 self.Spawn()
70
71 def Kill(self):
72 self.process.kill()
73 self.process.wait()
74
75
76def IsUmpireEnabled(project):
77 """Return True if corresponding Umpire container is running."""
78 container_name = 'umpire_%s' % project
79
80 container_list = process_utils.CheckOutput(
81 ['docker', 'ps', '--all', '--format', '{{.Names}}'],
82 encoding='utf-8').splitlines()
83 return container_name in container_list
84
85
86def ScanUftpArgsFromConfig(mcast_config, resource_dir, log_dir):
87 """Scan an Umpire config to extract UFTP arguments for the project.
88
89 Args:
90 mcast_config: The multicast config file generated by multicast service.
91 The content of multicast config is similar to Umpire payload
92 config file, but has an additional "multicast" key for
93 multicast information.
94 resource_dir: The Umpire resource directory.
95 log_dir: The log directory for UFTP server.
96 """
97 scanned_args = []
98
99 mcast_addr = mcast_config['multicast']
100 uftp_interface = mcast_config['multicast'].get('server_ip', '')
101
102 for component in mcast_addr:
103 if component == 'server_ip':
104 continue
105 for part in mcast_addr[component]:
106 file_name = mcast_config[component][part]
107
108 uftp_file_path = os.path.join(resource_dir, file_name)
109 uftp_mcast_addr = mcast_addr[component][part]
110 uftp_status_file_path = os.path.join(log_dir, 'uftp_%s.log' % file_name)
111
112 scanned_args += [(uftp_file_path, uftp_mcast_addr, uftp_status_file_path,
113 uftp_interface)]
114
115 return scanned_args
Pin-Yen Lin57d659f2020-04-28 15:59:28 +0800116
Pin-Yen Lin3f96b752020-04-28 15:48:49 +0800117
118def Main():
Pin-Yen Lin576bbca2020-06-02 15:45:45 +0800119 parser = argparse.ArgumentParser()
Pin-Yen Lin58875112021-01-15 16:51:59 +0800120 parser.add_argument('-l', '--log-dir', help='path to Umpire log directory',
121 required=True)
Pin-Yen Lin57d659f2020-04-28 15:59:28 +0800122
Pin-Yen Lin576bbca2020-06-02 15:45:45 +0800123 args = parser.parse_args()
124
Pin-Yen Lin58875112021-01-15 16:51:59 +0800125 active_process = {}
126 active_config = {}
Pin-Yen Lina996ae92020-06-01 14:53:42 +0800127
128 while True:
Pin-Yen Lin58875112021-01-15 16:51:59 +0800129 scanned_config = {}
130 scanned_args = {}
131
132 for project in os.listdir(UMPIRE_DIR):
133 project_path = os.path.join(UMPIRE_DIR, project)
134 try:
135 umpire_config = json_utils.LoadFile(
136 os.path.join(project_path, UMPIRE_CONFIG_NAME))
137 mcast_enabled = umpire_config['services']['multicast']['active']
138 mcast_config = json_utils.LoadFile(
139 os.path.join(project_path, MCAST_CONFIG_NAME))
140 except Exception:
141 continue
142
143 if not mcast_enabled or not IsUmpireEnabled(project):
144 continue
145
146 resource_dir = os.path.join(project_path, 'resources')
147
148 scanned_config[project] = mcast_config
149 scanned_args[project] = ScanUftpArgsFromConfig(mcast_config, resource_dir,
150 args.log_dir)
151
152 for project in scanned_config:
153 if scanned_config[project] != active_config.get(project):
154 active_config[project] = scanned_config[project]
155 for proc in active_process.get(project, []):
156 proc.Kill()
157
158 active_process[project] = []
159 for uftp_args in scanned_args[project]:
160 active_process[project] += [UftpProcess(*uftp_args)]
161 else:
162 for proc in active_process[project]:
163 proc.RespawnIfDied()
164
165 for project in set(active_config) - set(scanned_config):
166 active_config.pop(project)
167 for proc in active_process[project]:
168 proc.Kill()
169 active_process.pop(project)
170
Pin-Yen Lina996ae92020-06-01 14:53:42 +0800171 time.sleep(1)
Pin-Yen Lin57d659f2020-04-28 15:59:28 +0800172
Pin-Yen Lin3f96b752020-04-28 15:48:49 +0800173
174if __name__ == '__main__':
175 Main()