pydcache 0.1.6__py3-none-any.whl
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.
- pydcache/__init__.py +3 -0
- pydcache/config/__init__.py +79 -0
- pydcache/config/cta-nanny.service +32 -0
- pydcache/config/pydcache.yaml +29 -0
- pydcache/py.typed +0 -0
- pydcache/scripts/__init__.py +0 -0
- pydcache/scripts/cta_nanny.py +383 -0
- pydcache/scripts/install_service.py +97 -0
- pydcache/util/__init__.py +3 -0
- pydcache/util/admin.py +174 -0
- pydcache/util/kerberos.py +54 -0
- pydcache/util/ostools.py +51 -0
- pydcache/util/paramiko.py +40 -0
- pydcache/util/psycopg.py +165 -0
- pydcache-0.1.6.dist-info/METADATA +47 -0
- pydcache-0.1.6.dist-info/RECORD +20 -0
- pydcache-0.1.6.dist-info/WHEEL +5 -0
- pydcache-0.1.6.dist-info/entry_points.txt +2 -0
- pydcache-0.1.6.dist-info/licenses/LICENSE +661 -0
- pydcache-0.1.6.dist-info/top_level.txt +1 -0
pydcache/__init__.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""pydcache.config – configuration file deployment helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import shutil
|
|
8
|
+
import stat
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
# Bundled example / template config shipped inside the package
|
|
14
|
+
_TEMPLATE = Path(__file__).parent / "pydcache.yaml"
|
|
15
|
+
|
|
16
|
+
# Environment variable that overrides automatic path resolution
|
|
17
|
+
_ENV_VAR = "DCACHE_CONFIG"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _default_config_path() -> Path:
|
|
21
|
+
"""Return the platform-appropriate config path for the current user.
|
|
22
|
+
|
|
23
|
+
* root (uid 0) → /etc/pydcache/pydcache.yaml
|
|
24
|
+
* regular user → ~/.config/pydcache/pydcache.yaml (XDG Base Dir spec)
|
|
25
|
+
"""
|
|
26
|
+
if os.getuid() == 0:
|
|
27
|
+
return Path("/etc/pydcache/pydcache.yaml")
|
|
28
|
+
xdg_config = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config"))
|
|
29
|
+
return xdg_config / "pydcache" / "pydcache.yaml"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_config_path() -> Path:
|
|
33
|
+
"""Return the resolved config path, deploying the template if needed.
|
|
34
|
+
|
|
35
|
+
Resolution order:
|
|
36
|
+
|
|
37
|
+
1. ``$DCACHE_CONFIG`` environment variable (no auto-deploy, must exist).
|
|
38
|
+
2. Platform default (``/etc/pydcache/`` or ``~/.config/pydcache/``);
|
|
39
|
+
the bundled template is copied there on first use.
|
|
40
|
+
|
|
41
|
+
The deployed file is always created with mode **0600**.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Path to the config file to load.
|
|
45
|
+
|
|
46
|
+
Raises:
|
|
47
|
+
FileNotFoundError: If ``$DCACHE_CONFIG`` is set but does not exist.
|
|
48
|
+
"""
|
|
49
|
+
env_override = os.environ.get(_ENV_VAR)
|
|
50
|
+
if env_override:
|
|
51
|
+
path = Path(env_override)
|
|
52
|
+
if not path.exists():
|
|
53
|
+
raise FileNotFoundError(
|
|
54
|
+
f"{_ENV_VAR}={path} — file does not exist"
|
|
55
|
+
)
|
|
56
|
+
return path
|
|
57
|
+
|
|
58
|
+
path = _default_config_path()
|
|
59
|
+
if not path.exists():
|
|
60
|
+
_deploy_template(path)
|
|
61
|
+
return path
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _deploy_template(dest: Path) -> None:
|
|
65
|
+
"""Copy the bundled template to *dest* and set permissions to 0600.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
dest: Destination path (parent directories are created as needed).
|
|
69
|
+
|
|
70
|
+
Raises:
|
|
71
|
+
OSError: If the directory cannot be created or the file written.
|
|
72
|
+
"""
|
|
73
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
74
|
+
shutil.copy2(_TEMPLATE, dest)
|
|
75
|
+
dest.chmod(stat.S_IRUSR | stat.S_IWUSR) # 0600
|
|
76
|
+
logger.info(
|
|
77
|
+
"Deployed example config to %s — please fill in your credentials.",
|
|
78
|
+
dest,
|
|
79
|
+
)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description=CTA Nanny – dCache/CTA duplicate-key repair daemon
|
|
3
|
+
Documentation=https://github.com/DmitryLitvintsev/pydcache
|
|
4
|
+
After=network-online.target
|
|
5
|
+
Wants=network-online.target
|
|
6
|
+
|
|
7
|
+
[Service]
|
|
8
|
+
Type=simple
|
|
9
|
+
User=root
|
|
10
|
+
Group=root
|
|
11
|
+
|
|
12
|
+
# Path to your filled-in config (copy from /etc/pydcache/pydcache.yaml and edit)
|
|
13
|
+
Environment="DCACHE_CONFIG=/etc/pydcache/pydcache.yaml"
|
|
14
|
+
|
|
15
|
+
# Adjust --cpu-count and --instance to match your site
|
|
16
|
+
ExecStart=CTA_NANNY_EXEC --cpu-count 10 --instance public_prd
|
|
17
|
+
|
|
18
|
+
# Restart policy
|
|
19
|
+
Restart=on-failure
|
|
20
|
+
RestartSec=30s
|
|
21
|
+
|
|
22
|
+
# Resource limits
|
|
23
|
+
LimitNOFILE=65536
|
|
24
|
+
|
|
25
|
+
# Send stdout/stderr to the journal
|
|
26
|
+
StandardOutput=journal
|
|
27
|
+
StandardError=journal
|
|
28
|
+
SyslogIdentifier=cta-nanny
|
|
29
|
+
|
|
30
|
+
[Install]
|
|
31
|
+
WantedBy=multi-user.target
|
|
32
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# cta_nanny configuration example
|
|
2
|
+
#
|
|
3
|
+
# Copy this file to a secure location, fill in your real values,
|
|
4
|
+
# and point cta-nanny at it via the DCACHE_CONFIG environment variable:
|
|
5
|
+
#
|
|
6
|
+
# export DCACHE_CONFIG=/path/to/cta_nanny.yaml
|
|
7
|
+
#
|
|
8
|
+
# WARNING: this file contains credentials — do NOT commit it to version control.
|
|
9
|
+
|
|
10
|
+
# PostgreSQL connection URIs (require read/write access)
|
|
11
|
+
# Format: postgresql://<user>:<password>@<host>:<port>/<database>
|
|
12
|
+
cta_db: "postgresql://cta_user:changeme@cta-db.example.com:5432/cta_prd"
|
|
13
|
+
chimera_db: "postgresql://chimera_user:changeme@chimera-db.example.com:5432/chimera"
|
|
14
|
+
|
|
15
|
+
# dCache admin SSH shell
|
|
16
|
+
admin:
|
|
17
|
+
host: dcache-admin.example.com
|
|
18
|
+
port: 22224
|
|
19
|
+
user: dcache_admin_user
|
|
20
|
+
|
|
21
|
+
# Kafka consumer
|
|
22
|
+
kafka:
|
|
23
|
+
# Topic produced by cta-taped ingest logs
|
|
24
|
+
topic: "ingest.logs.cta.taped"
|
|
25
|
+
# topic: "ingest.dcache.billing"
|
|
26
|
+
group: "dcache-ctananny-consumer-group"
|
|
27
|
+
# group: "dcache-public-stores"
|
|
28
|
+
bootstrap_servers: "kafka-broker.example.com:9092"
|
|
29
|
+
|
pydcache/py.typed
ADDED
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import logging.config
|
|
5
|
+
import multiprocessing
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import socket
|
|
9
|
+
import sys
|
|
10
|
+
import time
|
|
11
|
+
import traceback
|
|
12
|
+
from multiprocessing import Lock, Process, Queue
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Dict, List, Optional, Tuple, NoReturn
|
|
15
|
+
from urllib.parse import urlparse
|
|
16
|
+
|
|
17
|
+
import paramiko
|
|
18
|
+
import psycopg2
|
|
19
|
+
import psycopg2.extensions
|
|
20
|
+
import psycopg2.extras
|
|
21
|
+
import yaml
|
|
22
|
+
|
|
23
|
+
from pydcache.util import admin, kerberos, ostools, paramiko, psycopg
|
|
24
|
+
from pydcache.config import get_config_path
|
|
25
|
+
|
|
26
|
+
from kafka import KafkaConsumer
|
|
27
|
+
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
epoch_time\":1775496237.108132665,\"local_time\":\"2026-04-06T12:23:57-0500\",\"hostname\":\"tpsrvf2104\",\"program\":\"cta-taped\",\"log_level\":\"ERROR\",\"pid\":1160350,\"tid\":1638518,\"message\":\"In ArchiveMount::reportJobsBatchTransferred(): got an exception\",\"drive_name\":\"F1_F9B5D4\",\"instance\":\"prd\",\"sched_backend\":\"cephUser\",\"thread\":\"MainThread\",\"tapeDrive\":\"F1_F9B5D4\",\"mountId\":\"453120\",\"vo\":\"cms\",\"tapePool\":\"cms.Run2025DPrompt\",\"successfulBatchSize\":7,\"exceptionMessageValue\":\"commit problem committing the DB transaction: Database library reported: ERROR: duplicate key value violates unique constraint \"archive_file_din_dfi_un\"DETAIL: Key (disk_instance_name, disk_file_id)=(cms_prd, 00003DE869D8B37F4516A7FF64556A8A7E01) already exists. (DB Result Status:7 SQLState:23505)\"}
|
|
31
|
+
|
|
32
|
+
{'date': '2026-04-13T13:04:20.794-05:00', 'msgType': 'store', 'hsm': {'instance': 'cta', 'provider': 'dcache-cta', 'type': 'cta'}, 'transferTime': 9, 'cellName': 'rw-stkendca61a-2', 'session': 'pool:rw-stkendca61a-2@rw-stkendca61a-2Domain:1776103460794-37454', 'version': '1.0', 'storageInfo': 'dune.dune@cta', 'cellType': 'pool', 'fileSize': 2967538045, 'queuingTime': 0, 'cellDomain': 'rw-stkendca61a-2Domain', 'locations': [], 'pnfsid': '0000082C1CA24ABE411FA957DCB563929441', 'transaction': 'pool:rw-stkendca61a-2@rw-stkendca61a-2Domain:1776103460794-37454', 'billingPath': '/pnfs/fnal.gov/usr/dune/tape_backed/dunepro/beam-data/040416/foo.root', 'status': {'msg': 'io.grpc.StatusRuntimeException: ABORTED: Storage class dune.dune@cta has no archive routes', 'code': 10011}}
|
|
33
|
+
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
# Configure logging
|
|
37
|
+
logging.basicConfig(
|
|
38
|
+
level=logging.INFO,
|
|
39
|
+
format='%(asctime)s - %(levelname)s - %(message)s',
|
|
40
|
+
stream=sys.stderr
|
|
41
|
+
)
|
|
42
|
+
logger = logging.getLogger(__name__)
|
|
43
|
+
|
|
44
|
+
# Global locks
|
|
45
|
+
print_lock = Lock()
|
|
46
|
+
kinit_lock = Lock()
|
|
47
|
+
|
|
48
|
+
CONFIG_FILE = os.getenv("DCACHE_CONFIG") # None → auto-resolved via get_config_path()
|
|
49
|
+
SSH_HOST = "fndca"
|
|
50
|
+
SSH_PORT = 24223
|
|
51
|
+
SSH_USER = "enstore"
|
|
52
|
+
HOSTNAME = socket.getfqdn()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
_VO = "vo"
|
|
56
|
+
_INSTANCE = "instance"
|
|
57
|
+
|
|
58
|
+
def safe_json_deserializer(v):
|
|
59
|
+
try:
|
|
60
|
+
return json.loads(v.decode('utf-8')) if v else None
|
|
61
|
+
except (json.JSONDecodeError, UnicodeDecodeError) as e:
|
|
62
|
+
return None # Or log the error
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class Worker(Process):
|
|
66
|
+
"""Worker process to query CTA DB and process results."""
|
|
67
|
+
|
|
68
|
+
def __init__(
|
|
69
|
+
self,
|
|
70
|
+
queue: Queue,
|
|
71
|
+
config: Dict[str, Any]
|
|
72
|
+
) -> None:
|
|
73
|
+
"""Initialize the worker.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
queue: Task queue
|
|
77
|
+
config: Configuration dictionary
|
|
78
|
+
"""
|
|
79
|
+
super().__init__()
|
|
80
|
+
self.queue = queue
|
|
81
|
+
self.config = config
|
|
82
|
+
|
|
83
|
+
def run(self) -> None:
|
|
84
|
+
"""Process tasks from the queue."""
|
|
85
|
+
cta_db = chimera_db = ssh = None
|
|
86
|
+
try:
|
|
87
|
+
cta_db = psycopg.create_connection(self.config["cta_db"])
|
|
88
|
+
chimera_db = psycopg.create_connection(self.config["chimera_db"])
|
|
89
|
+
ssh = paramiko.get_shell(
|
|
90
|
+
self.config["admin"].get("host", SSH_HOST),
|
|
91
|
+
self.config["admin"].get("port", SSH_PORT),
|
|
92
|
+
self.config["admin"].get("user", SSH_USER)
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
for pnfsid in iter(self.queue.get, None):
|
|
96
|
+
self._process_file(ssh, cta_db, chimera_db, pnfsid)
|
|
97
|
+
except Exception as exc:
|
|
98
|
+
error_string = traceback.format_exc()
|
|
99
|
+
#logger.error("Worker failed: %s", exc)
|
|
100
|
+
logger.error(f"Worker failed: {error_string}")
|
|
101
|
+
finally:
|
|
102
|
+
for conn in (ssh, cta_db, chimera_db):
|
|
103
|
+
if conn:
|
|
104
|
+
try:
|
|
105
|
+
conn.close()
|
|
106
|
+
except Exception:
|
|
107
|
+
pass
|
|
108
|
+
|
|
109
|
+
def _process_file(
|
|
110
|
+
self,
|
|
111
|
+
ssh,
|
|
112
|
+
cta_db: psycopg2.extensions.connection,
|
|
113
|
+
chimera_db: psycopg2.extensions.connection,
|
|
114
|
+
pnfsid: str
|
|
115
|
+
) -> None:
|
|
116
|
+
"""Process a single file.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
ssh: SSH connection
|
|
120
|
+
cta_db: CTA database connection
|
|
121
|
+
chimera_db: Chimera database connection
|
|
122
|
+
pnfsid: PNFS ID to process
|
|
123
|
+
instance: CTA instance name
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
rows = psycopg.select(
|
|
127
|
+
cta_db,
|
|
128
|
+
"select af.disk_instance_name, "
|
|
129
|
+
"sc.storage_class_name, "
|
|
130
|
+
"'cta://cta/'||af.disk_file_id||'?archiveid='||af.archive_file_id as location "
|
|
131
|
+
"from archive_file af inner join storage_class sc on sc.storage_class_id = af.storage_class_id "
|
|
132
|
+
"where af.disk_file_id = %s and af.creation_time < %s",
|
|
133
|
+
(pnfsid, int(time.time()) - 6 * 3600)
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
if not rows:
|
|
137
|
+
with print_lock:
|
|
138
|
+
logger.error(f"Failed to find CTA location for {pnfsid}")
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
disk_instance_name = rows[0]["disk_instance_name"]
|
|
143
|
+
location = rows[0]["location"]
|
|
144
|
+
storage_class = rows[0]["storage_class_name"]
|
|
145
|
+
storage_group, file_family = storage_class.split("@")[0].split(".")
|
|
146
|
+
|
|
147
|
+
with print_lock:
|
|
148
|
+
logger.error(f"Processing {pnfsid}, {disk_instance_name}, {storage_class}")
|
|
149
|
+
|
|
150
|
+
result = psycopg.select (
|
|
151
|
+
chimera_db,
|
|
152
|
+
"select count(*) from t_locationinfo "
|
|
153
|
+
"where itype = 0 and inumber = "
|
|
154
|
+
"(select inumber from t_inodes where ipnfsid = %s)",
|
|
155
|
+
(pnfsid,)
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
if result[0]["count"] != 0:
|
|
159
|
+
with print_lock:
|
|
160
|
+
logger.error(
|
|
161
|
+
f"File has location in chimera {pnfsid} {storage_class}, Skipping"
|
|
162
|
+
)
|
|
163
|
+
return
|
|
164
|
+
|
|
165
|
+
psycopg.insert(
|
|
166
|
+
chimera_db,
|
|
167
|
+
"insert into t_storageinfo "
|
|
168
|
+
"(inumber, ihsmname, istoragegroup, istoragesubgroup) "
|
|
169
|
+
"values (pnfsid2inumber(%s), 'cta', %s, %s)",
|
|
170
|
+
(pnfsid, storage_group, file_family)
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
psycopg.insert(
|
|
174
|
+
chimera_db,
|
|
175
|
+
"insert into t_locationinfo "
|
|
176
|
+
"(inumber, itype, ipriority, ictime, iatime, istate, ilocation) "
|
|
177
|
+
"values (pnfsid2inumber(%s), 0, 10, now(), now(), 1, %s)",
|
|
178
|
+
(pnfsid, location)
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
admin.execute_admin_command(
|
|
182
|
+
ssh,
|
|
183
|
+
f"\\sl {pnfsid} rep set cached {pnfsid}"
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
admin.execute_admin_command(
|
|
187
|
+
ssh,
|
|
188
|
+
f"\\sl {pnfsid} st kill {pnfsid}"
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def main() -> None:
|
|
193
|
+
|
|
194
|
+
"""Main function to process files that are already on tape."""
|
|
195
|
+
parser = argparse.ArgumentParser(
|
|
196
|
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
197
|
+
description="Process files that are already on tape and update their status"
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
parser.add_argument(
|
|
201
|
+
"--cpu-count",
|
|
202
|
+
type=int,
|
|
203
|
+
default=10,
|
|
204
|
+
help="Number of worker processes to spawn"
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
parser.add_argument(
|
|
208
|
+
"--verbose",
|
|
209
|
+
action="store_true",
|
|
210
|
+
help="Enable verbose logging"
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
parser.add_argument(
|
|
214
|
+
"-i", "--instance",
|
|
215
|
+
help='instance',
|
|
216
|
+
default="public_prd",
|
|
217
|
+
metavar="INSTANCE"
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
args = parser.parse_args()
|
|
221
|
+
|
|
222
|
+
# Load configuration
|
|
223
|
+
try:
|
|
224
|
+
config_path = get_config_path()
|
|
225
|
+
if config_path.stat().st_mode != 33152:
|
|
226
|
+
logger.error(
|
|
227
|
+
"Config file %s permissions too permissive, should be 0600",
|
|
228
|
+
config_path
|
|
229
|
+
)
|
|
230
|
+
sys.exit(1)
|
|
231
|
+
|
|
232
|
+
config = yaml.safe_load(config_path.read_text())
|
|
233
|
+
if not config:
|
|
234
|
+
logger.error("Failed to load configuration from %s", config_path)
|
|
235
|
+
sys.exit(1)
|
|
236
|
+
|
|
237
|
+
except (OSError, IOError) as exc:
|
|
238
|
+
if isinstance(exc, FileNotFoundError):
|
|
239
|
+
logger.error("Config file %s does not exist", exc)
|
|
240
|
+
else:
|
|
241
|
+
logger.error("Error reading config file: %s", exc)
|
|
242
|
+
sys.exit(1)
|
|
243
|
+
except yaml.YAMLError as exc:
|
|
244
|
+
logger.error("Error parsing config file: %s", exc)
|
|
245
|
+
sys.exit(1)
|
|
246
|
+
|
|
247
|
+
logger.info("Starting processing")
|
|
248
|
+
|
|
249
|
+
# Start Kerberos ticket refresh worker
|
|
250
|
+
kinit_worker = kerberos.KinitWorker()
|
|
251
|
+
kinit_worker.start()
|
|
252
|
+
|
|
253
|
+
# Set up worker processes
|
|
254
|
+
queue: Queue = Queue(maxsize=100)
|
|
255
|
+
workers = [
|
|
256
|
+
Worker(queue, config)
|
|
257
|
+
for _ in range(args.cpu_count)
|
|
258
|
+
]
|
|
259
|
+
|
|
260
|
+
for worker in workers:
|
|
261
|
+
worker.start()
|
|
262
|
+
|
|
263
|
+
consumer = KafkaConsumer(config["kafka"].get("topic", "ingest.logs.cta.taped"),
|
|
264
|
+
group_id=config["kafka"].get("group", "dcache-ctananny-prd"), # Required to resume
|
|
265
|
+
bootstrap_servers=config["kafka"].get("bootstrap_servers", "lskafka:9092"),
|
|
266
|
+
auto_offset_reset='latest', # Fallback if no offset is found
|
|
267
|
+
#auto_offset_reset='earliest', # Fallback if no offset is found
|
|
268
|
+
enable_auto_commit=True, # Automatically save progress
|
|
269
|
+
value_deserializer=lambda m: safe_json_deserializer(m))
|
|
270
|
+
#value_deserializer=lambda m: json.loads(m.decode("utf-8")))
|
|
271
|
+
|
|
272
|
+
try:
|
|
273
|
+
for msg in consumer:
|
|
274
|
+
|
|
275
|
+
if os.path.exists("/tmp/STOP"):
|
|
276
|
+
os.unlink("/tmp/STOP")
|
|
277
|
+
break
|
|
278
|
+
# Skip invalid bytes entirely
|
|
279
|
+
#message = msg.value.decode('utf-8', errors='ignore')
|
|
280
|
+
#print(message.keys())
|
|
281
|
+
|
|
282
|
+
# Replace invalid bytes with a placeholder ()
|
|
283
|
+
#decoded_value = message.value.decode('utf-8', errors='replace')
|
|
284
|
+
message = msg.value
|
|
285
|
+
if not message:
|
|
286
|
+
print(f"WHAT {msg}")
|
|
287
|
+
continue
|
|
288
|
+
message_string = message["message"]
|
|
289
|
+
payload = message["cta"]
|
|
290
|
+
if _VO in payload and _INSTANCE in payload:
|
|
291
|
+
vo = payload.get(_VO)
|
|
292
|
+
instance = payload.get(_INSTANCE)
|
|
293
|
+
exception_message = payload.get("exceptionMessageValue")
|
|
294
|
+
if not exception_message:
|
|
295
|
+
continue
|
|
296
|
+
logger.debug(f"Found exception {vo} {instance} {exception_message}")
|
|
297
|
+
if exception_message.find("duplicate key value violates unique constraint") != -1:
|
|
298
|
+
try:
|
|
299
|
+
tuple = exception_message.split("=")[1].split("already")[0].strip()
|
|
300
|
+
disk_instance, pnfsid = [i.strip() for i in re.sub("[()]", "", tuple).split(",")]
|
|
301
|
+
|
|
302
|
+
if disk_instance == args.instance:
|
|
303
|
+
logger.debug(f"Found {pnfsid} {disk_instance}, putting on the queue")
|
|
304
|
+
queue.put(pnfsid)
|
|
305
|
+
except Exception as e:
|
|
306
|
+
logger.info(f"Caught exception {e}")
|
|
307
|
+
pass
|
|
308
|
+
except KeyboardInterrupt:
|
|
309
|
+
logger.info("Interrupted successfully.")
|
|
310
|
+
finally:
|
|
311
|
+
for _ in range(args.cpu_count):
|
|
312
|
+
queue.put(None)
|
|
313
|
+
# Clean up
|
|
314
|
+
kinit_worker.stop()
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
# consumer = KafkaConsumer(config["kafka"].get("topic", "ingest.dcache.billing"),
|
|
318
|
+
# group_id=config["kafka"].get("group", "dcache-ctananny-prd"), # Required to resume
|
|
319
|
+
# bootstrap_servers=config["kafka"].get("bootstrap_servers", "lskafka:9092"),
|
|
320
|
+
# auto_offset_reset='latest', # Fallback if no offset is found
|
|
321
|
+
# #auto_offset_reset='earliest', # Fallback if no offset is found
|
|
322
|
+
# enable_auto_commit=True, # Automatically save progress
|
|
323
|
+
# value_deserializer=lambda m: safe_json_deserializer(m))
|
|
324
|
+
# #value_deserializer=lambda m: json.loads(m.decode("utf-8")))
|
|
325
|
+
#
|
|
326
|
+
# try:
|
|
327
|
+
# for msg in consumer:
|
|
328
|
+
#
|
|
329
|
+
# if os.path.exists("/tmp/STOP"):
|
|
330
|
+
# break
|
|
331
|
+
# # Skip invalid bytes entirely
|
|
332
|
+
# #message = msg.value.decode('utf-8', errors='ignore')
|
|
333
|
+
# #print(message.keys())
|
|
334
|
+
#
|
|
335
|
+
# # Replace invalid bytes with a placeholder ()
|
|
336
|
+
# #decoded_value = message.value.decode('utf-8', errors='replace')
|
|
337
|
+
# message = msg.value
|
|
338
|
+
# if not message:
|
|
339
|
+
# print(f"WHAT {msg}")
|
|
340
|
+
# continue
|
|
341
|
+
# msgType = message["msgType"]
|
|
342
|
+
# if msgType != "store":
|
|
343
|
+
# continue
|
|
344
|
+
# error_message, error_code = message["status"].values()
|
|
345
|
+
#
|
|
346
|
+
# if error_code == 0:
|
|
347
|
+
# continue
|
|
348
|
+
# storage_info = message["storageInfo"]
|
|
349
|
+
# pnfsid = message["pnfsid"].strip()
|
|
350
|
+
# print(f"{pnfsid} {storage_info} {error_message}")
|
|
351
|
+
# print(message)
|
|
352
|
+
# continue
|
|
353
|
+
# message_string = message["message"]
|
|
354
|
+
# payload = message["cta"]
|
|
355
|
+
# if _VO in payload and _INSTANCE in payload:
|
|
356
|
+
# vo = payload.get(_VO)
|
|
357
|
+
# instance = payload.get(_INSTANCE)
|
|
358
|
+
# exception_message = payload.get("exceptionMessageValue")
|
|
359
|
+
# if not exception_message:
|
|
360
|
+
# continue
|
|
361
|
+
# logger.debug(f"Found exception {vo} {instance} {exception_message}")
|
|
362
|
+
# if exception_message.find("duplicate key value violates unique constraint") != -1:
|
|
363
|
+
# try:
|
|
364
|
+
# tuple = exception_message.split("=")[1].split("already")[0].strip()
|
|
365
|
+
# disk_instance, pnfsid = [i.strip() for i in re.sub("[()]", "", tuple).split(",")]
|
|
366
|
+
#
|
|
367
|
+
# if disk_instance == args.instance:
|
|
368
|
+
# logger.debug(f"Found {pnfsid} {disk_instance}, putting on the queue")
|
|
369
|
+
# queue.put(pnfsid)
|
|
370
|
+
# except Exception as e:
|
|
371
|
+
# logger.info(f"Caught exception {e}")
|
|
372
|
+
# pass
|
|
373
|
+
# except KeyboardInterrupt:
|
|
374
|
+
# logger.info("Interrupted successfully.")
|
|
375
|
+
# finally:
|
|
376
|
+
# for _ in range(args.cpu_count):
|
|
377
|
+
# queue.put(None)
|
|
378
|
+
# # Clean up
|
|
379
|
+
# kinit_worker.stop()
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
if __name__ == "__main__":
|
|
383
|
+
main()
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Install the cta-nanny systemd service unit file."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
# Bundled unit file template
|
|
11
|
+
_UNIT_TEMPLATE = Path(__file__).parent.parent / "config" / "cta-nanny.service"
|
|
12
|
+
_SERVICE_NAME = "cta-nanny.service"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _find_cta_nanny_exec() -> str:
|
|
16
|
+
"""Return the absolute path to the cta-nanny executable."""
|
|
17
|
+
path = shutil.which("cta-nanny")
|
|
18
|
+
if path:
|
|
19
|
+
return path
|
|
20
|
+
# Fallback: same bin/ dir as this Python interpreter
|
|
21
|
+
candidate = Path(sys.executable).parent / "cta-nanny"
|
|
22
|
+
if candidate.exists():
|
|
23
|
+
return str(candidate)
|
|
24
|
+
raise FileNotFoundError(
|
|
25
|
+
"Cannot locate the cta-nanny executable. "
|
|
26
|
+
"Make sure pydcache is installed and its bin/ directory is on PATH."
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _system_unit_dir() -> Path:
|
|
31
|
+
return Path("/etc/systemd/system")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _user_unit_dir() -> Path:
|
|
35
|
+
xdg = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config"))
|
|
36
|
+
return xdg / "systemd" / "user"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def main() -> None:
|
|
40
|
+
"""Install cta-nanny.service and print next steps."""
|
|
41
|
+
is_root = os.getuid() == 0
|
|
42
|
+
|
|
43
|
+
# Resolve real executable path and bake it into the unit file
|
|
44
|
+
try:
|
|
45
|
+
exec_path = _find_cta_nanny_exec()
|
|
46
|
+
except FileNotFoundError as exc:
|
|
47
|
+
print(f"ERROR: {exc}", file=sys.stderr)
|
|
48
|
+
sys.exit(1)
|
|
49
|
+
|
|
50
|
+
unit_text = _UNIT_TEMPLATE.read_text()
|
|
51
|
+
unit_text = unit_text.replace("CTA_NANNY_EXEC", exec_path)
|
|
52
|
+
|
|
53
|
+
if is_root:
|
|
54
|
+
dest_dir = _system_unit_dir()
|
|
55
|
+
systemctl_scope = [] # system scope (default)
|
|
56
|
+
enable_cmd = ["systemctl", "enable", "--now", _SERVICE_NAME]
|
|
57
|
+
else:
|
|
58
|
+
dest_dir = _user_unit_dir()
|
|
59
|
+
systemctl_scope = ["--user"]
|
|
60
|
+
enable_cmd = ["systemctl", "--user", "enable", "--now", _SERVICE_NAME]
|
|
61
|
+
|
|
62
|
+
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
63
|
+
dest = dest_dir / _SERVICE_NAME
|
|
64
|
+
|
|
65
|
+
dest.write_text(unit_text)
|
|
66
|
+
print(f"Installed unit file → {dest}")
|
|
67
|
+
|
|
68
|
+
if is_root:
|
|
69
|
+
# Reload systemd and (optionally) enable the service
|
|
70
|
+
subprocess.run(["systemctl", "daemon-reload"], check=True)
|
|
71
|
+
print("\nUnit file installed. Review it before enabling:\n")
|
|
72
|
+
print(f" nano {dest}\n")
|
|
73
|
+
print("Then enable and start the service:")
|
|
74
|
+
print(f" systemctl enable --now {_SERVICE_NAME}")
|
|
75
|
+
print(f"\nUseful commands:")
|
|
76
|
+
print(f" systemctl status {_SERVICE_NAME}")
|
|
77
|
+
print(f" systemctl restart {_SERVICE_NAME}")
|
|
78
|
+
print(f" journalctl -u {_SERVICE_NAME} -f")
|
|
79
|
+
else:
|
|
80
|
+
subprocess.run(["systemctl", "--user", "daemon-reload"], check=True)
|
|
81
|
+
print("\nUnit file installed (user scope). Review it before enabling:\n")
|
|
82
|
+
print(f" nano {dest}\n")
|
|
83
|
+
print("Then enable and start the service:")
|
|
84
|
+
print(f" systemctl --user enable --now {_SERVICE_NAME}")
|
|
85
|
+
print(f"\nUseful commands:")
|
|
86
|
+
print(f" systemctl --user status {_SERVICE_NAME}")
|
|
87
|
+
print(f" systemctl --user restart {_SERVICE_NAME}")
|
|
88
|
+
print(f" journalctl --user -u {_SERVICE_NAME} -f")
|
|
89
|
+
print(
|
|
90
|
+
"\nNOTE: for a user service to survive logout, run once as root:\n"
|
|
91
|
+
f" loginctl enable-linger {os.environ.get('USER', 'youruser')}"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
if __name__ == "__main__":
|
|
96
|
+
main()
|
|
97
|
+
|