ducted 2.0__tar.gz → 2.2__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.
Files changed (109) hide show
  1. {ducted-2.0 → ducted-2.2}/PKG-INFO +15 -11
  2. ducted-2.2/README.md +180 -0
  3. {ducted-2.0 → ducted-2.2}/docs/conf.py +2 -2
  4. ducted-2.2/docs/update_modules.py +37 -0
  5. {ducted-2.0 → ducted-2.2}/duct/__init__.py +1 -1
  6. {ducted-2.0 → ducted-2.2}/duct/__main__.py +6 -2
  7. {ducted-2.0 → ducted-2.2}/duct/aggregators.py +1 -1
  8. {ducted-2.0 → ducted-2.2}/duct/configuration.py +44 -48
  9. {ducted-2.0 → ducted-2.2}/duct/interfaces.py +1 -1
  10. {ducted-2.0 → ducted-2.2}/duct/logs/follower.py +6 -1
  11. {ducted-2.0 → ducted-2.2}/duct/logs/parsers.py +1 -1
  12. {ducted-2.0 → ducted-2.2}/duct/objects.py +3 -3
  13. {ducted-2.0 → ducted-2.2}/duct/outputs/bosun.py +1 -3
  14. {ducted-2.0 → ducted-2.2}/duct/outputs/elasticsearch.py +1 -3
  15. ducted-2.2/duct/outputs/graphite.py +69 -0
  16. ducted-2.2/duct/outputs/influxdb3.py +128 -0
  17. {ducted-2.0 → ducted-2.2}/duct/outputs/logger.py +4 -5
  18. ducted-2.2/duct/outputs/nats.py +187 -0
  19. {ducted-2.0 → ducted-2.2}/duct/outputs/null.py +2 -2
  20. {ducted-2.0 → ducted-2.2}/duct/outputs/opentsdb.py +1 -3
  21. {ducted-2.0 → ducted-2.2}/duct/outputs/prometheus.py +3 -5
  22. {ducted-2.0 → ducted-2.2}/duct/outputs/riemann.py +2 -6
  23. {ducted-2.0 → ducted-2.2}/duct/protocol/elasticsearch.py +2 -1
  24. ducted-2.2/duct/protocol/i2c.py +110 -0
  25. {ducted-2.0 → ducted-2.2}/duct/protocol/icmp.py +1 -1
  26. ducted-2.2/duct/protocol/influxdb3.py +80 -0
  27. {ducted-2.0 → ducted-2.2}/duct/protocol/opentsdb.py +3 -2
  28. {ducted-2.0 → ducted-2.2}/duct/protocol/riemann.py +4 -3
  29. ducted-2.2/duct/protocol/senml.py +134 -0
  30. {ducted-2.0 → ducted-2.2}/duct/protocol/sflow/protocol/__init__.py +1 -1
  31. {ducted-2.0 → ducted-2.2}/duct/protocol/sflow/protocol/counters.py +11 -9
  32. {ducted-2.0 → ducted-2.2}/duct/protocol/sflow/protocol/flows.py +44 -43
  33. {ducted-2.0 → ducted-2.2}/duct/protocol/sflow/protocol/protocol.py +1 -1
  34. {ducted-2.0 → ducted-2.2}/duct/protocol/sflow/protocol/utils.py +1 -1
  35. {ducted-2.0 → ducted-2.2}/duct/protocol/sflow/protocol/xdr.py +1 -1
  36. {ducted-2.0 → ducted-2.2}/duct/protocol/sflow/server.py +1 -1
  37. {ducted-2.0 → ducted-2.2}/duct/protocol/ssh.py +5 -5
  38. {ducted-2.0 → ducted-2.2}/duct/service.py +66 -44
  39. {ducted-2.0 → ducted-2.2}/duct/sources/__init__.py +1 -1
  40. {ducted-2.0 → ducted-2.2}/duct/sources/apache.py +1 -3
  41. {ducted-2.0 → ducted-2.2}/duct/sources/database/elasticsearch.py +2 -3
  42. {ducted-2.0 → ducted-2.2}/duct/sources/database/memcache.py +1 -3
  43. {ducted-2.0 → ducted-2.2}/duct/sources/database/postgresql.py +1 -3
  44. {ducted-2.0 → ducted-2.2}/duct/sources/docker.py +0 -2
  45. {ducted-2.0 → ducted-2.2}/duct/sources/generator.py +1 -3
  46. {ducted-2.0 → ducted-2.2}/duct/sources/haproxy.py +1 -3
  47. {ducted-2.0 → ducted-2.2}/duct/sources/linux/basic.py +1 -7
  48. {ducted-2.0 → ducted-2.2}/duct/sources/linux/ipsec.py +1 -1
  49. {ducted-2.0 → ducted-2.2}/duct/sources/linux/process.py +1 -1
  50. {ducted-2.0 → ducted-2.2}/duct/sources/linux/sensors.py +1 -1
  51. {ducted-2.0 → ducted-2.2}/duct/sources/mail.py +1 -1
  52. {ducted-2.0 → ducted-2.2}/duct/sources/media/libav.py +1 -3
  53. {ducted-2.0 → ducted-2.2}/duct/sources/munin.py +1 -3
  54. ducted-2.2/duct/sources/nats.py +160 -0
  55. {ducted-2.0 → ducted-2.2}/duct/sources/network.py +1 -5
  56. {ducted-2.0 → ducted-2.2}/duct/sources/nginx.py +1 -7
  57. {ducted-2.0 → ducted-2.2}/duct/sources/postfix.py +1 -3
  58. ducted-2.2/duct/sources/power/nut.py +146 -0
  59. ducted-2.2/duct/sources/power/raritan_px.py +116 -0
  60. {ducted-2.0 → ducted-2.2}/duct/sources/python/uwsgi.py +1 -3
  61. {ducted-2.0 → ducted-2.2}/duct/sources/rabbitmq.py +1 -3
  62. {ducted-2.0 → ducted-2.2}/duct/sources/redis.py +1 -3
  63. {ducted-2.0 → ducted-2.2}/duct/sources/riak.py +0 -2
  64. {ducted-2.0 → ducted-2.2}/duct/sources/riemann.py +2 -3
  65. ducted-2.2/duct/sources/sensors/environment/__init__.py +10 -0
  66. ducted-2.2/duct/sources/sensors/environment/ds18b20.py +86 -0
  67. ducted-2.0/duct/sources/sensors/environment.py → ducted-2.2/duct/sources/sensors/environment/mpl115.py +28 -34
  68. {ducted-2.0 → ducted-2.2}/duct/sources/sflow.py +1 -3
  69. {ducted-2.0 → ducted-2.2}/duct/sources/snmp.py +21 -9
  70. {ducted-2.0 → ducted-2.2}/duct/sources/unbound.py +1 -3
  71. ducted-2.2/duct/tests/__init__.py +0 -0
  72. ducted-2.2/duct/tests/helpers.py +85 -0
  73. {ducted-2.0 → ducted-2.2}/duct/tests/test_duct.py +7 -7
  74. ducted-2.2/duct/tests/test_logs.py +86 -0
  75. ducted-2.2/duct/tests/test_outputs.py +415 -0
  76. ducted-2.2/duct/tests/test_protocol.py +233 -0
  77. {ducted-2.0 → ducted-2.2}/duct/tests/test_service.py +46 -8
  78. {ducted-2.0 → ducted-2.2}/duct/tests/test_sources.py +245 -2
  79. {ducted-2.0 → ducted-2.2}/duct/tests/test_ssh.py +3 -3
  80. {ducted-2.0 → ducted-2.2}/duct/tests/test_utils.py +4 -3
  81. {ducted-2.0 → ducted-2.2}/duct/utils.py +2 -2
  82. {ducted-2.0 → ducted-2.2}/ducted.egg-info/PKG-INFO +15 -11
  83. {ducted-2.0 → ducted-2.2}/ducted.egg-info/SOURCES.txt +16 -2
  84. ducted-2.2/ducted.egg-info/requires.txt +21 -0
  85. {ducted-2.0 → ducted-2.2}/ducted.egg-info/top_level.txt +1 -0
  86. {ducted-2.0 → ducted-2.2}/pyproject.toml +15 -11
  87. ducted-2.0/README.md +0 -54
  88. ducted-2.0/duct/protocol/imap4.py +0 -11
  89. ducted-2.0/duct/tests/test_logs.py +0 -86
  90. ducted-2.0/duct/tests/test_outputs.py +0 -59
  91. ducted-2.0/ducted.egg-info/requires.txt +0 -17
  92. {ducted-2.0 → ducted-2.2}/LICENSE +0 -0
  93. {ducted-2.0 → ducted-2.2}/duct/ihateprotobuf/__init__.py +0 -0
  94. {ducted-2.0 → ducted-2.2}/duct/ihateprotobuf/proto_pb2.py +0 -0
  95. {ducted-2.0 → ducted-2.2}/duct/logs/__init__.py +0 -0
  96. {ducted-2.0 → ducted-2.2}/duct/outputs/__init__.py +0 -0
  97. {ducted-2.0 → ducted-2.2}/duct/protocol/__init__.py +0 -0
  98. {ducted-2.0 → ducted-2.2}/duct/protocol/sflow/__init__.py +0 -0
  99. {ducted-2.0 → ducted-2.2}/duct/sources/database/__init__.py +0 -0
  100. {ducted-2.0 → ducted-2.2}/duct/sources/linux/__init__.py +0 -0
  101. {ducted-2.0 → ducted-2.2}/duct/sources/media/__init__.py +0 -0
  102. {ducted-2.0/duct/sources/python → ducted-2.2/duct/sources/power}/__init__.py +0 -0
  103. {ducted-2.0/duct/sources/sensors → ducted-2.2/duct/sources/python}/__init__.py +0 -0
  104. {ducted-2.0/duct/tests → ducted-2.2/duct/sources/sensors}/__init__.py +0 -0
  105. {ducted-2.0 → ducted-2.2}/duct/tests/globs.py +0 -0
  106. {ducted-2.0 → ducted-2.2}/duct/tests/test_sflow.py +0 -0
  107. {ducted-2.0 → ducted-2.2}/ducted.egg-info/dependency_links.txt +0 -0
  108. {ducted-2.0 → ducted-2.2}/ducted.egg-info/entry_points.txt +0 -0
  109. {ducted-2.0 → ducted-2.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ducted
3
- Version: 2.0
3
+ Version: 2.2
4
4
  Summary: A monitoring agent and event processor
5
5
  Author-email: Colin Alston <colin@tamvera.com>
6
6
  License: MIT
@@ -14,18 +14,22 @@ Classifier: Programming Language :: Python :: 3
14
14
  Classifier: Topic :: System :: Monitoring
15
15
  Requires-Python: >=3.11
16
16
  License-File: LICENSE
17
- Requires-Dist: zope.interface
18
- Requires-Dist: PyYaml
19
- Requires-Dist: protobuf
20
- Requires-Dist: aiohttp
21
- Requires-Dist: asyncssh
22
- Requires-Dist: aiodns
23
- Requires-Dist: pysnmp>=6.0
24
- Requires-Dist: cryptography
25
- Requires-Dist: service_identity
17
+ Requires-Dist: zope.interface>=8.5
18
+ Requires-Dist: PyYaml~=6.0
19
+ Requires-Dist: protobuf~=7.35
20
+ Requires-Dist: aiohttp~=3.14
21
+ Requires-Dist: asyncssh~=2.23
22
+ Requires-Dist: aiodns~=4.0
23
+ Requires-Dist: pysnmp~=7.1
24
+ Requires-Dist: cryptography~=48.0
25
+ Requires-Dist: service_identity~=26.1
26
+ Requires-Dist: pydantic~=2.13
27
+ Requires-Dist: construct~=2.10
28
+ Requires-Dist: nats-py~=2.15
29
+ Requires-Dist: cbor2~=6.1
30
+ Requires-Dist: smbus2~=0.6
26
31
  Provides-Extra: dev
27
32
  Requires-Dist: asyncpg; extra == "dev"
28
- Requires-Dist: construct<2.6; extra == "dev"
29
33
  Requires-Dist: pytest; extra == "dev"
30
34
  Requires-Dist: pytest-asyncio; extra == "dev"
31
35
  Requires-Dist: pytest-cov; extra == "dev"
ducted-2.2/README.md ADDED
@@ -0,0 +1,180 @@
1
+ # Duct
2
+
3
+ I want to talk to you about ducts. Do your ducts seem old-fashioned? Out of date?
4
+
5
+ [![CI](https://github.com/Tamvera/ducted/actions/workflows/ci.yml/badge.svg)](https://github.com/Tamvera/ducted/actions/workflows/ci.yml)
6
+ [![Latest Docs](https://img.shields.io/badge/docs-GitHub%20Pages-blue)](https://tamvera.github.io/ducted/)
7
+
8
+ **A pluggable Python telemetry pipeline.** Collect metrics from anywhere, route them to any backend, with nothing but Python and a YAML file.
9
+
10
+ Duct is an asyncio daemon that polls *sources* on configurable intervals and routes the resulting events to *outputs*. Both are plain Python classes: anything Python can reach - a socket, a file, an HTTP API, a subprocess, an SSH session, a hardware sensor - can be a source or an output. No DSL, no code generation.
11
+
12
+ ## What makes it different
13
+
14
+ - **Sources and outputs can run network servers.** The Prometheus output hosts an HTTP scrape endpoint inside the daemon; the sFlow source runs a UDP collector; the Riemann source acts as a full Riemann TCP server. Duct can receive telemetry from other systems, not only emit it.
15
+ - **SSH remote checks, no remote agent.** Mark any source `use_ssh: true` and it runs transparently on a remote host over a pooled SSH connection.
16
+ - **Modern, backend-agnostic.** InfluxDB 3, Prometheus, NATS (JetStream + SenML/CBOR), Elasticsearch, Graphite, and more. Use one output or several simultaneously.
17
+ - **Fine-grained routing.** Route individual sources to specific outputs or sets of outputs.
18
+ - **Threshold state evaluation.** Declare `warning`/`critical` expressions per-metric; Duct overrides event state automatically.
19
+ - **Blueprint macros.** DRY config: define a toolbox of checks once, expand it across a list of hosts with a single block.
20
+
21
+ ## Built-in sources
22
+
23
+ | Category | Sources |
24
+ |---|---|
25
+ | Linux system | CPU, memory, load average, disk I/O, disk free, network, process stats |
26
+ | Sensors & hardware | lm-sensors, SMART, DS18B20 temperature, MPL115 pressure/temperature, UPS (NUT) |
27
+ | Web & proxy | Apache, Nginx (status + log metrics), HAProxy |
28
+ | Databases | PostgreSQL, Elasticsearch, Redis, RabbitMQ, Memcache, Riak |
29
+ | Network | Ping, HTTP, sFlow UDP collector, SNMP |
30
+ | Infrastructure | Docker containers, Postfix, Unbound, StrongSwan/IPsec |
31
+ | Messaging | NATS subscriber (JetStream + SenML) |
32
+ | Ingestion | Riemann TCP listener, Munin node client, uWSGI Emperor |
33
+
34
+ ## Installation
35
+
36
+ ### System wide on Linux, with a systemd script
37
+ ```bash
38
+ curl -o - https://raw.githubusercontent.com/Tamvera/ducted/refs/heads/master/scripts/install.sh | sh
39
+ ```
40
+
41
+ ### With docker
42
+ ```bash
43
+ docker run -i -v ./duct.yml:/duct/duct.yml ghcr.io/tamvera/ducted:latest
44
+ ```
45
+
46
+ ### With pip
47
+ ```bash
48
+ pip install ducted
49
+ ```
50
+
51
+ Requires Python 3.11+.
52
+
53
+ ## Quick start
54
+
55
+ Create `duct.yml`:
56
+
57
+ ```yaml
58
+ interval: 1.0
59
+ ttl: 60.0
60
+
61
+ outputs:
62
+ # Prometheus scrape endpoint at http://0.0.0.0:9100/metrics
63
+ - output: duct.outputs.prometheus.Prometheus
64
+ port: 9100
65
+
66
+ sources:
67
+ - service: cpu
68
+ source: duct.sources.linux.basic.CPU
69
+ interval: 1.0
70
+ warning: { cpu: "> 0.5" }
71
+ critical: { cpu: "> 0.8" }
72
+
73
+ - service: memory
74
+ source: duct.sources.linux.basic.Memory
75
+ interval: 10.0
76
+
77
+ - service: disk
78
+ source: duct.sources.linux.basic.DiskFree
79
+ interval: 60.0
80
+ ```
81
+
82
+ ```bash
83
+ ductd -c duct.yml
84
+ ```
85
+
86
+ ## Outputs
87
+
88
+ Fan out to multiple backends simultaneously by naming them:
89
+
90
+ ```yaml
91
+ outputs:
92
+ - output: duct.outputs.prometheus.Prometheus
93
+ name: prom
94
+ port: 9100
95
+
96
+ - output: duct.outputs.influxdb3.InfluxDB3
97
+ name: influx
98
+ url: http://localhost:8086
99
+ database: mymetrics
100
+ token: my-token
101
+
102
+ - output: duct.outputs.nats.Nats
103
+ name: bus
104
+ servers: ["nats://localhost:4222"]
105
+ format: senml-cbor
106
+ jetstream: true
107
+ ```
108
+
109
+ ## Receiving telemetry
110
+
111
+ Sources can listen as well as poll. Funnel events from other systems into the same pipeline and route them to any output:
112
+
113
+ ```yaml
114
+ sources:
115
+ # Act as a Riemann TCP server - ingest events from other agents
116
+ - service: riemann-in
117
+ source: duct.sources.riemann.RiemannTCP
118
+ port: 5555
119
+ route: influx
120
+
121
+ # Collect sFlow from network switches (UDP server)
122
+ - service: sflow
123
+ source: duct.sources.sflow.sFlow
124
+ port: 6343
125
+
126
+ # Subscribe to a NATS subject
127
+ - service: nats-in
128
+ source: duct.sources.nats.Nats
129
+ servers: ["nats://localhost:4222"]
130
+ topics: ["metrics.>"]
131
+ ```
132
+
133
+ ## SSH remote checks
134
+
135
+ Run checks on remote hosts without installing an agent. SSH connections are pooled per host:
136
+
137
+ ```yaml
138
+ ssh_username: monitor
139
+ ssh_keyfile: /etc/duct/id_ed25519
140
+
141
+ sources:
142
+ - service: load
143
+ source: duct.sources.linux.basic.LoadAverage
144
+ use_ssh: true
145
+ ssh_host: web01.example.com
146
+ interval: 30.0
147
+ ```
148
+
149
+ Use **blueprint macros** to expand a toolbox of checks across many hosts with a single config block - see the [documentation](https://tamvera.github.io/ducted/) for details.
150
+
151
+ ## Writing a plugin
152
+
153
+ A source is a Python class. It can use any library, open any connection, or start any server:
154
+
155
+ ```python
156
+ from zope.interface import implementer
157
+ from duct.interfaces import IDuctSource
158
+ from duct.objects import Source
159
+
160
+ @implementer(IDuctSource)
161
+ class MySource(Source):
162
+ async def get(self):
163
+ value = await fetch_something()
164
+ return self.createEvent('ok', 'My metric', value, prefix='mymetric')
165
+ ```
166
+
167
+ Point to it in config by dotted import path:
168
+
169
+ ```yaml
170
+ sources:
171
+ - service: mything
172
+ source: mypackage.mysource.MySource
173
+ interval: 10.0
174
+ ```
175
+
176
+ Outputs follow the same pattern: subclass `Output`, implement `createClient`, and drain the event queue however you like - including running an embedded server.
177
+
178
+ ## Documentation
179
+
180
+ Full documentation at <https://tamvera.github.io/ducted/>
@@ -101,7 +101,7 @@ pygments_style = 'sphinx'
101
101
 
102
102
  # The theme to use for HTML and HTML Help pages. See the documentation for
103
103
  # a list of builtin themes.
104
- html_theme = 'alabaster'
104
+ html_theme = 'blue_nebula'
105
105
 
106
106
  # Theme options are theme-specific and customize the look and feel of a theme
107
107
  # further. For a list of options available for each theme, see the
@@ -130,7 +130,7 @@ html_theme = 'alabaster'
130
130
  # Add any paths that contain custom static files (such as style sheets) here,
131
131
  # relative to this directory. They are copied after the builtin static files,
132
132
  # so a file named "default.css" will overwrite the builtin "default.css".
133
- html_static_path = ['_static']
133
+ #html_static_path = ['_static']
134
134
 
135
135
  # Add any extra paths that contain custom files (such as robots.txt or
136
136
  # .htaccess) here, relative to this directory. These files are copied
@@ -0,0 +1,37 @@
1
+ from pathlib import Path
2
+
3
+ CWD = Path(__file__).parent
4
+ BASE_DIR = (CWD / "..").resolve()
5
+
6
+ RST_TEMPLATE = """%s
7
+
8
+ """
9
+
10
+ def scan_modules(name):
11
+ doc_path = CWD / "api" / f"{name}.rst"
12
+ mod_path = BASE_DIR / "duct" / name
13
+
14
+ modules = []
15
+ for node in mod_path.rglob("**/*.py"):
16
+ if node.is_file() and node.name != "__init__.py":
17
+ filepath = node.relative_to(BASE_DIR)
18
+ module = str(filepath.parents[0]/filepath.stem).replace('/', '.')
19
+ modules.append(module)
20
+
21
+ modules.sort()
22
+ print(modules)
23
+ with open(doc_path, "w+t") as f:
24
+ f.write(f"duct.{name}\n")
25
+ f.write("**************\n\n")
26
+
27
+ for module in modules:
28
+ f.write(module + '\n')
29
+ f.write("===================================\n\n")
30
+ f.write(f".. automodule:: {module}\n")
31
+ f.write(" :members:\n")
32
+ f.write(" :show-inheritance:\n\n")
33
+
34
+ if __name__ == "__main__":
35
+ scan_modules("sources")
36
+ scan_modules("outputs")
37
+ scan_modules("protocol")
@@ -1,6 +1,6 @@
1
1
  """Duct - A monitoring agent
2
2
 
3
- .. moduleauthor:: Colin Alston <colin@imcol.in>
3
+ .. moduleauthor:: Colin Alston <colin@tamvera.com>
4
4
 
5
5
  """
6
6
 
@@ -1,5 +1,5 @@
1
1
  """
2
- Duct — asyncio entry point
2
+ Duct - asyncio entry point
3
3
 
4
4
  Usage:
5
5
  ductd -c duct.yml
@@ -18,7 +18,7 @@ from duct.service import DuctService
18
18
  def main():
19
19
  """Entry point for the duct daemon."""
20
20
  parser = argparse.ArgumentParser(
21
- description='Duct — A monitoring agent and event processor'
21
+ description='Duct - A monitoring agent and event processor'
22
22
  )
23
23
  parser.add_argument(
24
24
  '-c', '--config',
@@ -42,7 +42,10 @@ def main():
42
42
 
43
43
 
44
44
  async def _run(config_path):
45
+ log = logging.getLogger(__name__)
46
+ log.info("Starting Ductd service.")
45
47
  config = ConfigFile(config_path)
48
+
46
49
  svc = DuctService(config)
47
50
 
48
51
  loop = asyncio.get_running_loop()
@@ -56,6 +59,7 @@ async def _run(config_path):
56
59
  loop.add_signal_handler(sig, _handle_signal)
57
60
 
58
61
  await svc.startService()
62
+ log.info("Service running.")
59
63
 
60
64
  await stop_event.wait()
61
65
 
@@ -2,7 +2,7 @@
2
2
  .. module:: aggregators
3
3
  :synopsis: Aggregation functions
4
4
 
5
- .. moduleauthor:: Colin Alston <colin@imcol.in>
5
+ .. moduleauthor:: Colin Alston <colin@tamvera.com>
6
6
  """
7
7
 
8
8
 
@@ -2,22 +2,56 @@
2
2
  .. module:: configuration
3
3
  :synopsis: Configuration file parser
4
4
 
5
- .. moduleauthor:: Colin Alston <colin@imcol.in>
5
+ .. moduleauthor:: Colin Alston <colin@tamvera.com>
6
6
  """
7
7
 
8
8
  import os
9
9
  import itertools
10
10
  import copy
11
11
  import logging
12
+ from typing import Optional
12
13
 
13
14
  import yaml
15
+ from pydantic import BaseModel, ConfigDict, ValidationError
14
16
 
15
17
  log = logging.getLogger(__name__)
16
18
 
19
+
17
20
  class ConfigurationError(Exception):
18
21
  """General exception class for Duct configuration issues
19
22
  """
20
23
 
24
+
25
+ class DuctConfig(BaseModel):
26
+ """Top-level configuration schema with centralised defaults."""
27
+ model_config = ConfigDict(extra='allow')
28
+
29
+ # Timing
30
+ interval: float = 60.0
31
+ ttl: float = 60.0
32
+ stagger: float = 0.2
33
+
34
+ # Debugging
35
+ debug: int = 0
36
+
37
+ default_route: Optional[str] = None
38
+
39
+ # Sources and outputs
40
+ sources: list = []
41
+ outputs: Optional[list] = []
42
+
43
+ # Global SSH defaults (can be overridden per-source)
44
+ ssh_knownhosts_file: Optional[str] = '/var/lib/duct/known_hosts'
45
+ ssh_keyfile: Optional[str] = None
46
+ ssh_key: Optional[str] = None
47
+ ssh_keypass: Optional[str] = None
48
+ ssh_username: Optional[str] = None
49
+ ssh_password: Optional[str] = None
50
+ ssh_port: int = 22
51
+
52
+ # Daemon config
53
+ base_path: str = "/var/lib/duct"
54
+
21
55
  class ConfigFile(object):
22
56
  """Duct configuration file parser and accessor
23
57
  """
@@ -32,45 +66,18 @@ class ConfigFile(object):
32
66
  else:
33
67
  raise ConfigurationError(f"Configuration file '{path}' not found")
34
68
 
35
- self.known_items = {
36
- 'sources': list,
37
- 'outputs': list,
38
- 'interval': (float, int,),
39
- 'ttl': (float, int,),
40
- 'blueprint': list,
41
- 'ssh_key': str,
42
- 'ssh_user': str,
43
- 'toolbox': dict
44
- }
45
-
46
69
  self._parse_config()
47
70
 
48
- def _validate_type(self, item, vtype):
49
- if not isinstance(vtype, tuple):
50
- vtype = (vtype, )
51
-
52
- parse = False
53
- for vt in vtype:
54
- try:
55
- assert isinstance(self.raw_config.get(item, vt()), vtype)
56
- parse = True
57
- except AssertionError:
58
- pass
59
-
60
- if not parse:
61
- raise ConfigurationError(
62
- f"{item} must be one of type {repr(vtype)}")
63
-
64
- def _validate_config(self):
65
- for key, val in self.known_items.items():
66
- self._validate_type(key, val)
67
-
68
71
  def _parse_config(self):
69
72
  self._merge_includes()
73
+ self._build_blueprints()
70
74
 
71
- self._validate_config()
75
+ try:
76
+ self.duct_config = DuctConfig(**self.raw_config)
77
+ except ValidationError as e:
78
+ raise ConfigurationError(str(e)) from e
72
79
 
73
- self._build_blueprints()
80
+ self.raw_config = self.duct_config.model_dump()
74
81
 
75
82
  def _merge_includes(self):
76
83
  def both(i1, i2, t):
@@ -86,7 +93,6 @@ class ConfigFile(object):
86
93
 
87
94
  paths.extend(paths2)
88
95
 
89
-
90
96
  for ipath in paths:
91
97
  if os.path.exists(ipath):
92
98
  files = [os.path.join(ipath, fi) for fi in os.listdir(ipath)
@@ -98,15 +104,12 @@ class ConfigFile(object):
98
104
  for key, val in conf.items():
99
105
  if key in self.raw_config:
100
106
  if both(val, self.raw_config[key], dict):
101
- # Merge dicts
102
107
  for k2, v2 in val.items():
103
108
  self.raw_config[key][k2] = v2
104
109
 
105
110
  elif both(val, self.raw_config[key], list):
106
- # Extend lists
107
111
  self.raw_config[key].extend(val)
108
112
  else:
109
- # Overwrite
110
113
  self.raw_config[key] = val
111
114
  else:
112
115
  self.raw_config[key] = val
@@ -117,24 +120,20 @@ class ConfigFile(object):
117
120
  ipath)
118
121
 
119
122
  def _build_blueprints(self):
120
- # Turn toolboxes into a dict
121
123
  toolboxes = self.raw_config.get('toolbox', {})
122
124
  blueprints = self.raw_config.get('blueprint', [])
123
125
 
124
126
  if blueprints:
125
- # Make sure raw config stubs exist if we have any blueprints
126
127
  if 'sources' not in self.raw_config:
127
128
  self.raw_config['sources'] = []
128
129
 
129
130
  for blueprint in blueprints:
130
131
  tbs = blueprint['toolbox']
131
- # Listify it so we can inherit multiple toolboxes
132
132
  if not isinstance(toolboxes, list):
133
133
  tbs = [tbs]
134
134
 
135
135
  tbs = [toolboxes[tool] for tool in tbs]
136
136
 
137
- # Compute a dot product of all the config settings vectors
138
137
  inversions = []
139
138
  for key, val in blueprint.get('sets', {}).items():
140
139
  inversions.append([(key, jay) for jay in val])
@@ -142,25 +141,19 @@ class ConfigFile(object):
142
141
  for options in itertools.product(*inversions):
143
142
  for toolbox in tbs:
144
143
  for source in toolbox.get('sources', []):
145
- # Make a copy of the source dict
146
144
  mysource = copy.copy(source)
147
145
 
148
- # Merge toolbox defaults
149
146
  for key, val in toolbox.get('defaults', {}).items():
150
147
  mysource[key] = val
151
148
 
152
- # Merge blueprint defaults
153
149
  for key, val in blueprint.get('defaults', {}).items():
154
150
  mysource[key] = val
155
151
 
156
- # Merge set permutation
157
152
  for key, val in options:
158
153
  mysource[key] = val
159
154
 
160
- # Bolt it into our real config file
161
155
  self.raw_config['sources'].append(mysource)
162
156
 
163
- # Free a tiny bit of memory
164
157
  if 'toolbox' in self.raw_config:
165
158
  del self.raw_config['toolbox']
166
159
 
@@ -175,3 +168,6 @@ class ConfigFile(object):
175
168
 
176
169
  def __getitem__(self, item):
177
170
  return self.raw_config[item]
171
+
172
+ def __getattr__(self, name):
173
+ return self.duct_config.__getattribute__(name)
@@ -2,7 +2,7 @@
2
2
  .. module:: interfaces
3
3
  :synopsis: Generic interfaces
4
4
 
5
- .. moduleauthor:: Colin Alston <colin@imcol.in>
5
+ .. moduleauthor:: Colin Alston <colin@tamvera.com>
6
6
  """
7
7
 
8
8
  from zope.interface import Interface
@@ -2,7 +2,7 @@
2
2
  .. module:: follower
3
3
  :synopsis: Log tailing classes
4
4
 
5
- .. moduleauthor:: Colin Alston <colin@imcol.in>
5
+ .. moduleauthor:: Colin Alston <colin@tamvera.com>
6
6
  """
7
7
  import os
8
8
 
@@ -14,6 +14,11 @@ class LogFollower(object):
14
14
  :type logfile: str
15
15
  :param parser: Optional parser method for log lines
16
16
  :type parser: str
17
+ :param tmp_path: Temporary path used to store position markers
18
+ :type tmp_path: str
19
+ :param history: If True the log is read from the start instead of tailed
20
+ unless its already been read
21
+ :type history: str
17
22
  """
18
23
 
19
24
  def __init__(self, logfile, parser=None, tmp_path="/var/lib/duct/",
@@ -2,7 +2,7 @@
2
2
  .. module:: parsers
3
3
  :synopsis: Log line parser classes
4
4
 
5
- .. moduleauthor:: Colin Alston <colin@imcol.in>
5
+ .. moduleauthor:: Colin Alston <colin@tamvera.com>
6
6
  """
7
7
 
8
8
  import re
@@ -2,7 +2,7 @@
2
2
  .. module:: objects
3
3
  :synopsis: Base classes for sources, outputs, and event objects
4
4
 
5
- .. moduleauthor:: Colin Alston <colin@imcol.in>
5
+ .. moduleauthor:: Colin Alston <colin@tamvera.com>
6
6
  """
7
7
 
8
8
  import hashlib
@@ -108,7 +108,7 @@ class Output(object):
108
108
  async def createClient(self):
109
109
  """Coroutine that sets up the output connection"""
110
110
 
111
- def eventsReceived(self, events):
111
+ async def eventsReceived(self, events):
112
112
  """Receives a list of events and queues them"""
113
113
  if self.maxsize > 0:
114
114
  if len(self.events) < self.maxsize:
@@ -242,7 +242,7 @@ class Source(object):
242
242
  self._loop_task = asyncio.create_task(self._run_loop())
243
243
 
244
244
  async def _run_loop(self):
245
- """Async polling loop — fires tick() then repeats every inter secs."""
245
+ """Async polling loop - fires tick() then repeats every inter secs."""
246
246
  try:
247
247
  while True:
248
248
  await self.tick()
@@ -2,7 +2,7 @@
2
2
  .. module:: bosun
3
3
  :synopsis: Bosun output
4
4
 
5
- .. moduleauthor:: Colin Alston <colin@imcol.in>
5
+ .. moduleauthor:: Colin Alston <colin@tamvera.com>
6
6
  """
7
7
  import json
8
8
  import base64
@@ -14,8 +14,6 @@ from duct.utils import HTTPRequest
14
14
  class Bosun(opentsdb.OpenTSDB):
15
15
  """Bosun HTTP API output
16
16
 
17
- **Configuration arguments:**
18
-
19
17
  :param url: URL (default: http://localhost:4242)
20
18
  :type url: str.
21
19
  :param maxsize: Maximum queue backlog size (default: 250000, 0 disables)
@@ -2,7 +2,7 @@
2
2
  .. module:: elasticsearch
3
3
  :synopsis: Elasticsearch event output
4
4
 
5
- .. moduleauthor:: Colin Alston <colin@imcol.in>
5
+ .. moduleauthor:: Colin Alston <colin@tamvera.com>
6
6
  """
7
7
  import asyncio
8
8
  import datetime
@@ -17,8 +17,6 @@ log = logging.getLogger(__name__)
17
17
  class ElasticSearch(Output):
18
18
  """ElasticSearch HTTP API output
19
19
 
20
- **Configuration arguments:**
21
-
22
20
  :param url: Elasticsearch URL (default: http://localhost:9200)
23
21
  :type url: str
24
22
  :param maxsize: Maximum queue backlog size (default: 250000, 0 disables)