2222# along with this program. If not, see <http://www.gnu.org/licenses/>.
2323
2424import errno
25+ import ipaddress
2526import json
2627import logging
2728import os
29+ import socket
2830import sys
2931from collections import namedtuple
32+ from contextlib import closing
3033
3134from .log import set_detailed_logs
3235
@@ -44,6 +47,7 @@ class ServiceCoord(namedtuple("ServiceCoord", "name shard")):
4447 service (thus identifying it).
4548
4649 """
50+
4751 def __repr__ (self ):
4852 return "%s,%d" % (self .name , self .shard )
4953
@@ -53,6 +57,75 @@ class ConfigError(Exception):
5357 pass
5458
5559
60+ class EphemeralServiceConfig :
61+ """Configuration of an ephemeral service. An ephemeral service is a
62+ normal service whose shard is chosen depending on its address and
63+ port. The port is assigned inside a range and the address must be
64+ inside the subnet.
65+ """
66+ EPHEMERAL_SHARD_OFFSET = 10000
67+
68+ def __init__ (self , subnet , min_port , max_port ):
69+ self .subnet = ipaddress .ip_network (subnet )
70+ self .min_port = min_port
71+ self .max_port = max_port
72+ if min_port > max_port :
73+ raise ConfigError ("Invalid port range: [%s, %s]"
74+ % (min_port , max_port ))
75+
76+ def get_shard (self , address , port ):
77+ """Get the ephemeral shard for a service given its address and port.
78+
79+ address (IPv4Address|IPv6Address): address of the service.
80+ port (int): port of the service.
81+
82+ return (int): shard of the service
83+ """
84+ if address not in self .subnet :
85+ raise ValueError ("The address is not inside the subnet" )
86+ host_id = int (address ) & int (self .subnet .hostmask )
87+ num_ports = self .max_port - self .min_port + 1
88+ shard = host_id * num_ports + (port - self .min_port )
89+ return shard + self .EPHEMERAL_SHARD_OFFSET
90+
91+ def get_address (self , shard ):
92+ """Get the address and port of a service given its shard.
93+
94+ shard (int): shard of the service
95+
96+ return (Address): address and port of the service
97+ """
98+ shard -= self .EPHEMERAL_SHARD_OFFSET
99+ num_ports = self .max_port - self .min_port + 1
100+ port_offset = shard % num_ports
101+ host_id = (shard - port_offset ) // num_ports
102+
103+ port = self .min_port + port_offset
104+ addr = self .subnet .network_address + host_id
105+ if addr not in self .subnet :
106+ raise ValueError ("The shard is not valid" )
107+ return Address (str (addr ), port )
108+
109+ def find_free_port (self , address ):
110+ """Find the first open port.
111+
112+ address (IPv4Address|IPv6Address): local address to bind to
113+ """
114+ if address .version == 4 :
115+ family = socket .AF_INET
116+ else :
117+ family = socket .AF_INET6
118+ for port in range (self .min_port , self .max_port + 1 ):
119+ with closing (socket .socket (family , socket .SOCK_STREAM )) as sock :
120+ try :
121+ sock .bind ((str (address ), port ))
122+ return port
123+ except socket .error :
124+ continue
125+ raise ValueError ("No free port found in range [%s, %s] "
126+ "for address %s" % (minport , maxport , address ))
127+
128+
56129class AsyncConfig :
57130 """This class will contain the configuration for the
58131 services. This needs to be populated at the initilization stage.
@@ -69,6 +142,7 @@ class AsyncConfig:
69142 """
70143 core_services = {}
71144 other_services = {}
145+ ephemeral_services = {} # type: dict[str, EphemeralServiceConfig]
72146
73147
74148async_config = AsyncConfig ()
@@ -81,6 +155,7 @@ class Config:
81155 directory for information on the meaning of the fields.
82156
83157 """
158+
84159 def __init__ (self ):
85160 """Default values for configuration, plus decide if this
86161 instance is running from the system path or from the source
@@ -274,6 +349,18 @@ def _load_unique(self, path):
274349 self .async_config .other_services [coord ] = Address (* shard )
275350 del data ["other_services" ]
276351
352+ for service_name in data ["ephemeral_services" ]:
353+ if service_name .startswith ("_" ):
354+ continue
355+ service = data ["ephemeral_services" ][service_name ]
356+ self .async_config .ephemeral_services [service_name ] = \
357+ EphemeralServiceConfig (
358+ service ["subnet" ],
359+ service ["min_port" ],
360+ service ["max_port" ],
361+ )
362+ del data ["ephemeral_services" ]
363+
277364 # Put everything else in self.
278365 for key , value in data .items ():
279366 setattr (self , key , value )
0 commit comments