gluetool 3.2__tar.gz → 3.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {gluetool-3.2 → gluetool-3.4}/PKG-INFO +1 -2
- {gluetool-3.2 → gluetool-3.4}/gluetool/action.py +63 -74
- {gluetool-3.2 → gluetool-3.4}/gluetool/glue.py +10 -0
- {gluetool-3.2 → gluetool-3.4}/gluetool/log.py +8 -8
- gluetool-3.4/gluetool/tests/test_action.py +109 -0
- {gluetool-3.2 → gluetool-3.4}/gluetool/tests/test_tool.py +66 -0
- {gluetool-3.2 → gluetool-3.4}/gluetool/tool.py +31 -1
- {gluetool-3.2 → gluetool-3.4}/pyproject.toml +6 -4
- gluetool-3.2/gluetool/core.moduleinfo +0 -33
- gluetool-3.2/gluetool_modules/dep_list.py +0 -285
- {gluetool-3.2 → gluetool-3.4}/README.rst +0 -0
- {gluetool-3.2 → gluetool-3.4}/assets/html-log/prism.css +0 -0
- {gluetool-3.2 → gluetool-3.4}/assets/html-log/prism.js +0 -0
- {gluetool-3.2 → gluetool-3.4}/assets/html-log/semantic.min.css +0 -0
- {gluetool-3.2 → gluetool-3.4}/assets/html-log/semantic.min.js +0 -0
- {gluetool-3.2 → gluetool-3.4}/gluetool/__init__.py +0 -0
- {gluetool-3.2 → gluetool-3.4}/gluetool/color.py +0 -0
- {gluetool-3.2 → gluetool-3.4}/gluetool/help.py +0 -0
- {gluetool-3.2 → gluetool-3.4}/gluetool/html_log.py +0 -0
- {gluetool-3.2 → gluetool-3.4}/gluetool/proxy.py +0 -0
- {gluetool-3.2 → gluetool-3.4}/gluetool/py.typed +0 -0
- {gluetool-3.2 → gluetool-3.4}/gluetool/pylint/__init__.py +0 -0
- {gluetool-3.2 → gluetool-3.4}/gluetool/pylint/option_default.py +0 -0
- {gluetool-3.2 → gluetool-3.4}/gluetool/pylint/shared_defined.py +0 -0
- {gluetool-3.2 → gluetool-3.4}/gluetool/pylint/unknown_option.py +0 -0
- {gluetool-3.2 → gluetool-3.4}/gluetool/result.py +0 -0
- {gluetool-3.2 → gluetool-3.4}/gluetool/sentry.py +0 -0
- {gluetool-3.2 → gluetool-3.4}/gluetool/tests/__init__.py +0 -0
- {gluetool-3.2 → gluetool-3.4}/gluetool/tests/assets/parse_config/configroot/config/data_config_root_a +0 -0
- {gluetool-3.2 → gluetool-3.4}/gluetool/tests/assets/parse_config/configroot/config/data_config_root_b +0 -0
- {gluetool-3.2 → gluetool-3.4}/gluetool/tests/assets/parse_config/configroot/config/data_not_unicode +0 -0
- {gluetool-3.2 → gluetool-3.4}/gluetool/tests/assets/parse_config/configroota/config/data_config_root_a +0 -0
- {gluetool-3.2 → gluetool-3.4}/gluetool/tests/assets/parse_config/configrootb/config/data_config_root_b +0 -0
- {gluetool-3.2 → gluetool-3.4}/gluetool/tests/conftest.py +0 -0
- {gluetool-3.2 → gluetool-3.4}/gluetool/tests/test_core.py +0 -0
- {gluetool-3.2 → gluetool-3.4}/gluetool/tests/test_error.py +0 -0
- {gluetool-3.2 → gluetool-3.4}/gluetool/tests/test_eval_context.py +0 -0
- {gluetool-3.2 → gluetool-3.4}/gluetool/tests/test_help.py +0 -0
- {gluetool-3.2 → gluetool-3.4}/gluetool/tests/test_json.py +0 -0
- {gluetool-3.2 → gluetool-3.4}/gluetool/tests/test_load_yaml.py +0 -0
- {gluetool-3.2 → gluetool-3.4}/gluetool/tests/test_log_exception.py +0 -0
- {gluetool-3.2 → gluetool-3.4}/gluetool/tests/test_logging.py +0 -0
- {gluetool-3.2 → gluetool-3.4}/gluetool/tests/test_module_discovery.py +0 -0
- {gluetool-3.2 → gluetool-3.4}/gluetool/tests/test_new_xml_element.py +0 -0
- {gluetool-3.2 → gluetool-3.4}/gluetool/tests/test_normalize_option.py +0 -0
- {gluetool-3.2 → gluetool-3.4}/gluetool/tests/test_option.py +0 -0
- {gluetool-3.2 → gluetool-3.4}/gluetool/tests/test_parse_config.py +0 -0
- {gluetool-3.2 → gluetool-3.4}/gluetool/tests/test_pipeline_step.py +0 -0
- {gluetool-3.2 → gluetool-3.4}/gluetool/tests/test_render_template.py +0 -0
- {gluetool-3.2 → gluetool-3.4}/gluetool/tests/test_requests.py +0 -0
- {gluetool-3.2 → gluetool-3.4}/gluetool/tests/test_result.py +0 -0
- {gluetool-3.2 → gluetool-3.4}/gluetool/tests/test_run_command.py +0 -0
- {gluetool-3.2 → gluetool-3.4}/gluetool/tests/test_run_modules.py +0 -0
- {gluetool-3.2 → gluetool-3.4}/gluetool/tests/test_treat_url.py +0 -0
- {gluetool-3.2 → gluetool-3.4}/gluetool/tests/test_utils.py +0 -0
- {gluetool-3.2 → gluetool-3.4}/gluetool/tests/test_wait.py +0 -0
- {gluetool-3.2 → gluetool-3.4}/gluetool/utils.py +0 -0
- {gluetool-3.2 → gluetool-3.4}/gluetool/version.py +0 -0
- {gluetool-3.2 → gluetool-3.4}/gluetool_modules/__init__.py +0 -0
- {gluetool-3.2 → gluetool-3.4}/gluetool_modules/bash_completion.py +0 -0
- {gluetool-3.2 → gluetool-3.4}/gluetool_modules/yaml_pipeline.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: gluetool
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.4
|
|
4
4
|
Summary: Python framework for constructing command-line pipelines.
|
|
5
5
|
Home-page: https://gluetool.readthedocs.org/
|
|
6
6
|
License: BSD
|
|
@@ -30,7 +30,6 @@ Requires-Dist: docutils
|
|
|
30
30
|
Requires-Dist: lxml
|
|
31
31
|
Requires-Dist: mock
|
|
32
32
|
Requires-Dist: mypy-extensions
|
|
33
|
-
Requires-Dist: packaging
|
|
34
33
|
Requires-Dist: psutil
|
|
35
34
|
Requires-Dist: requests
|
|
36
35
|
Requires-Dist: requests-toolbelt
|
|
@@ -3,9 +3,9 @@ Actions are pieces of workflow. Actions have a name and start/end time, and acti
|
|
|
3
3
|
|
|
4
4
|
This module provides simple instrumentation to enable logging of actions, and - when client
|
|
5
5
|
libraries are installed - submission of actions to storages compatible with
|
|
6
|
-
<https://
|
|
6
|
+
`OpenTelemetry <https://opentelemetry.io/>`_.
|
|
7
7
|
|
|
8
|
-
Actions - which are wrapping
|
|
8
|
+
Actions - which are wrapping OpenTelemetry's _span_ objects - can form an acyclic graph - actions
|
|
9
9
|
can have a single parent, it is then possible to start with a "root" action (e.g. "running the pipeline")
|
|
10
10
|
from which are children actions spawned (e.g. "executing module") and these actions can become parents
|
|
11
11
|
to actions spawned deeper in modules, end so on and on, forming a nice overview of what the code did, how
|
|
@@ -34,37 +34,43 @@ When a supported tracing client is installed, spans created by actions actions a
|
|
|
34
34
|
|
|
35
35
|
Supported clients:
|
|
36
36
|
|
|
37
|
-
* https://
|
|
37
|
+
* `OpenTelemetry SDK <https://opentelemetry-python.readthedocs.io/>`_ with OTLP exporter
|
|
38
38
|
|
|
39
39
|
To control behavior of tracing subsystem, you can use following environment variables:
|
|
40
40
|
|
|
41
41
|
* ``GLUETOOL_TRACING_DISABLE`` - when set to anything, tracing won't be enabled even when a client
|
|
42
42
|
is available.
|
|
43
43
|
* ``GLUETOOL_TRACING_SERVICE_NAME`` - given string is used to name the trace produced by ``gluetool`` execution.
|
|
44
|
-
* ``
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
* ``GLUETOOL_TRACING_REPORTING_ENDPOINT`` - OTLP endpoint URL (e.g. ``http://localhost:4317``).
|
|
45
|
+
* ``GLUETOOL_TRACING_INSECURE`` - controls whether the gRPC connection uses TLS. The connection
|
|
46
|
+
is insecure (plaintext) by default. Set to ``0``, ``false``, or ``no`` to enable TLS.
|
|
47
|
+
* ``GLUETOOL_TRACING_TLS_CERT`` - path to a custom CA certificate bundle (PEM) for TLS connections.
|
|
48
|
+
Only used when ``GLUETOOL_TRACING_INSECURE`` is disabled.
|
|
48
49
|
"""
|
|
49
50
|
|
|
50
51
|
import datetime
|
|
51
52
|
import os
|
|
52
53
|
import threading
|
|
53
|
-
import time
|
|
54
54
|
|
|
55
55
|
from six import iteritems
|
|
56
56
|
|
|
57
57
|
from .log import Logging
|
|
58
|
-
from .result import Result
|
|
59
58
|
|
|
60
59
|
try:
|
|
61
60
|
if os.getenv('GLUETOOL_TRACING_DISABLE'):
|
|
62
61
|
raise ImportError('Tracing is disabled')
|
|
63
62
|
|
|
64
|
-
import
|
|
63
|
+
import grpc
|
|
64
|
+
from opentelemetry import trace
|
|
65
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
66
|
+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|
67
|
+
from opentelemetry.sdk.resources import Resource
|
|
68
|
+
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
|
|
65
69
|
|
|
66
|
-
|
|
67
|
-
|
|
70
|
+
HAS_TRACING = True
|
|
71
|
+
|
|
72
|
+
except ImportError:
|
|
73
|
+
HAS_TRACING = False
|
|
68
74
|
|
|
69
75
|
# Type annotations
|
|
70
76
|
# pylint: disable=unused-import, wrong-import-order
|
|
@@ -72,18 +78,16 @@ from typing import cast, Any, Dict, Generator, Iterator, List, Optional, Union
|
|
|
72
78
|
|
|
73
79
|
from .log import ContextAdapter # noqa
|
|
74
80
|
|
|
75
|
-
TracingClientType = Any # pylint: disable=invalid-name
|
|
76
|
-
|
|
77
81
|
|
|
78
82
|
TRACING_DISABLE_ENVVAR = 'GLUETOOL_TRACING_DISABLE'
|
|
79
83
|
TRACING_SERVICE_NAME_ENVVAR = 'GLUETOOL_TRACING_SERVICE_NAME'
|
|
80
|
-
|
|
81
|
-
TRACING_REPORTING_PORT_ENVVAR = 'GLUETOOL_TRACING_REPORTING_PORT'
|
|
84
|
+
TRACING_REPORTING_ENDPOINT_ENVVAR = 'GLUETOOL_TRACING_REPORTING_ENDPOINT'
|
|
82
85
|
TRACING_FLUSH_TIMEOUT_ENVVAR = 'GLUETOOL_TRACING_FLUSH_TIMEOUT'
|
|
86
|
+
TRACING_INSECURE_ENVVAR = 'GLUETOOL_TRACING_INSECURE'
|
|
87
|
+
TRACING_TLS_CERT_ENVVAR = 'GLUETOOL_TRACING_TLS_CERT'
|
|
83
88
|
|
|
84
89
|
DEFAULT_TRACING_SERVICE_NAME = 'gluetool'
|
|
85
|
-
|
|
86
|
-
DEFAULT_TRACING_REPORTING_PORT = 5775
|
|
90
|
+
DEFAULT_TRACING_REPORTING_ENDPOINT = 'http://localhost:4317'
|
|
87
91
|
DEFAULT_TRACING_FLUSH_TIMEOUT = 30
|
|
88
92
|
|
|
89
93
|
|
|
@@ -93,54 +97,50 @@ class Tracer(object):
|
|
|
93
97
|
|
|
94
98
|
:param str service_name: name to apply to all traces produced by this tracer.
|
|
95
99
|
:param ContextAdapter logger: logger to use for logging.
|
|
96
|
-
:param str
|
|
97
|
-
:param int reporting_port: port to which tracer should submit tracers.
|
|
100
|
+
:param str endpoint: OTLP endpoint URL to which tracer should submit traces.
|
|
98
101
|
"""
|
|
99
102
|
# pylint: disable=too-few-public-methods
|
|
100
103
|
|
|
101
|
-
TRACER: Optional[
|
|
104
|
+
TRACER: Optional[Any] = None
|
|
105
|
+
_PROVIDER: Optional[Any] = None
|
|
102
106
|
|
|
103
|
-
# pylint: disable=too-many-arguments
|
|
104
107
|
def __init__(self,
|
|
105
108
|
service_name: Optional[str] = None,
|
|
106
109
|
logger: Optional[ContextAdapter] = None,
|
|
107
|
-
|
|
108
|
-
reporting_port: Optional[int] = None) -> None:
|
|
110
|
+
endpoint: Optional[str] = None) -> None:
|
|
109
111
|
|
|
110
|
-
if not
|
|
112
|
+
if not HAS_TRACING or os.getenv(TRACING_DISABLE_ENVVAR):
|
|
111
113
|
return
|
|
112
114
|
|
|
113
115
|
self.logger = logger or Logging.get_logger()
|
|
114
116
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
+
resolved_service_name: str = service_name or os.getenv(
|
|
118
|
+
TRACING_SERVICE_NAME_ENVVAR, DEFAULT_TRACING_SERVICE_NAME
|
|
119
|
+
) or DEFAULT_TRACING_SERVICE_NAME
|
|
117
120
|
|
|
118
|
-
|
|
119
|
-
|
|
121
|
+
resolved_endpoint: str = endpoint or os.getenv(
|
|
122
|
+
TRACING_REPORTING_ENDPOINT_ENVVAR, DEFAULT_TRACING_REPORTING_ENDPOINT
|
|
123
|
+
) or DEFAULT_TRACING_REPORTING_ENDPOINT
|
|
120
124
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
'logging': True
|
|
138
|
-
},
|
|
139
|
-
service_name=service_name,
|
|
140
|
-
validate=True
|
|
141
|
-
)
|
|
125
|
+
insecure = os.getenv(TRACING_INSECURE_ENVVAR, '').lower() not in ('0', 'false', 'no')
|
|
126
|
+
tls_cert = os.getenv(TRACING_TLS_CERT_ENVVAR)
|
|
127
|
+
|
|
128
|
+
resource = Resource.create({'service.name': resolved_service_name})
|
|
129
|
+
provider = TracerProvider(resource=resource)
|
|
130
|
+
|
|
131
|
+
if insecure:
|
|
132
|
+
exporter = OTLPSpanExporter(endpoint=resolved_endpoint, insecure=True)
|
|
133
|
+
elif tls_cert:
|
|
134
|
+
with open(tls_cert, 'rb') as f:
|
|
135
|
+
credentials = grpc.ssl_channel_credentials(root_certificates=f.read())
|
|
136
|
+
exporter = OTLPSpanExporter(endpoint=resolved_endpoint, credentials=credentials)
|
|
137
|
+
else:
|
|
138
|
+
exporter = OTLPSpanExporter(endpoint=resolved_endpoint)
|
|
139
|
+
provider.add_span_processor(BatchSpanProcessor(exporter))
|
|
140
|
+
trace.set_tracer_provider(provider)
|
|
142
141
|
|
|
143
|
-
Tracer.
|
|
142
|
+
Tracer._PROVIDER = provider
|
|
143
|
+
Tracer.TRACER = trace.get_tracer(resolved_service_name)
|
|
144
144
|
|
|
145
145
|
def close(self, flush_timeout: Optional[int] = None, logger: Optional[ContextAdapter] = None) -> None:
|
|
146
146
|
"""
|
|
@@ -151,31 +151,19 @@ class Tracer(object):
|
|
|
151
151
|
:param ContextAdapter logger: logger to use for logging.
|
|
152
152
|
""".format(TRACING_FLUSH_TIMEOUT_ENVVAR, DEFAULT_TRACING_FLUSH_TIMEOUT)
|
|
153
153
|
|
|
154
|
-
if not Tracer.
|
|
154
|
+
if not Tracer._PROVIDER:
|
|
155
155
|
return
|
|
156
156
|
|
|
157
157
|
logger = logger or self.logger
|
|
158
158
|
|
|
159
159
|
logger.info('Flushing tracing data')
|
|
160
160
|
|
|
161
|
-
# Make pylint happy about circular imports by not using global import.
|
|
162
|
-
# pylint: disable=cyclic-import
|
|
163
|
-
from .utils import wait
|
|
164
|
-
|
|
165
161
|
if not flush_timeout:
|
|
166
162
|
# pylint: disable=bad-option-value,invalid-envvar-default
|
|
167
163
|
flush_timeout = int(os.getenv(TRACING_FLUSH_TIMEOUT_ENVVAR, default=DEFAULT_TRACING_FLUSH_TIMEOUT))
|
|
168
164
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
future = Tracer.TRACER.close()
|
|
173
|
-
|
|
174
|
-
def _check_flush() -> Result[bool, str]:
|
|
175
|
-
|
|
176
|
-
return Result.Ok(True) if future.done() else Result.Error('flush pending')
|
|
177
|
-
|
|
178
|
-
wait('tracing flush', _check_flush, timeout=flush_timeout, tick=2, logger=logger)
|
|
165
|
+
Tracer._PROVIDER.force_flush(timeout_millis=flush_timeout * 1000)
|
|
166
|
+
Tracer._PROVIDER.shutdown()
|
|
179
167
|
|
|
180
168
|
|
|
181
169
|
class Action(object):
|
|
@@ -287,14 +275,15 @@ class Action(object):
|
|
|
287
275
|
|
|
288
276
|
self.tags = tags or {}
|
|
289
277
|
|
|
290
|
-
if Tracer.TRACER:
|
|
291
|
-
if parent:
|
|
292
|
-
|
|
293
|
-
|
|
278
|
+
if Tracer.TRACER and HAS_TRACING:
|
|
279
|
+
if parent and parent.span:
|
|
280
|
+
parent_ctx = trace.set_span_in_context(parent.span)
|
|
294
281
|
else:
|
|
295
|
-
|
|
282
|
+
parent_ctx = None
|
|
296
283
|
|
|
297
|
-
self.span: Any = Tracer.TRACER.start_span(
|
|
284
|
+
self.span: Any = Tracer.TRACER.start_span(
|
|
285
|
+
label, context=parent_ctx, attributes=tags
|
|
286
|
+
)
|
|
298
287
|
|
|
299
288
|
else:
|
|
300
289
|
self.span = None
|
|
@@ -323,7 +312,7 @@ class Action(object):
|
|
|
323
312
|
Action._drop_action(self)
|
|
324
313
|
|
|
325
314
|
if self.span:
|
|
326
|
-
self.span.
|
|
315
|
+
self.span.end()
|
|
327
316
|
|
|
328
317
|
self.logger.debug("action '{}', child of '{}', with span '{}', finished at {}".format(
|
|
329
318
|
self.label,
|
|
@@ -345,7 +334,7 @@ class Action(object):
|
|
|
345
334
|
self.tags[name] = value
|
|
346
335
|
|
|
347
336
|
if self.span:
|
|
348
|
-
self.span.
|
|
337
|
+
self.span.set_attribute(name, value)
|
|
349
338
|
|
|
350
339
|
def set_tags(self, tags: Dict[str, Any]) -> None:
|
|
351
340
|
|
|
@@ -1788,6 +1788,15 @@ class Glue(Configurable):
|
|
|
1788
1788
|
'action': 'store_true',
|
|
1789
1789
|
'default': False
|
|
1790
1790
|
},
|
|
1791
|
+
'sigterm-timeout': {
|
|
1792
|
+
'help': """
|
|
1793
|
+
Time, in seconds, to wait for a child process to exit after sending it ``SIGTERM``,
|
|
1794
|
+
before resorting to ``SIGKILL``. Takes precedence over the ``GLUETOOL_SIGTERM_TIMEOUT``
|
|
1795
|
+
environment variable (Default: 60).
|
|
1796
|
+
""",
|
|
1797
|
+
'type': int,
|
|
1798
|
+
'default': None
|
|
1799
|
+
},
|
|
1791
1800
|
}),
|
|
1792
1801
|
('Output control', {
|
|
1793
1802
|
('c', 'colors'): {
|
|
@@ -2510,6 +2519,7 @@ class Glue(Configurable):
|
|
|
2510
2519
|
* GLUETOOL_TRACING_SERVICE_NAME (string) - name of the trace produced by tool execution
|
|
2511
2520
|
* GLUETOOL_TRACING_REPORTING_HOST (string) - a hostname where tracing collector listens
|
|
2512
2521
|
* GLUETOOL_TRACING_REPORTING_PORT (int) - a port on which tracing collector listens
|
|
2522
|
+
* GLUETOOL_SIGTERM_TIMEOUT (int) - seconds to wait for a child to exit after SIGTERM before SIGKILL
|
|
2513
2523
|
""").format(module_dirs, data_dirs, module_config_dirs)
|
|
2514
2524
|
|
|
2515
2525
|
self._parse_args(args,
|
|
@@ -1132,17 +1132,17 @@ class Logging(object):
|
|
|
1132
1132
|
assert Logging.stderr_handler is not None
|
|
1133
1133
|
logger.addHandler(Logging.stderr_handler)
|
|
1134
1134
|
|
|
1135
|
-
# Extra handling for
|
|
1136
|
-
# - replace it with a context adapter to keep track of what's being done by
|
|
1137
|
-
# - patch its `info` with `debug` -
|
|
1135
|
+
# Extra handling for OpenTelemetry logger:
|
|
1136
|
+
# - replace it with a context adapter to keep track of what's being done by tracing code,
|
|
1137
|
+
# - patch its `info` with `debug` - OpenTelemetry logs with INFO severity, which spoils
|
|
1138
1138
|
# our output, DEBUG is perfectly fine for us.
|
|
1139
|
-
|
|
1139
|
+
otel_logger = logging.getLogger('opentelemetry')
|
|
1140
1140
|
|
|
1141
|
-
if logger ==
|
|
1142
|
-
|
|
1141
|
+
if logger == otel_logger:
|
|
1142
|
+
otel_context_adapter = PackageAdapter(Logging.get_logger(), 'tracing')
|
|
1143
1143
|
|
|
1144
1144
|
for attr in ('debug', 'info', 'warning', 'error', 'exception'):
|
|
1145
|
-
setattr(logger, attr, getattr(
|
|
1145
|
+
setattr(logger, attr, getattr(otel_context_adapter, attr))
|
|
1146
1146
|
|
|
1147
1147
|
logger.info = logger.debug # type: ignore
|
|
1148
1148
|
|
|
@@ -1172,7 +1172,7 @@ class Logging(object):
|
|
|
1172
1172
|
|
|
1173
1173
|
OUR_LOGGERS = (
|
|
1174
1174
|
logging.getLogger('gluetool'),
|
|
1175
|
-
logging.getLogger('
|
|
1175
|
+
logging.getLogger('opentelemetry'),
|
|
1176
1176
|
logging.getLogger('urllib3')
|
|
1177
1177
|
)
|
|
1178
1178
|
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from unittest.mock import patch, MagicMock
|
|
5
|
+
|
|
6
|
+
import gluetool.action
|
|
7
|
+
from gluetool.action import (
|
|
8
|
+
TRACING_INSECURE_ENVVAR,
|
|
9
|
+
TRACING_TLS_CERT_ENVVAR,
|
|
10
|
+
Tracer,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@pytest.fixture(autouse=True)
|
|
15
|
+
def _reset_tracer():
|
|
16
|
+
"""Reset Tracer class state between tests."""
|
|
17
|
+
Tracer.TRACER = None
|
|
18
|
+
Tracer._PROVIDER = None
|
|
19
|
+
yield
|
|
20
|
+
Tracer.TRACER = None
|
|
21
|
+
Tracer._PROVIDER = None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@pytest.fixture()
|
|
25
|
+
def mock_otel():
|
|
26
|
+
"""Mock all OpenTelemetry objects used by Tracer.__init__."""
|
|
27
|
+
with patch.object(gluetool.action, 'HAS_TRACING', True), \
|
|
28
|
+
patch.object(gluetool.action, 'Resource'), \
|
|
29
|
+
patch.object(gluetool.action, 'TracerProvider'), \
|
|
30
|
+
patch.object(gluetool.action, 'BatchSpanProcessor'), \
|
|
31
|
+
patch.object(gluetool.action, 'OTLPSpanExporter') as mock_exporter_cls, \
|
|
32
|
+
patch.object(gluetool.action, 'trace'), \
|
|
33
|
+
patch.object(gluetool.action, 'grpc', create=True) as mock_grpc:
|
|
34
|
+
yield {
|
|
35
|
+
'exporter_cls': mock_exporter_cls,
|
|
36
|
+
'grpc': mock_grpc,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@pytest.mark.parametrize('env_value', [
|
|
41
|
+
'', # not set
|
|
42
|
+
'true',
|
|
43
|
+
'yes',
|
|
44
|
+
'1',
|
|
45
|
+
'anything',
|
|
46
|
+
])
|
|
47
|
+
def test_insecure_by_default(env_value, mock_otel):
|
|
48
|
+
"""Connection is insecure when GLUETOOL_TRACING_INSECURE is unset or not explicitly disabled."""
|
|
49
|
+
|
|
50
|
+
env = {TRACING_INSECURE_ENVVAR: env_value} if env_value else {}
|
|
51
|
+
|
|
52
|
+
with patch.dict(os.environ, env, clear=False):
|
|
53
|
+
if not env_value:
|
|
54
|
+
os.environ.pop(TRACING_INSECURE_ENVVAR, None)
|
|
55
|
+
Tracer()
|
|
56
|
+
|
|
57
|
+
_, kwargs = mock_otel['exporter_cls'].call_args
|
|
58
|
+
assert kwargs.get('insecure') is True
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@pytest.mark.parametrize('env_value', ['0', 'false', 'no', 'False', 'NO', 'No'])
|
|
62
|
+
def test_secure_when_insecure_disabled(env_value, mock_otel):
|
|
63
|
+
"""Connection uses TLS when GLUETOOL_TRACING_INSECURE is set to 0/false/no."""
|
|
64
|
+
|
|
65
|
+
with patch.dict(os.environ, {TRACING_INSECURE_ENVVAR: env_value}, clear=False):
|
|
66
|
+
os.environ.pop(TRACING_TLS_CERT_ENVVAR, None)
|
|
67
|
+
Tracer()
|
|
68
|
+
|
|
69
|
+
_, kwargs = mock_otel['exporter_cls'].call_args
|
|
70
|
+
assert 'insecure' not in kwargs
|
|
71
|
+
assert 'credentials' not in kwargs
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_tls_cert_used_when_secure(mock_otel, tmp_path):
|
|
75
|
+
"""Custom CA cert is loaded and passed as gRPC credentials when TLS is enabled."""
|
|
76
|
+
|
|
77
|
+
cert_content = b'--- FAKE CA CERT ---'
|
|
78
|
+
cert_file = tmp_path / 'ca.pem'
|
|
79
|
+
cert_file.write_bytes(cert_content)
|
|
80
|
+
|
|
81
|
+
mock_creds = MagicMock()
|
|
82
|
+
mock_otel['grpc'].ssl_channel_credentials.return_value = mock_creds
|
|
83
|
+
|
|
84
|
+
with patch.dict(os.environ, {
|
|
85
|
+
TRACING_INSECURE_ENVVAR: 'false',
|
|
86
|
+
TRACING_TLS_CERT_ENVVAR: str(cert_file),
|
|
87
|
+
}, clear=False):
|
|
88
|
+
Tracer()
|
|
89
|
+
|
|
90
|
+
mock_otel['grpc'].ssl_channel_credentials.assert_called_once_with(root_certificates=cert_content)
|
|
91
|
+
_, kwargs = mock_otel['exporter_cls'].call_args
|
|
92
|
+
assert kwargs.get('credentials') is mock_creds
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_tls_cert_ignored_when_insecure(mock_otel, tmp_path):
|
|
96
|
+
"""GLUETOOL_TRACING_TLS_CERT is ignored when connection is insecure."""
|
|
97
|
+
|
|
98
|
+
cert_file = tmp_path / 'ca.pem'
|
|
99
|
+
cert_file.write_bytes(b'--- FAKE CA CERT ---')
|
|
100
|
+
|
|
101
|
+
with patch.dict(os.environ, {
|
|
102
|
+
TRACING_TLS_CERT_ENVVAR: str(cert_file),
|
|
103
|
+
}, clear=False):
|
|
104
|
+
os.environ.pop(TRACING_INSECURE_ENVVAR, None)
|
|
105
|
+
Tracer()
|
|
106
|
+
|
|
107
|
+
mock_otel['grpc'].ssl_channel_credentials.assert_not_called()
|
|
108
|
+
_, kwargs = mock_otel['exporter_cls'].call_args
|
|
109
|
+
assert kwargs.get('insecure') is True
|
|
@@ -175,3 +175,69 @@ def test_signal(monkeypatch, log, tested_signal, exception, terminate_process_tr
|
|
|
175
175
|
assert log.match(levelno=logging.WARNING, message=warnmsg)
|
|
176
176
|
assert log.match(levelno=logging.DEBUG, message='Exiting with status 0')
|
|
177
177
|
assert log.match(levelno=logging.INFO, message='gluetool some-version')
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def test_sigterm_timeout_default(monkeypatch):
|
|
181
|
+
monkeypatch.delenv(gluetool.tool.SIGTERM_TIMEOUT_ENVVAR, raising=False)
|
|
182
|
+
|
|
183
|
+
assert gluetool.tool.sigterm_timeout() == gluetool.tool.DEFAULT_SIGTERM_TIMEOUT
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@pytest.mark.parametrize('value,expected', [
|
|
187
|
+
('0', 0),
|
|
188
|
+
('1', 1),
|
|
189
|
+
('120', 120),
|
|
190
|
+
], ids=['zero', 'one', 'custom'])
|
|
191
|
+
def test_sigterm_timeout_envvar(monkeypatch, value, expected):
|
|
192
|
+
monkeypatch.setenv(gluetool.tool.SIGTERM_TIMEOUT_ENVVAR, value)
|
|
193
|
+
|
|
194
|
+
assert gluetool.tool.sigterm_timeout() == expected
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def test_sigterm_timeout_envvar_invalid(monkeypatch):
|
|
198
|
+
monkeypatch.setenv(gluetool.tool.SIGTERM_TIMEOUT_ENVVAR, 'not-a-number')
|
|
199
|
+
|
|
200
|
+
with pytest.raises(ValueError):
|
|
201
|
+
gluetool.tool.sigterm_timeout()
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def test_sigterm_timeout_option_overrides_envvar(monkeypatch):
|
|
205
|
+
# The `sigterm-timeout` option (command line or configuration file) takes precedence over the env variable.
|
|
206
|
+
monkeypatch.setenv(gluetool.tool.SIGTERM_TIMEOUT_ENVVAR, '30')
|
|
207
|
+
|
|
208
|
+
glue = MagicMock()
|
|
209
|
+
glue.option = MagicMock(return_value=90)
|
|
210
|
+
|
|
211
|
+
assert gluetool.tool.sigterm_timeout(glue) == 90
|
|
212
|
+
glue.option.assert_called_once_with('sigterm-timeout')
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def test_sigterm_timeout_option_unset_falls_back_to_envvar(monkeypatch):
|
|
216
|
+
# When the option is not set, the env variable is used.
|
|
217
|
+
monkeypatch.setenv(gluetool.tool.SIGTERM_TIMEOUT_ENVVAR, '30')
|
|
218
|
+
|
|
219
|
+
glue = MagicMock()
|
|
220
|
+
glue.option = MagicMock(return_value=None)
|
|
221
|
+
|
|
222
|
+
assert gluetool.tool.sigterm_timeout(glue) == 30
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def test_sigterm_timeout_option_unset_falls_back_to_default(monkeypatch):
|
|
226
|
+
# When neither the option nor the env variable is set, the default is used.
|
|
227
|
+
monkeypatch.delenv(gluetool.tool.SIGTERM_TIMEOUT_ENVVAR, raising=False)
|
|
228
|
+
|
|
229
|
+
glue = MagicMock()
|
|
230
|
+
glue.option = MagicMock(return_value=None)
|
|
231
|
+
|
|
232
|
+
assert gluetool.tool.sigterm_timeout(glue) == gluetool.tool.DEFAULT_SIGTERM_TIMEOUT
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def test_sigterm_timeout_option_from_cmdline(monkeypatch):
|
|
236
|
+
# The `sigterm-timeout` option is wired into the command-line parser and parsed as an integer.
|
|
237
|
+
monkeypatch.delenv(gluetool.tool.SIGTERM_TIMEOUT_ENVVAR, raising=False)
|
|
238
|
+
|
|
239
|
+
glue = NonLoadingGlue()
|
|
240
|
+
glue._parse_args(['--sigterm-timeout', '120'])
|
|
241
|
+
|
|
242
|
+
assert glue.option('sigterm-timeout') == 120
|
|
243
|
+
assert gluetool.tool.sigterm_timeout(glue) == 120
|
|
@@ -44,6 +44,36 @@ DEFAULT_GLUETOOL_CONFIG_PATHS = [
|
|
|
44
44
|
|
|
45
45
|
DEFAULT_SIGTERM_TIMEOUT = 60
|
|
46
46
|
|
|
47
|
+
# Environment variable overriding the time (in seconds) ``gluetool`` waits for a child process to exit after
|
|
48
|
+
# sending it ``SIGTERM`` before resorting to ``SIGKILL``.
|
|
49
|
+
SIGTERM_TIMEOUT_ENVVAR = 'GLUETOOL_SIGTERM_TIMEOUT'
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def sigterm_timeout(glue: Optional['gluetool.glue.Glue'] = None) -> int:
|
|
53
|
+
"""
|
|
54
|
+
Return the time, in seconds, ``gluetool`` waits for a child process to exit after sending it ``SIGTERM``
|
|
55
|
+
before resorting to ``SIGKILL``.
|
|
56
|
+
|
|
57
|
+
The value is resolved with the following precedence:
|
|
58
|
+
|
|
59
|
+
1. the ``sigterm-timeout`` option (set on the command line or in a configuration file),
|
|
60
|
+
2. the :py:data:`SIGTERM_TIMEOUT_ENVVAR` environment variable,
|
|
61
|
+
3. :py:data:`DEFAULT_SIGTERM_TIMEOUT`.
|
|
62
|
+
|
|
63
|
+
:param glue: ``Glue`` instance to read the ``sigterm-timeout`` option from. When not provided, the option
|
|
64
|
+
is skipped and the value is resolved from the environment variable or the default.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
if glue is not None:
|
|
68
|
+
option = glue.option('sigterm-timeout')
|
|
69
|
+
if option is not None:
|
|
70
|
+
return cast(int, option)
|
|
71
|
+
|
|
72
|
+
if SIGTERM_TIMEOUT_ENVVAR in os.environ:
|
|
73
|
+
return int(os.environ[SIGTERM_TIMEOUT_ENVVAR])
|
|
74
|
+
|
|
75
|
+
return DEFAULT_SIGTERM_TIMEOUT
|
|
76
|
+
|
|
47
77
|
|
|
48
78
|
def handle_exc(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
49
79
|
|
|
@@ -305,7 +335,7 @@ Will try to submit it to Sentry but giving up on everything else.
|
|
|
305
335
|
child.terminate()
|
|
306
336
|
|
|
307
337
|
try:
|
|
308
|
-
child.wait(timeout=
|
|
338
|
+
child.wait(timeout=sigterm_timeout(Glue))
|
|
309
339
|
|
|
310
340
|
except psutil.TimeoutExpired:
|
|
311
341
|
Glue.warn("Sending SIGKILL to child process '{}' (PID {})".format(child.name(), child.pid))
|
|
@@ -4,7 +4,7 @@ dynamic = ["version"]
|
|
|
4
4
|
|
|
5
5
|
[tool.poetry]
|
|
6
6
|
name = "gluetool"
|
|
7
|
-
version = "3.
|
|
7
|
+
version = "3.4"
|
|
8
8
|
description = "Python framework for constructing command-line pipelines."
|
|
9
9
|
authors = [
|
|
10
10
|
"Milos Prchlik <mprchlik@redhat.com>",
|
|
@@ -42,7 +42,6 @@ include = [
|
|
|
42
42
|
"assets/html-log/semantic.min.js",
|
|
43
43
|
"gluetool_modules/__init__.py",
|
|
44
44
|
"gluetool_modules/bash_completion.py",
|
|
45
|
-
"gluetool_modules/dep_list.py",
|
|
46
45
|
"gluetool_modules/yaml_pipeline.py"
|
|
47
46
|
]
|
|
48
47
|
|
|
@@ -67,7 +66,6 @@ urlnormalizer = "*"
|
|
|
67
66
|
# depending on extended argument annotations.
|
|
68
67
|
mypy-extensions = "*"
|
|
69
68
|
zipp = "*"
|
|
70
|
-
packaging = "*"
|
|
71
69
|
MarkupSafe = "*"
|
|
72
70
|
attrs = "*"
|
|
73
71
|
cattrs = "*"
|
|
@@ -102,9 +100,13 @@ gluetool-html-log = "gluetool.html_log:main"
|
|
|
102
100
|
|
|
103
101
|
[tool.poetry.plugins."gluetool.modules"]
|
|
104
102
|
".bash_completion" = "gluetool_modules.bash_completion:BashCompletion"
|
|
105
|
-
".dep_list" = "gluetool_modules.dep_list:DepList"
|
|
106
103
|
".yaml_pipeline" = "gluetool_modules.yaml_pipeline:YAMLPipeline"
|
|
107
104
|
|
|
105
|
+
|
|
106
|
+
[tool.poetry.group.dev.dependencies]
|
|
107
|
+
opentelemetry-sdk = "^1.40.0"
|
|
108
|
+
opentelemetry-exporter-otlp-proto-grpc = "^1.40.0"
|
|
109
|
+
|
|
108
110
|
[tool.poetry-dynamic-versioning]
|
|
109
111
|
enable = false
|
|
110
112
|
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
- name: citool-core
|
|
3
|
-
description: Continuous Integration Swiss Army Knife
|
|
4
|
-
dependencies:
|
|
5
|
-
yum:
|
|
6
|
-
- krb5-devel
|
|
7
|
-
pip:
|
|
8
|
-
- alabaster==0.7.10
|
|
9
|
-
- Babel==2.5.1
|
|
10
|
-
- beautifulsoup4==4.5.3
|
|
11
|
-
- certifi==2017.7.27.1
|
|
12
|
-
- chardet==3.0.4
|
|
13
|
-
- colorama==0.3.7
|
|
14
|
-
- contextlib2==0.5.5
|
|
15
|
-
- docutils==0.13.1
|
|
16
|
-
- enum34==1.1.6
|
|
17
|
-
- idna==2.6
|
|
18
|
-
- imagesize==0.7.1
|
|
19
|
-
- Jinja2==2.9.6
|
|
20
|
-
- Mako==1.0.6
|
|
21
|
-
- MarkupSafe==1.0
|
|
22
|
-
- Pygments==2.2.0
|
|
23
|
-
- pytz==2017.2
|
|
24
|
-
- raven==6.0.0
|
|
25
|
-
- requests==2.18.4
|
|
26
|
-
- ruamel.ordereddict==0.4.13
|
|
27
|
-
- ruamel.yaml==0.15.34
|
|
28
|
-
- six==1.11.0
|
|
29
|
-
- snowballstemmer==1.2.1
|
|
30
|
-
- Sphinx==1.5.2
|
|
31
|
-
- tabulate==0.7.7
|
|
32
|
-
- urllib3==1.22
|
|
33
|
-
- urlnorm==1.1.4
|
|
@@ -1,285 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import operator
|
|
3
|
-
|
|
4
|
-
from packaging.version import Version
|
|
5
|
-
|
|
6
|
-
from six import iteritems, itervalues
|
|
7
|
-
|
|
8
|
-
import gluetool
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class ModuleInfo(object):
|
|
12
|
-
# pylint: disable=too-few-public-methods
|
|
13
|
-
def __init__(self, data):
|
|
14
|
-
self.name = data['name']
|
|
15
|
-
self.description = data.get('description', '')
|
|
16
|
-
self.dependencies = data.get('dependencies', {})
|
|
17
|
-
deps = self.dependencies
|
|
18
|
-
|
|
19
|
-
self.repo = self.normalize_repos(deps.get('repo', []))
|
|
20
|
-
self.yum = deps.get('yum', [])
|
|
21
|
-
self.pip = deps.get('pip', [])
|
|
22
|
-
self.tasks = deps.get('ansible_tasks', [])
|
|
23
|
-
|
|
24
|
-
@staticmethod
|
|
25
|
-
def normalize_repos(repos):
|
|
26
|
-
for repo in repos:
|
|
27
|
-
if 'name' not in repo:
|
|
28
|
-
repo['name'] = 'repo' + str(id(repo))
|
|
29
|
-
return repos
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
class ModuleInfoGroup(object):
|
|
33
|
-
def __init__(self, logger):
|
|
34
|
-
self.items = {}
|
|
35
|
-
self.logger = logger
|
|
36
|
-
self.ops_map = {
|
|
37
|
-
'>': operator.gt,
|
|
38
|
-
'<': operator.lt,
|
|
39
|
-
'>=': operator.ge,
|
|
40
|
-
'<=': operator.le,
|
|
41
|
-
'==': operator.eq
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
def add_moduleinfo(self, moduleinfo):
|
|
45
|
-
if moduleinfo.name in self.items:
|
|
46
|
-
self.logger.warn("Module '{}' is already loaded".format(moduleinfo.name))
|
|
47
|
-
return False
|
|
48
|
-
|
|
49
|
-
self.items[moduleinfo.name] = moduleinfo
|
|
50
|
-
return True
|
|
51
|
-
|
|
52
|
-
def get_dependencies(self, only_modules):
|
|
53
|
-
data = {
|
|
54
|
-
'repo': [],
|
|
55
|
-
'yum': [],
|
|
56
|
-
'pip': [],
|
|
57
|
-
'ansible_tasks': []
|
|
58
|
-
}
|
|
59
|
-
for moduleinfo in itervalues(self.items):
|
|
60
|
-
if only_modules and moduleinfo.name not in only_modules:
|
|
61
|
-
self.logger.debug("Skip module '{}'".format(moduleinfo.name))
|
|
62
|
-
continue
|
|
63
|
-
|
|
64
|
-
self.logger.info('Collect dependencies for \'{}\''.format(moduleinfo.name))
|
|
65
|
-
if moduleinfo.yum:
|
|
66
|
-
data['yum'].extend(moduleinfo.yum)
|
|
67
|
-
if moduleinfo.pip:
|
|
68
|
-
data['pip'].extend(moduleinfo.pip)
|
|
69
|
-
if moduleinfo.repo:
|
|
70
|
-
data['repo'].extend(moduleinfo.repo)
|
|
71
|
-
if moduleinfo.tasks:
|
|
72
|
-
data['ansible_tasks'].extend(moduleinfo.tasks)
|
|
73
|
-
|
|
74
|
-
self.logger.debug('pip dependencies:\n{}'.format(data['pip']))
|
|
75
|
-
data['pip'] = self.pip_version_unify(data['pip'])
|
|
76
|
-
|
|
77
|
-
return data
|
|
78
|
-
|
|
79
|
-
def pip_version_unify(self, pip_deps):
|
|
80
|
-
result = []
|
|
81
|
-
versions = {}
|
|
82
|
-
for item in pip_deps:
|
|
83
|
-
pkg, oper, version = ModuleInfoGroup.parse_pkgver(item)
|
|
84
|
-
if pkg in versions:
|
|
85
|
-
version_limit = versions[pkg]
|
|
86
|
-
else:
|
|
87
|
-
version_limit = gluetool.utils.Bunch(pkg=pkg, equal=None, lower=None, upper=None)
|
|
88
|
-
versions[pkg] = version_limit
|
|
89
|
-
if oper and version:
|
|
90
|
-
self.logger.debug(item)
|
|
91
|
-
self.limit_version(version_limit, oper, version)
|
|
92
|
-
self.logger.debug(versions)
|
|
93
|
-
for _, item in iteritems(versions):
|
|
94
|
-
result.append(self.get_allowed_version_bounds(item))
|
|
95
|
-
return result
|
|
96
|
-
|
|
97
|
-
def get_allowed_version_bounds(self, item):
|
|
98
|
-
if not item.equal and not item.lower and not item.upper:
|
|
99
|
-
return item.pkg
|
|
100
|
-
if item.equal:
|
|
101
|
-
if item.upper:
|
|
102
|
-
oper, version = item.upper
|
|
103
|
-
relate = self.ops_map[oper]
|
|
104
|
-
if not relate(Version(item.equal), Version(version)):
|
|
105
|
-
raise gluetool.GlueError("Cannot find common version for package '{}'".format(item.pkg))
|
|
106
|
-
if item.lower:
|
|
107
|
-
oper, version = item.lower
|
|
108
|
-
relate = self.ops_map[oper]
|
|
109
|
-
if not relate(Version(item.equal), Version(version)):
|
|
110
|
-
raise gluetool.GlueError("Cannot find common version for package '{}'".format(item.pkg))
|
|
111
|
-
return '{}=={}'.format(item.pkg, item.equal)
|
|
112
|
-
if item.upper and item.lower:
|
|
113
|
-
lower_operator, lower_version = item.lower
|
|
114
|
-
upper_operator, upper_version = item.upper
|
|
115
|
-
relate = self.ops_map[lower_operator]
|
|
116
|
-
if not relate(Version(upper_version), Version(lower_version)):
|
|
117
|
-
raise gluetool.GlueError("Cannot find common version for package '{}'".format(item.pkg))
|
|
118
|
-
relate = self.ops_map[upper_operator]
|
|
119
|
-
if not relate(Version(lower_version), Version(upper_version)):
|
|
120
|
-
raise gluetool.GlueError("Cannot find common version for package '{}'".format(item.pkg))
|
|
121
|
-
return '{}{}{},{}{}'.format(item.pkg, lower_operator, lower_version, upper_operator, upper_version)
|
|
122
|
-
if item.upper:
|
|
123
|
-
upper_operator, upper_version = item.upper
|
|
124
|
-
return '{}{}{}'.format(item.pkg, upper_operator, upper_version)
|
|
125
|
-
if item.lower:
|
|
126
|
-
lower_operator, lower_version = item.lower
|
|
127
|
-
return '{}{}{}'.format(item.pkg, lower_operator, lower_version)
|
|
128
|
-
|
|
129
|
-
@staticmethod
|
|
130
|
-
def limit_version(item, oper, version):
|
|
131
|
-
if oper in ['>', '>=']:
|
|
132
|
-
if item.lower:
|
|
133
|
-
_, saved_version = item.lower
|
|
134
|
-
if Version(saved_version) < Version(version):
|
|
135
|
-
item.lower = [oper, version]
|
|
136
|
-
elif Version(saved_version) == Version(version) and oper == '>':
|
|
137
|
-
item.lower = [oper, version]
|
|
138
|
-
else:
|
|
139
|
-
item.lower = [oper, version]
|
|
140
|
-
elif oper in ['<', '<=']:
|
|
141
|
-
if item.upper:
|
|
142
|
-
_, saved_version = item.upper
|
|
143
|
-
if Version(saved_version) > Version(version):
|
|
144
|
-
item.upper = [oper, version]
|
|
145
|
-
elif Version(saved_version) == Version(version) and oper == '<':
|
|
146
|
-
item.upper = [oper, version]
|
|
147
|
-
else:
|
|
148
|
-
item.upper = [oper, version]
|
|
149
|
-
elif oper in ['==']:
|
|
150
|
-
if item.equal and item.equal != version:
|
|
151
|
-
raise gluetool.GlueError(
|
|
152
|
-
"Different versions '{}' and '{}' of package '{}' required".format(item.equal, version, item.pkg)
|
|
153
|
-
)
|
|
154
|
-
item.equal = version
|
|
155
|
-
else:
|
|
156
|
-
raise gluetool.GlueError("Unsupported operator '{}'".format(oper))
|
|
157
|
-
|
|
158
|
-
@staticmethod
|
|
159
|
-
def parse_pkgver(string):
|
|
160
|
-
ops = ['==', '<=', '>=', '<', '>']
|
|
161
|
-
for item in ops:
|
|
162
|
-
if item in string:
|
|
163
|
-
split = string.split(item)
|
|
164
|
-
return split[0], item, split[1]
|
|
165
|
-
return string, None, None
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
class DepList(gluetool.Module):
|
|
169
|
-
"""
|
|
170
|
-
Module collect dependencies from moduleinfo files.
|
|
171
|
-
Type of dependencies:
|
|
172
|
-
|
|
173
|
-
repo: list of yum repositories to import on host before yum dependencies are installed
|
|
174
|
-
yum: resolved with yum/dnf manager
|
|
175
|
-
pip: resolved with pip manager, also version collisions are checked, if specified, \
|
|
176
|
-
if more modules specify same package, but only one specify version, this version will be taken
|
|
177
|
-
ansible_tasks: list of ansible tasks as you know it from playbooks
|
|
178
|
-
|
|
179
|
-
Example of moduleinfo file, file can contain more definitions
|
|
180
|
-
|
|
181
|
-
.. code-block:: yaml
|
|
182
|
-
|
|
183
|
-
---
|
|
184
|
-
|
|
185
|
-
name: postgresql
|
|
186
|
-
description: Connect to PostgreSQL database
|
|
187
|
-
dependencies:
|
|
188
|
-
yum:
|
|
189
|
-
- postgresql
|
|
190
|
-
pip:
|
|
191
|
-
- psycopg2==2.7.1
|
|
192
|
-
repo:
|
|
193
|
-
- name: dummyrepo
|
|
194
|
-
baseurl: http://dummy.url/release/repo
|
|
195
|
-
gpgcheck: 0
|
|
196
|
-
ansible_tasks:
|
|
197
|
-
- name: ensure file is present
|
|
198
|
-
file: path=/etc/magic state=file
|
|
199
|
-
"""
|
|
200
|
-
name = 'dep-list'
|
|
201
|
-
description = 'Collect module dependencies for citool modules'
|
|
202
|
-
options = {
|
|
203
|
-
'module-dirs': {
|
|
204
|
-
'help': 'Where to find modules'
|
|
205
|
-
},
|
|
206
|
-
'only-modules': {
|
|
207
|
-
'help': 'If specified, modules for which dependencies will beresolved'
|
|
208
|
-
},
|
|
209
|
-
'output': {
|
|
210
|
-
'help': 'Basename of the output file: ``FILE``-requirements.yml and ``FILE``-tasks.yml will be created.',
|
|
211
|
-
'metavar': 'FILE'
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
shared_functions = ['prepare_dependencies']
|
|
215
|
-
|
|
216
|
-
@staticmethod
|
|
217
|
-
def collect_moduleinfo_files(path):
|
|
218
|
-
collection = []
|
|
219
|
-
for root, _, files in os.walk(path):
|
|
220
|
-
for filename in files:
|
|
221
|
-
if filename.endswith('.moduleinfo'):
|
|
222
|
-
collection.append(os.path.join(root, filename))
|
|
223
|
-
return collection
|
|
224
|
-
|
|
225
|
-
def load_moduleinfo_files(self, files):
|
|
226
|
-
loaded = ModuleInfoGroup(self.logger)
|
|
227
|
-
for filename in files:
|
|
228
|
-
yaml = gluetool.utils.load_yaml(filename)
|
|
229
|
-
try:
|
|
230
|
-
item = ModuleInfo(yaml)
|
|
231
|
-
if loaded.add_moduleinfo(item):
|
|
232
|
-
self.info("Module info '{}' loaded".format(item.name))
|
|
233
|
-
self.debug("Module: '{}'\nDescription: '{}'\nDependencies:\n'{}'".format(
|
|
234
|
-
item.name, item.description, gluetool.log.format_dict(item.dependencies)
|
|
235
|
-
))
|
|
236
|
-
except KeyError:
|
|
237
|
-
self.warn("Module info '{}' cannot be properly loaded".format(filename))
|
|
238
|
-
return loaded
|
|
239
|
-
|
|
240
|
-
def prepare_dependencies(self, module_dirs, only_modules):
|
|
241
|
-
dirs = module_dirs
|
|
242
|
-
|
|
243
|
-
files = []
|
|
244
|
-
for path in dirs:
|
|
245
|
-
self.info("Searching for modules within path '{}'".format(path))
|
|
246
|
-
files += self.collect_moduleinfo_files(path)
|
|
247
|
-
self.debug(files)
|
|
248
|
-
loaded = self.load_moduleinfo_files(files)
|
|
249
|
-
dependencies = loaded.get_dependencies(only_modules)
|
|
250
|
-
self.debug('Resulting dependencies:\n{}'.format(gluetool.log.format_dict(dependencies)))
|
|
251
|
-
|
|
252
|
-
return dependencies
|
|
253
|
-
|
|
254
|
-
def execute(self):
|
|
255
|
-
dirs = self.option('module-dirs')
|
|
256
|
-
if dirs:
|
|
257
|
-
dirs = [s.strip() for s in dirs.split(',')]
|
|
258
|
-
modules = self.option('only-modules')
|
|
259
|
-
if modules:
|
|
260
|
-
if modules == '*':
|
|
261
|
-
modules = None
|
|
262
|
-
else:
|
|
263
|
-
modules = [s.strip() for s in modules.split(',')]
|
|
264
|
-
outfile = self.option('output')
|
|
265
|
-
if dirs and outfile:
|
|
266
|
-
dependencies = self.prepare_dependencies(dirs, modules)
|
|
267
|
-
|
|
268
|
-
requirements_file, tasks_file = '{}-requirements.yml'.format(outfile), '{}-tasks.yml'.format(outfile)
|
|
269
|
-
|
|
270
|
-
gluetool.utils.dump_yaml({
|
|
271
|
-
'compose_requirements': {
|
|
272
|
-
'repositories': dependencies['repo'],
|
|
273
|
-
'pip': dependencies['pip'],
|
|
274
|
-
'yum': dependencies['yum']
|
|
275
|
-
}
|
|
276
|
-
}, requirements_file)
|
|
277
|
-
|
|
278
|
-
self.info("Requirements written into '{}'".format(requirements_file))
|
|
279
|
-
|
|
280
|
-
gluetool.utils.dump_yaml(dependencies['ansible_tasks'], tasks_file)
|
|
281
|
-
|
|
282
|
-
self.info("Requirements written into '{}'".format(tasks_file))
|
|
283
|
-
|
|
284
|
-
else:
|
|
285
|
-
self.info('To execute provide --module-dirs and --output, skipping dependencies generation in execute')
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gluetool-3.2 → gluetool-3.4}/gluetool/tests/assets/parse_config/configroot/config/data_not_unicode
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|