11"""SystemAdministrator service is a tool to control and monitor the DIRAC services and agents"""
22
3- import socket
4- import os
5- import re
6- import time
73import getpass
84import importlib
9- import shutil
5+ import os
106import platform
11- import tempfile
7+ import re
8+ import shutil
9+ import socket
1210import subprocess
11+ import tempfile
12+ import time
1313from datetime import datetime , timedelta
14- import requests
1514
1615import psutil
17- from packaging .version import Version , InvalidVersion
18-
16+ import requests
1917from diraccfg import CFG
18+ from packaging .version import InvalidVersion , Version
2019
21- from DIRAC import S_OK , S_ERROR , gConfig , rootPath , gLogger , convertToPy3VersionNumber
20+ from DIRAC import S_ERROR , S_OK , convertToPy3VersionNumber , gConfig , gLogger , rootPath
21+ from DIRAC .ConfigurationSystem .Client import PathFinder
2222from DIRAC .Core .DISET .RequestHandler import RequestHandler
23+ from DIRAC .Core .Security .Locations import getHostCertificateAndKeyLocation
24+ from DIRAC .Core .Security .X509Chain import X509Chain # pylint: disable=import-error
2325from DIRAC .Core .Utilities import Os
2426from DIRAC .Core .Utilities .Extensions import extensionsByPriority , getExtensionMetadata
2527from DIRAC .Core .Utilities .File import mkLink
26- from DIRAC .Core .Utilities .TimeUtilities import fromString , hour , day
2728from DIRAC .Core .Utilities .Subprocess import shellCall
2829from DIRAC .Core .Utilities .ThreadScheduler import gThreadScheduler
29- from DIRAC .Core .Security .Locations import getHostCertificateAndKeyLocation
30- from DIRAC .Core .Security .X509Chain import X509Chain # pylint: disable=import-error
31- from DIRAC .ConfigurationSystem .Client import PathFinder
30+ from DIRAC .Core .Utilities .TimeUtilities import day , fromString , hour
3231from DIRAC .FrameworkSystem .Client .ComponentInstaller import gComponentInstaller
3332from DIRAC .FrameworkSystem .Client .ComponentMonitoringClient import ComponentMonitoringClient
3433
@@ -48,6 +47,76 @@ def loadDIRACCFG():
4847 return S_OK ((cfgPath , diracCFG ))
4948
5049
50+ def _normalise_version (version ):
51+ """Validate and normalise a raw version string supplied by the operator.
52+
53+ :param str version: Raw string as received from the client (may contain surrounding
54+ whitespace or use the spaced ``pkg @ url`` pip syntax).
55+ :returns: A 4-tuple ``(version, primaryExtension, released_version, isPrerelease)`` where
56+
57+ - *version* is the normalised version string ready to be passed to pip,
58+ - *primaryExtension* is the package name when the caller used
59+ ``extension==version`` syntax, or ``None`` otherwise,
60+ - *released_version* is ``True`` when installing a PEP 440 release and
61+ ``False`` when installing from a VCS URL,
62+ - *isPrerelease* is ``True`` when the PEP 440 version is a pre-release.
63+
64+ :rtype: tuple(str, str or None, bool, bool)
65+ :raises ValueError: When the version string is empty, or is not a valid PEP 440
66+ version and does not contain a recognised VCS URL.
67+ """
68+ version = version .strip ()
69+ if not version :
70+ raise ValueError ("No version specified" )
71+
72+ primaryExtension = None
73+ if "==" in version :
74+ primaryExtension , version = version .split ("==" , 1 )
75+
76+ released_version = True
77+ isPrerelease = False
78+
79+ # Special aliases: install DIRAC from the integration branch
80+ if version .lower () in ("integration" , "devel" , "master" , "main" ):
81+ released_version = False
82+ version = "DIRAC[server] @ git+https://github.com/DIRACGrid/DIRAC.git@integration"
83+ return version , primaryExtension , released_version , isPrerelease
84+
85+ # Try to parse as a PEP 440 version number
86+ try :
87+ parsed = Version (version )
88+ isPrerelease = parsed .is_prerelease
89+ version = f"v{ parsed } "
90+ except InvalidVersion :
91+ if "https://" in version :
92+ # Treat as a VCS URL (e.g. "DIRAC[server] @ git+https://...")
93+ released_version = False
94+ else :
95+ raise ValueError (f"Invalid version passed { version !r} " )
96+
97+ return version , primaryExtension , released_version , isPrerelease
98+
99+
100+ def _directory_label (version , released_version ):
101+ """Derive the filesystem directory label for a given version.
102+
103+ For released versions this is the version string itself. For VCS URLs
104+ (pip ``pkg @ url`` syntax) it is the URL part, stripped of any
105+ ``#egg=...`` fragment and surrounding whitespace.
106+
107+ :param str version: Normalised version string as returned by :func:`_normalise_version`.
108+ :param bool released_version: ``True`` when *version* is a PEP 440 release string.
109+ :returns: A filesystem-safe label derived from *version*.
110+ :rtype: str
111+ """
112+ if released_version :
113+ return version
114+ # version is "pkg @ git+https://host/repo.git@branch"
115+ # Split on the *first* "@" (the pip separator) only, then strip spaces
116+ # and drop any "#egg=..." fragment so the branch name is preserved.
117+ return version .split ("@" , 1 )[1 ].strip ().split ("#" )[0 ]
118+
119+
51120class SystemAdministratorHandler (RequestHandler ):
52121 @classmethod
53122 def initializeHandler (cls , serviceInfo ):
@@ -263,29 +332,11 @@ def export_updateSoftware(self, version):
263332 - a git tag/branch like "DIRAC[server] @ git+https://github.com/fstagni/DIRAC.git@test_branch"
264333 """
265334 # Validate and normalise the requested version
266- primaryExtension = None
267- if "==" in version :
268- primaryExtension , version = version .split ("==" )
269-
270- released_version = True
271- isPrerelease = False
272-
273- # Special cases (e.g. installing the integration/main branch)
274- if version .lower () in ["integration" , "devel" , "master" , "main" ]:
275- released_version = False
276- version = "DIRAC[server] @ git+https://github.com/DIRACGrid/DIRAC.git@integration"
277-
278- if released_version :
279- try :
280- version = Version (version )
281- isPrerelease = version .is_prerelease
282- version = f"v{ version } "
283- except InvalidVersion :
284- if "https://" in version :
285- released_version = False
286- else :
287- self .log .exception ("Invalid version passed" , version )
288- return S_ERROR (f"Invalid version passed { version !r} " )
335+ try :
336+ version , primaryExtension , released_version , isPrerelease = _normalise_version (version )
337+ except ValueError as e :
338+ self .log .exception ("Invalid version passed" , version )
339+ return S_ERROR (str (e ))
289340
290341 # Find what to install
291342 otherExtensions = []
@@ -311,7 +362,7 @@ def export_updateSoftware(self, version):
311362 installer .flush ()
312363 self .log .info ("Downloaded DIRACOS installer to" , installer .name )
313364
314- directory = version if released_version else version . split ( "@" )[ 1 ]. split ( "#" )[ 0 ]
365+ directory = _directory_label ( version , released_version )
315366 newProPrefix = os .path .join (
316367 rootPath ,
317368 "versions" ,
0 commit comments