|
| 1 | +from datetime import UTC, datetime |
| 2 | +from typing import Self |
| 3 | +from urllib.parse import urlparse |
1 | 4 | from warnings import warn |
2 | 5 |
|
3 | 6 | from dotenv import load_dotenv |
4 | | -from pydantic import BaseModel, Field, field_validator |
| 7 | +from pydantic import BaseModel, Field, computed_field, field_serializer, field_validator, model_validator |
5 | 8 | from pydantic_settings import BaseSettings, SettingsConfigDict |
6 | 9 |
|
7 | 10 | from .common.enums import ScriptMethodEnum, StatusEnum, WmsComputeSite |
@@ -210,6 +213,144 @@ class HTCondorConfiguration(BaseModel): |
210 | 213 | ) |
211 | 214 |
|
212 | 215 |
|
| 216 | +class PandaConfiguration(BaseModel, validate_assignment=True): |
| 217 | + """Configuration parameters for the PanDA WMS""" |
| 218 | + |
| 219 | + tls_url: str | None = Field( |
| 220 | + description="Base HTTPS URL of PanDA server", |
| 221 | + serialization_alias="PANDA_URL_SSL", |
| 222 | + default=None, |
| 223 | + ) |
| 224 | + |
| 225 | + url: str | None = Field( |
| 226 | + description="Base HTTP URL of PanDA server", |
| 227 | + serialization_alias="PANDA_URL", |
| 228 | + default=None, |
| 229 | + ) |
| 230 | + |
| 231 | + monitor_url: str | None = Field( |
| 232 | + description="URL of PanDA monitor", |
| 233 | + serialization_alias="PANDAMON_URL", |
| 234 | + default=None, |
| 235 | + ) |
| 236 | + |
| 237 | + cache_url: str | None = Field( |
| 238 | + description="Base URL of PanDA sandbox server", |
| 239 | + serialization_alias="PANDACACHE_URL", |
| 240 | + default=None, |
| 241 | + ) |
| 242 | + |
| 243 | + virtual_organization: str = Field( |
| 244 | + description="Virtual organization name used with Panda OIDC", |
| 245 | + serialization_alias="PANDA_AUTH_VO", |
| 246 | + default="Rubin", |
| 247 | + ) |
| 248 | + |
| 249 | + renew_after: int = Field( |
| 250 | + description="Minimum auth token lifetime in seconds before renewal attempts are made", |
| 251 | + default=302_400, |
| 252 | + exclude=True, |
| 253 | + ) |
| 254 | + |
| 255 | + # The presence of this environment variable should cause the panda client |
| 256 | + # to use specified token directly, skipping IO related to reading a token |
| 257 | + # file. |
| 258 | + id_token: str | None = Field( |
| 259 | + description="Current id token for PanDA authentication", |
| 260 | + serialization_alias="PANDA_AUTH_ID_TOKEN", |
| 261 | + default=None, |
| 262 | + ) |
| 263 | + |
| 264 | + refresh_token: str | None = Field( |
| 265 | + description="Current refresh token for PanDA token operations", |
| 266 | + default=None, |
| 267 | + exclude=True, |
| 268 | + ) |
| 269 | + |
| 270 | + token_expiry: datetime = Field( |
| 271 | + description="Time at which the current idtoken expires", |
| 272 | + default=datetime.now(tz=UTC), |
| 273 | + exclude=True, |
| 274 | + ) |
| 275 | + |
| 276 | + config_root: str = Field( |
| 277 | + description="Location of the PanDA .token file", |
| 278 | + serialization_alias="PANDA_CONFIG_ROOT", |
| 279 | + default="/var/run/secrets/panda", |
| 280 | + exclude=True, |
| 281 | + ) |
| 282 | + |
| 283 | + auth_type: str = Field( |
| 284 | + description="Panda Auth type", |
| 285 | + serialization_alias="PANDA_AUTH", |
| 286 | + default="oidc", |
| 287 | + ) |
| 288 | + |
| 289 | + behind_lb: bool = Field( |
| 290 | + description="Whether Panda is behind a loadbalancer", |
| 291 | + default=False, |
| 292 | + serialization_alias="PANDA_BEHIND_REAL_LB", |
| 293 | + ) |
| 294 | + |
| 295 | + verify_host: bool = Field( |
| 296 | + description="Whether to verify PanDA host TLS", |
| 297 | + default=True, |
| 298 | + serialization_alias="PANDA_VERIFY_HOST", |
| 299 | + ) |
| 300 | + |
| 301 | + use_native_httplib: bool = Field( |
| 302 | + description="Use native http lib instead of curl", |
| 303 | + default=True, |
| 304 | + serialization_alias="PANDA_USE_NATIVE_HTTPLIB", |
| 305 | + ) |
| 306 | + |
| 307 | + @computed_field(repr=False) # type: ignore[prop-decorator] |
| 308 | + @property |
| 309 | + def auth_config_url(self) -> str | None: |
| 310 | + """Location of auth config for PanDA VO.""" |
| 311 | + if self.tls_url is None: |
| 312 | + return None |
| 313 | + url_parts = urlparse(self.tls_url) |
| 314 | + return f"{url_parts.scheme}://{url_parts.hostname}:{url_parts.port}/auth/{self.virtual_organization}_auth_config.json" |
| 315 | + |
| 316 | + @model_validator(mode="after") |
| 317 | + def set_base_url_fields(self) -> Self: |
| 318 | + """Set all url fields when only a subset of urls are supplied.""" |
| 319 | + # NOTE: there is a danger of this validator creating a recursion error |
| 320 | + # if unbounded field-setters are used. Every update to the model |
| 321 | + # will itself trigger this validator because of the |
| 322 | + # `validate_assignment` directive on the model itself. |
| 323 | + |
| 324 | + # If no panda urls have been specified there is no need to continue |
| 325 | + # with model validation |
| 326 | + if self.url is None and self.tls_url is None: |
| 327 | + return self |
| 328 | + # It does not seem critical that these URLs actually use the scheme |
| 329 | + # with which they are nominally associated, only that both be set. |
| 330 | + elif self.url is None: |
| 331 | + self.url = self.tls_url |
| 332 | + elif self.tls_url is None: |
| 333 | + self.tls_url = self.url |
| 334 | + |
| 335 | + # default the cache url to the tls url |
| 336 | + if self.cache_url is None: |
| 337 | + self.cache_url = self.tls_url |
| 338 | + return self |
| 339 | + |
| 340 | + @field_validator("token_expiry", mode="after") |
| 341 | + @classmethod |
| 342 | + def set_datetime_utc(cls, value: datetime) -> datetime: |
| 343 | + """Applies UTC timezone to datetime value.""" |
| 344 | + # For tz-naive datetimes, treat the time as UTC in the first place |
| 345 | + # otherwise coerce the tz-aware datetime into UTC |
| 346 | + return value.replace(tzinfo=UTC) if value.tzinfo is None else value.astimezone(tz=UTC) |
| 347 | + |
| 348 | + @field_serializer("behind_lb", "verify_host", "use_native_httplib") |
| 349 | + def serialize_booleans(self, value: bool) -> str: # noqa: FBT001 |
| 350 | + """Serialize boolean fields as string values.""" |
| 351 | + return "on" if value else "off" |
| 352 | + |
| 353 | + |
213 | 354 | # TODO deprecate and remove "slurm"-specific logic from cm-service; it is |
214 | 355 | # unlikely that interfacing with slurm directly from k8s will be possible. |
215 | 356 | class SlurmConfiguration(BaseModel): |
@@ -383,6 +524,7 @@ class Configuration(BaseSettings): |
383 | 524 | htcondor: HTCondorConfiguration = HTCondorConfiguration() |
384 | 525 | logging: LoggingConfiguration = LoggingConfiguration() |
385 | 526 | slurm: SlurmConfiguration = SlurmConfiguration() |
| 527 | + panda: PandaConfiguration = PandaConfiguration() |
386 | 528 |
|
387 | 529 | # Root fields |
388 | 530 | script_handler: ScriptMethodEnum = Field( |
|
0 commit comments