firewatcher 1.63__tar.gz → 1.64__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.
- {firewatcher-1.63/firewatcher.egg-info → firewatcher-1.64}/PKG-INFO +2 -2
- {firewatcher-1.63 → firewatcher-1.64}/README.md +1 -1
- {firewatcher-1.63 → firewatcher-1.64/firewatcher.egg-info}/PKG-INFO +2 -2
- {firewatcher-1.63 → firewatcher-1.64}/firewatcher.py +45 -4
- {firewatcher-1.63 → firewatcher-1.64}/tests/test_cli.py +1 -1
- {firewatcher-1.63 → firewatcher-1.64}/tests/test_systemd.py +181 -0
- {firewatcher-1.63 → firewatcher-1.64}/LICENSE +0 -0
- {firewatcher-1.63 → firewatcher-1.64}/MANIFEST.in +0 -0
- {firewatcher-1.63 → firewatcher-1.64}/examples/firewatcher.conf +0 -0
- {firewatcher-1.63 → firewatcher-1.64}/examples/nvme_failure.regex +0 -0
- {firewatcher-1.63 → firewatcher-1.64}/examples/sys_msg.txt +0 -0
- {firewatcher-1.63 → firewatcher-1.64}/firewatcher.egg-info/SOURCES.txt +0 -0
- {firewatcher-1.63 → firewatcher-1.64}/firewatcher.egg-info/dependency_links.txt +0 -0
- {firewatcher-1.63 → firewatcher-1.64}/firewatcher.egg-info/entry_points.txt +0 -0
- {firewatcher-1.63 → firewatcher-1.64}/firewatcher.egg-info/top_level.txt +0 -0
- {firewatcher-1.63 → firewatcher-1.64}/setup.cfg +0 -0
- {firewatcher-1.63 → firewatcher-1.64}/setup.py +0 -0
- {firewatcher-1.63 → firewatcher-1.64}/tests/conftest.py +0 -0
- {firewatcher-1.63 → firewatcher-1.64}/tests/test_capture.py +0 -0
- {firewatcher-1.63 → firewatcher-1.64}/tests/test_compressor.py +0 -0
- {firewatcher-1.63 → firewatcher-1.64}/tests/test_config.py +0 -0
- {firewatcher-1.63 → firewatcher-1.64}/tests/test_log_source.py +0 -0
- {firewatcher-1.63 → firewatcher-1.64}/tests/test_matching.py +0 -0
- {firewatcher-1.63 → firewatcher-1.64}/tests/test_notify.py +0 -0
- {firewatcher-1.63 → firewatcher-1.64}/tests/test_patterns.py +0 -0
- {firewatcher-1.63 → firewatcher-1.64}/tests/test_position.py +0 -0
- {firewatcher-1.63 → firewatcher-1.64}/tests/test_slugify.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: firewatcher
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.64
|
|
4
4
|
Summary: Watch logs for incident patterns and persist surrounding context off-box
|
|
5
5
|
Home-page: https://github.com/yufei-pan/firewatcher
|
|
6
6
|
Author: Yufei Pan
|
|
@@ -131,7 +131,7 @@ Useful flags:
|
|
|
131
131
|
| Flag | Description |
|
|
132
132
|
|------|-------------|
|
|
133
133
|
| `--unit-name NAME` | Unit name (default: `firewatcher`) |
|
|
134
|
-
| `--requires-mounts-for PATH` | Add `RequiresMountsFor=` (repeat for NFS/remote output) |
|
|
134
|
+
| `--requires-mounts-for PATH` | Add `RequiresMountsFor=` (repeat for NFS/remote output). Each path is made absolute and normalized as text (`.`, `..`, `//`, a trailing `/`; symlinks are not followed, as systemd follows none), quoted for you when it has spaces, quotes or backslashes, and refused if it has a newline or is not UTF-8 (systemd cannot read either back there) |
|
|
135
135
|
| `--no-enable` | Write the unit and reload, but do not enable or start it |
|
|
136
136
|
|
|
137
137
|
```bash
|
|
@@ -104,7 +104,7 @@ Useful flags:
|
|
|
104
104
|
| Flag | Description |
|
|
105
105
|
|------|-------------|
|
|
106
106
|
| `--unit-name NAME` | Unit name (default: `firewatcher`) |
|
|
107
|
-
| `--requires-mounts-for PATH` | Add `RequiresMountsFor=` (repeat for NFS/remote output) |
|
|
107
|
+
| `--requires-mounts-for PATH` | Add `RequiresMountsFor=` (repeat for NFS/remote output). Each path is made absolute and normalized as text (`.`, `..`, `//`, a trailing `/`; symlinks are not followed, as systemd follows none), quoted for you when it has spaces, quotes or backslashes, and refused if it has a newline or is not UTF-8 (systemd cannot read either back there) |
|
|
108
108
|
| `--no-enable` | Write the unit and reload, but do not enable or start it |
|
|
109
109
|
|
|
110
110
|
```bash
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: firewatcher
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.64
|
|
4
4
|
Summary: Watch logs for incident patterns and persist surrounding context off-box
|
|
5
5
|
Home-page: https://github.com/yufei-pan/firewatcher
|
|
6
6
|
Author: Yufei Pan
|
|
@@ -131,7 +131,7 @@ Useful flags:
|
|
|
131
131
|
| Flag | Description |
|
|
132
132
|
|------|-------------|
|
|
133
133
|
| `--unit-name NAME` | Unit name (default: `firewatcher`) |
|
|
134
|
-
| `--requires-mounts-for PATH` | Add `RequiresMountsFor=` (repeat for NFS/remote output) |
|
|
134
|
+
| `--requires-mounts-for PATH` | Add `RequiresMountsFor=` (repeat for NFS/remote output). Each path is made absolute and normalized as text (`.`, `..`, `//`, a trailing `/`; symlinks are not followed, as systemd follows none), quoted for you when it has spaces, quotes or backslashes, and refused if it has a newline or is not UTF-8 (systemd cannot read either back there) |
|
|
135
135
|
| `--no-enable` | Write the unit and reload, but do not enable or start it |
|
|
136
136
|
|
|
137
137
|
```bash
|
|
@@ -31,7 +31,7 @@ import syslog
|
|
|
31
31
|
import threading
|
|
32
32
|
import zlib
|
|
33
33
|
|
|
34
|
-
version = '1.
|
|
34
|
+
version = '1.64'
|
|
35
35
|
__version__ = version
|
|
36
36
|
|
|
37
37
|
DEFAULT_OUTPUT_FOLDER = '/var/log/captured_messages/'
|
|
@@ -914,11 +914,41 @@ def resolve_settings(args, environ=None):
|
|
|
914
914
|
settings.ntfy_url = (settings.ntfy_url or '').strip()
|
|
915
915
|
settings.unit_name = getattr(args, 'unit_name', None) or DEFAULT_UNIT_NAME
|
|
916
916
|
settings.no_enable = bool(getattr(args, 'no_enable', False))
|
|
917
|
-
settings.requires_mounts_for =
|
|
917
|
+
settings.requires_mounts_for = _mount_paths(getattr(args, 'requires_mounts_for', None))
|
|
918
918
|
validate_settings(settings)
|
|
919
919
|
return settings
|
|
920
920
|
|
|
921
921
|
|
|
922
|
+
def _mount_paths(paths):
|
|
923
|
+
"""The --requires-mounts-for paths for the unit, with empty ones dropped.
|
|
924
|
+
|
|
925
|
+
systemd ignores a relative or unnormalized path in RequiresMountsFor= with a warning, so each path is
|
|
926
|
+
made absolute and normalized as text ('.', '..', '//' and a trailing '/'). The file system is not
|
|
927
|
+
touched: symlinks are not followed (systemd follows none either, and one inside a remote share would
|
|
928
|
+
lead off it), and a hung NFS mount or an automount point can't stall or trigger anything. The work is
|
|
929
|
+
done on the path's bytes, so the locale doesn't matter. A newline or carriage return can't be written
|
|
930
|
+
in any form systemd reads back, and systemd rejects a path that is not UTF-8, so those are refused,
|
|
931
|
+
before anything is installed."""
|
|
932
|
+
result = []
|
|
933
|
+
for path in paths or []:
|
|
934
|
+
if not path:
|
|
935
|
+
continue
|
|
936
|
+
try:
|
|
937
|
+
raw = os.path.abspath(os.fsencode(path))
|
|
938
|
+
except OSError as exc: # a relative path, and the current directory is gone
|
|
939
|
+
raise ConfigError('--requires-mounts-for cannot make %r absolute: %s' % (path, exc))
|
|
940
|
+
raw = b'/' + raw.lstrip(b'/') # POSIX normpath keeps a leading '//'; systemd does not accept it
|
|
941
|
+
if b'\n' in raw or b'\r' in raw:
|
|
942
|
+
raise ConfigError('--requires-mounts-for cannot take a path with a newline or carriage return, '
|
|
943
|
+
'which systemd cannot read back from a unit file: %r' % os.fsdecode(raw))
|
|
944
|
+
try:
|
|
945
|
+
result.append(raw.decode('utf-8'))
|
|
946
|
+
except UnicodeError:
|
|
947
|
+
raise ConfigError('--requires-mounts-for cannot take a path that is not UTF-8, which systemd '
|
|
948
|
+
'rejects: %r' % os.fsdecode(raw))
|
|
949
|
+
return result
|
|
950
|
+
|
|
951
|
+
|
|
922
952
|
def validate_settings(settings):
|
|
923
953
|
if settings.llm_enabled and (not settings.llm_base_url or not settings.llm_model):
|
|
924
954
|
raise ConfigError('LLM analysis requires base_url and model (--llm-base-url and --llm-model, or llm.base_url and llm.model in the config)')
|
|
@@ -2295,6 +2325,17 @@ def _quote_unit_arg(value, program=False):
|
|
|
2295
2325
|
|
|
2296
2326
|
|
|
2297
2327
|
|
|
2328
|
+
def _quote_unit_path(path):
|
|
2329
|
+
"""A path for RequiresMountsFor=, a space-separated list in which systemd expands % specifiers. A path
|
|
2330
|
+
with whitespace, a quote or a backslash goes in double quotes, with the backslash and the double quote
|
|
2331
|
+
each prefixed by a backslash: that is all systemd reads back there. It has no \\n, so a newline does not
|
|
2332
|
+
survive (such a path is refused in _mount_paths), unlike in an ExecStart argument."""
|
|
2333
|
+
path = path.replace('%', '%%')
|
|
2334
|
+
if any(ch.isspace() or ch in '"\'\\' for ch in path):
|
|
2335
|
+
path = '"' + path.replace('\\', '\\\\').replace('"', '\\"') + '"'
|
|
2336
|
+
return path
|
|
2337
|
+
|
|
2338
|
+
|
|
2298
2339
|
def render_unit_file(args, executable=None, config_exists=None, raw=None):
|
|
2299
2340
|
"""Render a systemd unit. Does not require systemd to be present."""
|
|
2300
2341
|
resolved, raw = _prepare_unit_settings(args, raw=raw)
|
|
@@ -2323,7 +2364,7 @@ def render_unit_file(args, executable=None, config_exists=None, raw=None):
|
|
|
2323
2364
|
'Wants=' + ' '.join(wants),
|
|
2324
2365
|
]
|
|
2325
2366
|
for mount in requires:
|
|
2326
|
-
lines.append('RequiresMountsFor=' + mount)
|
|
2367
|
+
lines.append('RequiresMountsFor=' + _quote_unit_path(mount))
|
|
2327
2368
|
stop_sec = max(30, int(resolved.llm_timeout) + int(resolved.ntfy_timeout) + 15)
|
|
2328
2369
|
lines.extend([
|
|
2329
2370
|
'',
|
|
@@ -2503,7 +2544,7 @@ def build_parser():
|
|
|
2503
2544
|
svc.add_argument('--print-unit', action='store_true', help='Print the systemd unit that --install-service would write, then exit. Works without systemd.')
|
|
2504
2545
|
svc.add_argument('--uninstall-service', action='store_true', help='Disable and remove the systemd unit. Warns and exits if systemd is not available.')
|
|
2505
2546
|
parser.add_argument('--unit-name', type=str, default=DEFAULT_UNIT_NAME, help='systemd unit name (default: firewatcher).')
|
|
2506
|
-
parser.add_argument('--requires-mounts-for', action='append', default=[], metavar='PATH', help='Add RequiresMountsFor= to the unit (repeatable). Use when -o is on NFS or another remote filesystem.')
|
|
2547
|
+
parser.add_argument('--requires-mounts-for', action='append', default=[], metavar='PATH', help='Add RequiresMountsFor= to the unit (repeatable). Use when -o is on NFS or another remote filesystem. The path is made absolute and normalized (symlinks are not followed); one with a newline or not in UTF-8 is refused.')
|
|
2507
2548
|
parser.add_argument('--no-enable', action='store_true', help='With --install-service, write the unit and daemon-reload but do not enable or start it.')
|
|
2508
2549
|
|
|
2509
2550
|
parser.add_argument('--config', default=None, help='INI config file. Default: /etc/firewatcher/firewatcher.conf when that file exists. An explicit path must exist. CLI flags override the file.')
|
|
@@ -104,7 +104,7 @@ def test_parser_rejects_conflicting_service_flags():
|
|
|
104
104
|
def test_module_has_no_nebula_product_name():
|
|
105
105
|
src = open(fw.__file__, encoding='utf-8').read().lower()
|
|
106
106
|
assert 'nebula' not in src
|
|
107
|
-
assert fw.version == '1.
|
|
107
|
+
assert fw.version == '1.64'
|
|
108
108
|
assert fw.__version__ == fw.version
|
|
109
109
|
|
|
110
110
|
|
|
@@ -535,3 +535,184 @@ def test_quote_unit_arg_keeps_a_bare_semicolon_and_newlines_inside_one_argument(
|
|
|
535
535
|
# A bare ';' ends an ExecStart command, and a raw newline would end the line (or add a directive).
|
|
536
536
|
assert fw._quote_unit_arg(';') == '";"'
|
|
537
537
|
assert fw._quote_unit_arg('m\nExecStartPre=/bin/false\r') == '"m\\nExecStartPre=/bin/false\\r"'
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
# --- RequiresMountsFor= paths ---
|
|
541
|
+
# Checked against systemd 257: a bind mount at each path, then which mount unit the unit's After= named.
|
|
542
|
+
|
|
543
|
+
def _mounts_line(*paths):
|
|
544
|
+
argv = ['--print-unit']
|
|
545
|
+
for path in paths:
|
|
546
|
+
argv += ['--requires-mounts-for', path]
|
|
547
|
+
unit = fw.render_unit_file(parse_cli(argv + ['/etc/firewatcher/patterns.d']), executable='/bin/fw')
|
|
548
|
+
return [line for line in unit.splitlines() if line.startswith('RequiresMountsFor=')]
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def _systemd_reads(value):
|
|
552
|
+
"""How systemd reads one RequiresMountsFor= value: words split on whitespace, quotes group, a backslash
|
|
553
|
+
inside double quotes takes the next character, %% is one %."""
|
|
554
|
+
words, word, quoted, started, chars = [], '', False, False, iter(value)
|
|
555
|
+
for ch in chars:
|
|
556
|
+
if quoted and ch == '\\':
|
|
557
|
+
word += next(chars)
|
|
558
|
+
elif ch == '"':
|
|
559
|
+
quoted, started = not quoted, True
|
|
560
|
+
elif ch.isspace() and not quoted:
|
|
561
|
+
if started or word:
|
|
562
|
+
words.append(word)
|
|
563
|
+
word, started = '', False
|
|
564
|
+
else:
|
|
565
|
+
word += ch
|
|
566
|
+
if started or word:
|
|
567
|
+
words.append(word)
|
|
568
|
+
return [w.replace('%%', '%') for w in words]
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
@pytest.mark.parametrize('path, line', [
|
|
572
|
+
('/mnt/logs/captured_messages', 'RequiresMountsFor=/mnt/logs/captured_messages'),
|
|
573
|
+
('/mnt/a$b', 'RequiresMountsFor=/mnt/a$b'),
|
|
574
|
+
('/mnt/a;b', 'RequiresMountsFor=/mnt/a;b'),
|
|
575
|
+
('/mnt/a#b', 'RequiresMountsFor=/mnt/a#b'),
|
|
576
|
+
('/mnt/my logs', 'RequiresMountsFor="/mnt/my logs"'),
|
|
577
|
+
('/mnt/a\tb', 'RequiresMountsFor="/mnt/a\tb"'),
|
|
578
|
+
('/mnt/100%', 'RequiresMountsFor=/mnt/100%%'),
|
|
579
|
+
('/mnt/%h y', 'RequiresMountsFor="/mnt/%%h y"'),
|
|
580
|
+
('/mnt/a"b', 'RequiresMountsFor="/mnt/a\\"b"'),
|
|
581
|
+
('/mnt/a\\b', 'RequiresMountsFor="/mnt/a\\\\b"'),
|
|
582
|
+
("/mnt/a'b", 'RequiresMountsFor="/mnt/a\'b"'),
|
|
583
|
+
('/mnt/a b\\', 'RequiresMountsFor="/mnt/a b\\\\"'),
|
|
584
|
+
('/mnt/a"b c\\d', 'RequiresMountsFor="/mnt/a\\"b c\\\\d"'),
|
|
585
|
+
])
|
|
586
|
+
def test_requires_mounts_for_line_is_quoted_so_systemd_reads_the_path_back(path, line):
|
|
587
|
+
assert _mounts_line(path) == [line]
|
|
588
|
+
assert _systemd_reads(line.partition('=')[2]) == [path]
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def test_requires_mounts_for_paths_are_normalized_as_text(tmp_path):
|
|
592
|
+
# systemd ignores '..', '//' and '.' ("not normalized"); normalize as text, follow no symlinks.
|
|
593
|
+
assert _mounts_line('/mnt/logs/') == ['RequiresMountsFor=/mnt/logs']
|
|
594
|
+
assert _mounts_line('//mnt/./logs') == ['RequiresMountsFor=/mnt/logs']
|
|
595
|
+
assert _mounts_line('/mnt/nfs/../logs') == ['RequiresMountsFor=/mnt/logs']
|
|
596
|
+
assert _mounts_line('/mnt/a..b') == ['RequiresMountsFor=/mnt/a..b']
|
|
597
|
+
(tmp_path / 'nfs').mkdir()
|
|
598
|
+
(tmp_path / 'data').symlink_to(tmp_path / 'nfs')
|
|
599
|
+
assert _mounts_line(str(tmp_path / 'data' / 'logs')) == ['RequiresMountsFor=%s' % (tmp_path / 'data' / 'logs')]
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def test_requires_mounts_for_touches_no_file(monkeypatch):
|
|
603
|
+
# A hung hard NFS mount or an automount point must not stall or trigger anything.
|
|
604
|
+
def no_fs(*args, **kwargs):
|
|
605
|
+
raise AssertionError('the file system was touched')
|
|
606
|
+
|
|
607
|
+
for name in ('lstat', 'stat', 'readlink'):
|
|
608
|
+
monkeypatch.setattr(fw.os, name, no_fs)
|
|
609
|
+
assert fw._mount_paths(['/mnt/nfs/../logs', '/mnt/x']) == ['/mnt/logs', '/mnt/x']
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
def test_requires_mounts_for_refuses_a_newline_the_current_directory_brings_in(tmp_path, monkeypatch):
|
|
613
|
+
odd = tmp_path / 'a\nb'
|
|
614
|
+
odd.mkdir()
|
|
615
|
+
monkeypatch.chdir(str(odd))
|
|
616
|
+
with pytest.raises(fw.ConfigError) as err:
|
|
617
|
+
fw.resolve_settings(parse_cli(['--print-unit', '--requires-mounts-for', 'logs', 'p.d']), environ={})
|
|
618
|
+
assert 'newline' in str(err.value)
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
def test_requires_mounts_for_keeps_a_utf8_path_whatever_python_decoded(tmp_path):
|
|
622
|
+
# Python 3.6 under a C locale hands over 'caf\u00e9' as surrogate-escaped bytes; the unit gets the text.
|
|
623
|
+
path = b'/mnt/caf\xc3\xa9'.decode('ascii', 'surrogateescape')
|
|
624
|
+
assert _mounts_line(path) == ['RequiresMountsFor=/mnt/caf\u00e9']
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
def test_requires_mounts_for_refuses_a_path_that_is_not_utf8():
|
|
628
|
+
path = b'/mnt/caf\xe9'.decode('utf-8', 'surrogateescape')
|
|
629
|
+
with pytest.raises(fw.ConfigError) as err:
|
|
630
|
+
fw.resolve_settings(parse_cli(['--print-unit', '--requires-mounts-for', path, 'p.d']), environ={})
|
|
631
|
+
assert 'not UTF-8' in str(err.value)
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
def test_requires_mounts_for_relative_path_with_the_current_directory_gone(monkeypatch):
|
|
635
|
+
def gone():
|
|
636
|
+
raise FileNotFoundError(2, 'No such file or directory')
|
|
637
|
+
|
|
638
|
+
monkeypatch.setattr(fw.os, 'getcwd', gone)
|
|
639
|
+
monkeypatch.setattr(fw.os, 'getcwdb', gone)
|
|
640
|
+
with pytest.raises(fw.ConfigError) as err:
|
|
641
|
+
fw.resolve_settings(parse_cli(['--print-unit', '--requires-mounts-for', 'logs', 'p.d']), environ={})
|
|
642
|
+
assert 'cannot make' in str(err.value)
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
def test_requires_mounts_for_relative_path_in_a_non_utf8_directory_is_refused(tmp_path, monkeypatch):
|
|
646
|
+
# The current directory's bytes count too: the resolved path must be UTF-8 as a whole.
|
|
647
|
+
odd = os.path.join(os.fsencode(str(tmp_path)), b'caf\xe9')
|
|
648
|
+
os.mkdir(odd)
|
|
649
|
+
monkeypatch.chdir(odd)
|
|
650
|
+
with pytest.raises(fw.ConfigError) as err:
|
|
651
|
+
fw.resolve_settings(parse_cli(['--print-unit', '--requires-mounts-for', 'logs', 'p.d']), environ={})
|
|
652
|
+
assert 'not UTF-8' in str(err.value)
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
def test_requires_mounts_for_keeps_each_path_a_separate_line_and_adds_remote_fs():
|
|
656
|
+
lines = _mounts_line('/mnt/my logs', '/mnt/b')
|
|
657
|
+
assert lines == ['RequiresMountsFor="/mnt/my logs"', 'RequiresMountsFor=/mnt/b']
|
|
658
|
+
unit = fw.render_unit_file(parse_cli(['--print-unit', '--requires-mounts-for', '/mnt/my logs', 'p.d']),
|
|
659
|
+
executable='/bin/fw')
|
|
660
|
+
assert 'remote-fs.target' in unit
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
def test_requires_mounts_for_makes_a_relative_path_absolute(tmp_path, monkeypatch):
|
|
664
|
+
monkeypatch.chdir(tmp_path)
|
|
665
|
+
assert _mounts_line('nfs/logs') == ['RequiresMountsFor=' + str(tmp_path / 'nfs' / 'logs')]
|
|
666
|
+
assert _mounts_line('./a b') == ['RequiresMountsFor="%s"' % (tmp_path / 'a b')]
|
|
667
|
+
assert fw.resolve_settings(parse_cli(['--requires-mounts-for', 'x', 'p.d']),
|
|
668
|
+
environ={}).requires_mounts_for == [str(tmp_path / 'x')]
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
def test_requires_mounts_for_empty_path_is_dropped():
|
|
672
|
+
assert _mounts_line('') == []
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
@pytest.mark.parametrize('path', ['/mnt/a\nb', '/mnt/a\rb', 'rel\nx'])
|
|
676
|
+
def test_requires_mounts_for_refuses_a_newline_or_carriage_return(path, capsys):
|
|
677
|
+
with pytest.raises(fw.ConfigError) as exc:
|
|
678
|
+
fw.resolve_settings(parse_cli(['--requires-mounts-for', path, 'p.d']), environ={})
|
|
679
|
+
assert '--requires-mounts-for' in str(exc.value)
|
|
680
|
+
assert 'newline' in str(exc.value)
|
|
681
|
+
with pytest.raises(SystemExit) as exit_info:
|
|
682
|
+
fw.main(['--print-unit', '--requires-mounts-for', path])
|
|
683
|
+
assert exit_info.value.code == 2
|
|
684
|
+
assert '--requires-mounts-for' in capsys.readouterr().err
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
def test_install_refuses_a_bad_mount_path_before_writing_anything(tmp_path, capsys):
|
|
688
|
+
unit_dir = tmp_path / 'system'
|
|
689
|
+
patterns = tmp_path / 'patterns.d'
|
|
690
|
+
out = tmp_path / 'captures'
|
|
691
|
+
calls = []
|
|
692
|
+
parsed = parse_cli(['--install-service', '-o', str(out), '--requires-mounts-for', '/mnt/a\nb', str(patterns)])
|
|
693
|
+
rc = fw.install_service(parsed, systemd_available=True, unit_dir=str(unit_dir),
|
|
694
|
+
systemctl=systemctl_recorder(calls), executable='/bin/fw')
|
|
695
|
+
assert rc == 2
|
|
696
|
+
err = capsys.readouterr().err
|
|
697
|
+
assert '--requires-mounts-for' in err and 'newline' in err
|
|
698
|
+
assert not unit_dir.exists() and not patterns.exists() and not out.exists()
|
|
699
|
+
assert not os.path.exists(fw.DEFAULT_CONFIG_PATH)
|
|
700
|
+
assert calls == []
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
def test_main_install_refuses_a_bad_mount_path_with_exit_2_and_no_traceback(tmp_path, capsys):
|
|
704
|
+
out = tmp_path / 'captures'
|
|
705
|
+
with pytest.raises(SystemExit) as exit_info:
|
|
706
|
+
fw.main(['--install-service', '-o', str(out), '--requires-mounts-for', '/mnt/a\rb', str(tmp_path / 'p.d')])
|
|
707
|
+
assert exit_info.value.code == 2
|
|
708
|
+
assert 'Traceback' not in capsys.readouterr().err
|
|
709
|
+
assert not out.exists() and not os.path.exists(fw.DEFAULT_CONFIG_PATH)
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
def test_install_writes_a_quoted_mount_path_to_the_unit(tmp_path):
|
|
713
|
+
unit_dir = tmp_path / 'system'
|
|
714
|
+
parsed = parse_cli(['--install-service', '--no-enable', '-o', str(tmp_path / 'out'),
|
|
715
|
+
'--requires-mounts-for', '/mnt/my logs', str(tmp_path / 'p.d')])
|
|
716
|
+
assert fw.install_service(parsed, systemd_available=True, unit_dir=str(unit_dir),
|
|
717
|
+
systemctl=ok_systemctl, executable='/bin/fw') == 0
|
|
718
|
+
assert 'RequiresMountsFor="/mnt/my logs"\n' in (unit_dir / 'firewatcher.service').read_text()
|
|
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
|