Mercurial > repos > shellac > sam_consensus_v3
comparison env/lib/python3.9/site-packages/galaxy/containers/__init__.py @ 0:4f3585e2f14b draft default tip
"planemo upload commit 60cee0fc7c0cda8592644e1aad72851dec82c959"
author | shellac |
---|---|
date | Mon, 22 Mar 2021 18:12:50 +0000 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:4f3585e2f14b |
---|---|
1 """ | |
2 Interfaces to containerization software | |
3 """ | |
4 | |
5 import errno | |
6 import inspect | |
7 import logging | |
8 import shlex | |
9 import subprocess | |
10 import sys | |
11 import uuid | |
12 from abc import ( | |
13 ABCMeta, | |
14 abstractmethod, | |
15 abstractproperty | |
16 ) | |
17 from typing import Any, Dict, NamedTuple, Optional, Type | |
18 | |
19 import yaml | |
20 | |
21 from galaxy.exceptions import ContainerCLIError | |
22 from galaxy.util.submodules import import_submodules | |
23 | |
24 | |
25 DEFAULT_CONTAINER_TYPE = 'docker' | |
26 DEFAULT_CONF = {'_default_': {'type': DEFAULT_CONTAINER_TYPE}} | |
27 | |
28 log = logging.getLogger(__name__) | |
29 | |
30 | |
31 class ContainerPort(NamedTuple): | |
32 """Named tuple representing ports published by a container, with attributes""" | |
33 port: int # Port number (inside the container) | |
34 protocol: str # Port protocol, either ``tcp`` or ``udp`` | |
35 hostaddr: str # Address or hostname where the published port can be accessed | |
36 hostport: int # Published port number on which the container can be accessed | |
37 | |
38 | |
39 class ContainerVolume(metaclass=ABCMeta): | |
40 | |
41 valid_modes = frozenset({"ro", "rw"}) | |
42 | |
43 def __init__(self, path, host_path=None, mode=None): | |
44 self.path = path | |
45 self.host_path = host_path | |
46 self.mode = mode | |
47 if mode and not self.mode_is_valid: | |
48 raise ValueError("Invalid container volume mode: %s" % mode) | |
49 | |
50 @abstractmethod | |
51 def from_str(cls, as_str): | |
52 """Classmethod to convert from this container type's string representation. | |
53 | |
54 :param as_str: string representation of volume | |
55 :type as_str: str | |
56 """ | |
57 | |
58 @abstractmethod | |
59 def __str__(self): | |
60 """Return this container type's string representation of the volume. | |
61 """ | |
62 | |
63 @abstractmethod | |
64 def to_native(self): | |
65 """Return this container type's native representation of the volume. | |
66 """ | |
67 | |
68 @property | |
69 def mode_is_valid(self): | |
70 return self.mode in self.valid_modes | |
71 | |
72 | |
73 class Container(metaclass=ABCMeta): | |
74 | |
75 def __init__(self, interface, id, name=None, **kwargs): | |
76 """ | |
77 | |
78 :param interface: Container interface for the given container type | |
79 :type interface: :class:`ContainerInterface` subclass instance | |
80 :param id: Container identifier | |
81 :type id: str | |
82 :param name: Container name | |
83 :type name: str | |
84 | |
85 """ | |
86 self._interface = interface | |
87 self._id = id | |
88 self._name = name | |
89 | |
90 @property | |
91 def id(self): | |
92 """The container's id""" | |
93 return self._id | |
94 | |
95 @property | |
96 def name(self): | |
97 """The container's name""" | |
98 return self._name | |
99 | |
100 @abstractmethod | |
101 def from_id(cls, interface, id): | |
102 """Classmethod to create an instance of Container from the container system's id for the given container type. | |
103 | |
104 :param interface: Container insterface for the given id | |
105 :type interface: :class:`ContainerInterface` subclass instance | |
106 :param id: Container identifier | |
107 :type id: str | |
108 :returns: Container object | |
109 :rtype: :class:`Container` subclass instance | |
110 """ | |
111 | |
112 @abstractproperty | |
113 def ports(self): | |
114 """Attribute for accessing details of ports published by the container. | |
115 | |
116 :returns: Port details | |
117 :rtype: list of :class:`ContainerPort` s | |
118 """ | |
119 | |
120 @abstractproperty | |
121 def address(self): | |
122 """Attribute for accessing the address or hostname where published ports can be accessed. | |
123 | |
124 :returns: Hostname or IP address | |
125 :rtype: str | |
126 """ | |
127 | |
128 @abstractmethod | |
129 def is_ready(self): | |
130 """Indicate whether or not the container is "ready" (up, available, running). | |
131 | |
132 :returns: True if ready, else False | |
133 :rtpe: bool | |
134 """ | |
135 | |
136 def map_port(self, port): | |
137 """Map a given container port to a host address/port. | |
138 | |
139 For legacy reasons, if port is ``None``, the first port (if any) will be returned | |
140 | |
141 :param port: Container port to map | |
142 :type port: int | |
143 :returns: Mapping to host address/port for given container port | |
144 :rtype: :class:`ContainerPort` instance | |
145 """ | |
146 mapping = None | |
147 ports = self.ports or [] | |
148 for mapping in ports: | |
149 if port == mapping.port: | |
150 return mapping | |
151 if port is None: | |
152 log.warning("Container %s (%s): Don't know how to map ports to containers with multiple exposed ports " | |
153 "when a specific port is not requested. Arbitrarily choosing first: %s", | |
154 self.name, self.id, mapping) | |
155 return mapping | |
156 else: | |
157 if port is None: | |
158 log.warning("Container %s (%s): No exposed ports found!", self.name, self.id) | |
159 else: | |
160 log.warning("Container %s (%s): No mapping found for port: %s", self.name, self.id, port) | |
161 return None | |
162 | |
163 | |
164 class ContainerInterface(metaclass=ABCMeta): | |
165 | |
166 container_type: Optional[str] = None | |
167 container_class: Optional[Type[Container]] = None | |
168 volume_class = Optional[Type[ContainerVolume]] | |
169 conf_defaults: Dict[str, Optional[Any]] = { | |
170 'name_prefix': 'galaxy_', | |
171 } | |
172 option_map: Dict[str, Dict] = {} | |
173 publish_port_list_required = False | |
174 supports_volumes = True | |
175 | |
176 def __init__(self, conf, key, containers_config_file): | |
177 self._key = key | |
178 self._containers_config_file = containers_config_file | |
179 mro = reversed(self.__class__.__mro__) | |
180 next(mro) | |
181 self._conf = ContainerInterfaceConfig() | |
182 for c in mro: | |
183 self._conf.update(c.conf_defaults) | |
184 self._conf.update(conf) | |
185 self.validate_config() | |
186 | |
187 def _normalize_command(self, command): | |
188 if isinstance(command, str): | |
189 command = shlex.split(command) | |
190 return command | |
191 | |
192 def _guess_kwopt_type(self, val): | |
193 opttype = 'string' | |
194 if isinstance(val, bool): | |
195 opttype = 'boolean' | |
196 elif isinstance(val, list): | |
197 opttype = 'list' | |
198 try: | |
199 if isinstance(val[0], tuple) and len(val[0]) == 3: | |
200 opttype = 'list_of_kovtrips' | |
201 except IndexError: | |
202 pass | |
203 elif isinstance(val, dict): | |
204 opttype = 'list_of_kvpairs' | |
205 return opttype | |
206 | |
207 def _guess_kwopt_flag(self, opt): | |
208 return '--%s' % opt.replace('_', '-') | |
209 | |
210 def _stringify_kwopts(self, kwopts): | |
211 opts = [] | |
212 for opt, val in kwopts.items(): | |
213 try: | |
214 optdef = self.option_map[opt] | |
215 except KeyError: | |
216 optdef = { | |
217 'flag': self._guess_kwopt_flag(opt), | |
218 'type': self._guess_kwopt_type(val), | |
219 } | |
220 log.warning("option '%s' not in %s.option_map, guessing flag '%s' type '%s'", | |
221 opt, self.__class__.__name__, optdef['flag'], optdef['type']) | |
222 opts.append(getattr(self, '_stringify_kwopt_' + optdef['type'])(optdef['flag'], val)) | |
223 return ' '.join(opts) | |
224 | |
225 def _stringify_kwopt_boolean(self, flag, val): | |
226 """ | |
227 """ | |
228 return '{flag}={value}'.format(flag=flag, value=str(val).lower()) | |
229 | |
230 def _stringify_kwopt_string(self, flag, val): | |
231 """ | |
232 """ | |
233 return '{flag} {value}'.format(flag=flag, value=shlex.quote(str(val))) | |
234 | |
235 def _stringify_kwopt_list(self, flag, val): | |
236 """ | |
237 """ | |
238 if isinstance(val, str): | |
239 return self._stringify_kwopt_string(flag, val) | |
240 return ' '.join('{flag} {value}'.format(flag=flag, value=shlex.quote(str(v))) for v in val) | |
241 | |
242 def _stringify_kwopt_list_of_kvpairs(self, flag, val): | |
243 """ | |
244 """ | |
245 l = [] | |
246 if isinstance(val, list): | |
247 # ['foo=bar', 'baz=quux'] | |
248 l = val | |
249 else: | |
250 # {'foo': 'bar', 'baz': 'quux'} | |
251 for k, v in dict(val).items(): | |
252 l.append(f'{k}={v}') | |
253 return self._stringify_kwopt_list(flag, l) | |
254 | |
255 def _stringify_kwopt_list_of_kovtrips(self, flag, val): | |
256 """ | |
257 """ | |
258 if isinstance(val, str): | |
259 return self._stringify_kwopt_string(flag, val) | |
260 l = [] | |
261 for k, o, v in val: | |
262 l.append(f'{k}{o}{v}') | |
263 return self._stringify_kwopt_list(flag, l) | |
264 | |
265 def _run_command(self, command, verbose=False): | |
266 if verbose: | |
267 log.debug('running command: [%s]', command) | |
268 command_list = self._normalize_command(command) | |
269 p = subprocess.Popen(command_list, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True) | |
270 stdout, stderr = p.communicate() | |
271 if p.returncode == 0: | |
272 return stdout.strip() | |
273 else: | |
274 msg = f"Command '{command}' returned non-zero exit status {p.returncode}" | |
275 log.error(msg + ': ' + stderr.strip()) | |
276 raise ContainerCLIError( | |
277 msg, | |
278 stdout=stdout.strip(), | |
279 stderr=stderr.strip(), | |
280 returncode=p.returncode, | |
281 command=command, | |
282 subprocess_command=command_list) | |
283 | |
284 @property | |
285 def key(self): | |
286 return self._key | |
287 | |
288 @property | |
289 def containers_config_file(self): | |
290 return self._containers_config_file | |
291 | |
292 def get_container(self, container_id): | |
293 return self.container_class.from_id(self, container_id) | |
294 | |
295 def set_kwopts_name(self, kwopts): | |
296 if self._name_prefix is not None: | |
297 name = '{prefix}{name}'.format( | |
298 prefix=self._name_prefix, | |
299 name=kwopts.get('name', uuid.uuid4().hex) | |
300 ) | |
301 kwopts['name'] = name | |
302 | |
303 def validate_config(self): | |
304 """ | |
305 """ | |
306 self._name_prefix = self._conf.name_prefix | |
307 | |
308 @abstractmethod | |
309 def run_in_container(self, command, image=None, **kwopts): | |
310 """ | |
311 """ | |
312 | |
313 | |
314 class ContainerInterfaceConfig(dict): | |
315 | |
316 def __setattr__(self, name, value): | |
317 self[name] = value | |
318 | |
319 def __getattr__(self, name): | |
320 try: | |
321 return self[name] | |
322 except KeyError: | |
323 raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'") | |
324 | |
325 def get(self, name, default=None): | |
326 try: | |
327 return self[name] | |
328 except KeyError: | |
329 return default | |
330 | |
331 | |
332 def build_container_interfaces(containers_config_file, containers_conf=None): | |
333 """Build :class:`ContainerInterface`s. Pass ``containers_conf`` to avoid rereading the config file. | |
334 | |
335 :param containers_config_file: Filename of containers_conf.yml | |
336 :type containers_config_file: str | |
337 :param containers_conf: Optional containers conf (as read from containers_conf.yml), will be used in place | |
338 of containers_config_file | |
339 :type containers_conf: dict | |
340 :returns: Instantiated container interfaces with keys corresponding to ``containers`` keys | |
341 :rtype: dict of :class:`ContainerInterface` subclass instances | |
342 """ | |
343 if not containers_conf: | |
344 containers_conf = parse_containers_config(containers_config_file) | |
345 interface_classes = _get_interface_modules() | |
346 interfaces = {} | |
347 for k, conf in containers_conf.items(): | |
348 container_type = conf.get('type', DEFAULT_CONTAINER_TYPE) | |
349 assert container_type in interface_classes, "unknown container interface type: %s" % container_type | |
350 interfaces[k] = interface_classes[container_type](conf, k, containers_config_file) | |
351 return interfaces | |
352 | |
353 | |
354 def parse_containers_config(containers_config_file): | |
355 """Parse a ``containers_conf.yml`` and return the contents of its ``containers`` dictionary. | |
356 | |
357 :param containers_config_file: Filename of containers_conf.yml | |
358 :type containers_config_file: str | |
359 :returns: Contents of the dictionary under the ``containers`` key | |
360 :rtype: dict | |
361 """ | |
362 conf = DEFAULT_CONF.copy() | |
363 try: | |
364 with open(containers_config_file) as fh: | |
365 c = yaml.safe_load(fh) | |
366 conf.update(c.get('containers', {})) | |
367 except OSError as exc: | |
368 if exc.errno == errno.ENOENT: | |
369 log.debug("config file '%s' does not exist, running with default config", containers_config_file) | |
370 else: | |
371 raise | |
372 return conf | |
373 | |
374 | |
375 def _get_interface_modules(): | |
376 interfaces = [] | |
377 modules = import_submodules(sys.modules[__name__]) | |
378 for module in modules: | |
379 module_names = [getattr(module, _) for _ in dir(module)] | |
380 classes = [_ for _ in module_names if inspect.isclass(_) and | |
381 not _ == ContainerInterface and issubclass(_, ContainerInterface)] | |
382 interfaces.extend(classes) | |
383 return {x.container_type: x for x in interfaces} |