mirror of
https://github.com/d9pouces/id_configurator.git
synced 2026-11-09 01:47:21 +01:00
refactor: remove unused NPM_TO_COPY variable from build.py
Signed-off-by: d9pouces <github@19pouces.net>
This commit is contained in:
@@ -213,8 +213,18 @@ class CertificatesModule(Module):
|
||||
logger.info("rebuilt %s from %d certificate(s)", _CERT_BUNDLE, len(entries))
|
||||
|
||||
if etccertsdir_relative == _ETCCERTSDIR:
|
||||
cert_file = context.dst_path(_OPENSSL_DEFAULT_CERT_FILE)
|
||||
cert_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
# Not context.dst_path(_OPENSSL_DEFAULT_CERT_FILE) directly: on a
|
||||
# host that already has this symlink (e.g. re-applying to a live
|
||||
# `--dst /`), resolving the leaf itself would follow it straight
|
||||
# to the bundle we just wrote, and the unlink+symlink_to below
|
||||
# would then target the bundle's own path instead of this one -
|
||||
# replacing the bundle with a symlink to itself. Resolving only
|
||||
# the containing directory keeps the leaf name literal.
|
||||
cert_file_dir = context.dst_path(
|
||||
posixpath.dirname(_OPENSSL_DEFAULT_CERT_FILE)
|
||||
)
|
||||
cert_file_dir.mkdir(parents=True, exist_ok=True)
|
||||
cert_file = cert_file_dir / posixpath.basename(_OPENSSL_DEFAULT_CERT_FILE)
|
||||
cert_file.unlink(missing_ok=True)
|
||||
cert_file.symlink_to(f"/{etccertsdir_relative}/{_CERT_BUNDLE}")
|
||||
|
||||
|
||||
@@ -0,0 +1,635 @@
|
||||
#!/usr/bin/python
|
||||
"""Ansible module wrapping id-configurator.
|
||||
|
||||
Applies an id-configurator configuration directly to this host's live root
|
||||
filesystem, exactly like the `id_configurator` role does, but as a native
|
||||
Ansible module: it imports id-configurator's own Python API in-process
|
||||
(instead of shelling out to the CLI through a temporary file), which also
|
||||
lets it honor Ansible's check mode - reporting which modules would be
|
||||
applied without writing anything.
|
||||
|
||||
This module is a thin, additive wrapper: it does not change anything about
|
||||
id-configurator's own behavior (the `id_configurator` Python package,
|
||||
including its module registry and every individual module, is used exactly
|
||||
as-is - the same `Configuration`, `Context` and module registry the CLI
|
||||
itself uses).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
module: id_configurator
|
||||
short_description: Apply an id-configurator configuration to this host.
|
||||
description:
|
||||
- Assembles the given options into an id-configurator configuration and
|
||||
applies it directly to this host's live root filesystem, by importing
|
||||
id-configurator's own Python API in-process (no CLI subprocess, no
|
||||
temporary file).
|
||||
- The option names and structure mirror id_configurator.config.Configuration
|
||||
and id_configurator.config.ConfigurationsBlock one for one. The
|
||||
`id_configurator` Python package must already be installed on the
|
||||
target host, using the same Python interpreter Ansible uses there.
|
||||
- Supports check mode - reports which modules would be applied without
|
||||
writing anything.
|
||||
author:
|
||||
- InterDiode
|
||||
options:
|
||||
uuid:
|
||||
type: str
|
||||
required: true
|
||||
description: Configuration.uuid - a UUID identifying this configuration.
|
||||
organisation:
|
||||
type: str
|
||||
required: true
|
||||
description: Configuration.organisation.
|
||||
description:
|
||||
type: str
|
||||
default: ""
|
||||
description: Configuration.description.
|
||||
certificates:
|
||||
type: list
|
||||
elements: dict
|
||||
default: []
|
||||
description: ConfigurationsBlock.certificates - root certificates to install.
|
||||
suboptions:
|
||||
name:
|
||||
type: str
|
||||
required: true
|
||||
description: A short, unique name for this entry.
|
||||
content:
|
||||
type: str
|
||||
required: true
|
||||
description: PEM-encoded certificate content.
|
||||
apt:
|
||||
type: dict
|
||||
default: {}
|
||||
description: ConfigurationsBlock.apt.
|
||||
suboptions:
|
||||
sources:
|
||||
type: list
|
||||
elements: dict
|
||||
default: []
|
||||
description: DEB822 APT sources (AptSource).
|
||||
suboptions:
|
||||
name:
|
||||
type: str
|
||||
required: true
|
||||
description: A short, unique name for this source.
|
||||
types:
|
||||
type: list
|
||||
elements: str
|
||||
default: [deb]
|
||||
description: DEB822 "Types" field.
|
||||
uris:
|
||||
type: list
|
||||
elements: str
|
||||
required: true
|
||||
description: DEB822 "URIs" field.
|
||||
suites:
|
||||
type: list
|
||||
elements: str
|
||||
required: true
|
||||
description: DEB822 "Suites" field.
|
||||
components:
|
||||
type: list
|
||||
elements: str
|
||||
default: []
|
||||
description: DEB822 "Components" field.
|
||||
architectures:
|
||||
type: list
|
||||
elements: str
|
||||
default: []
|
||||
description: DEB822 "Architectures" field.
|
||||
languages:
|
||||
type: list
|
||||
elements: str
|
||||
default: []
|
||||
description: DEB822 "Languages" field.
|
||||
targets:
|
||||
type: list
|
||||
elements: str
|
||||
default: []
|
||||
description: DEB822 "Targets" field.
|
||||
enabled:
|
||||
type: bool
|
||||
default: true
|
||||
description: DEB822 "Enabled" field.
|
||||
trusted:
|
||||
type: bool
|
||||
description: DEB822 "Trusted" field.
|
||||
signed_by:
|
||||
type: str
|
||||
description: DEB822 "Signed-By" field.
|
||||
pdiffs:
|
||||
type: bool
|
||||
description: DEB822 "PDiffs" field.
|
||||
by_hash:
|
||||
type: bool
|
||||
description: DEB822 "By-Hash" field.
|
||||
allow_insecure:
|
||||
type: bool
|
||||
description: DEB822 "Allow-Insecure" field.
|
||||
allow_weak:
|
||||
type: bool
|
||||
description: DEB822 "Allow-Weak" field.
|
||||
allow_downgrade_to_insecure:
|
||||
type: bool
|
||||
description: DEB822 "Allow-Downgrade-To-Insecure" field.
|
||||
check_valid_until:
|
||||
type: bool
|
||||
description: DEB822 "Check-Valid-Until" field.
|
||||
valid_until_min:
|
||||
type: str
|
||||
description: DEB822 "Valid-Until-Min" field.
|
||||
valid_until_max:
|
||||
type: str
|
||||
description: DEB822 "Valid-Until-Max" field.
|
||||
check_date:
|
||||
type: bool
|
||||
description: DEB822 "Check-Date" field.
|
||||
date_max_future:
|
||||
type: str
|
||||
description: DEB822 "Date-Max-Future" field.
|
||||
description:
|
||||
type: str
|
||||
description: DEB822 "Description" field.
|
||||
keys:
|
||||
type: list
|
||||
elements: dict
|
||||
default: []
|
||||
description: GPG keys trusted for the sources above (NamedContent).
|
||||
suboptions:
|
||||
name:
|
||||
type: str
|
||||
required: true
|
||||
description: A short, unique name for this entry.
|
||||
content:
|
||||
type: str
|
||||
required: true
|
||||
description: PEM-encoded/ASCII-armored key content.
|
||||
apk:
|
||||
type: dict
|
||||
default: {}
|
||||
description: ConfigurationsBlock.apk.
|
||||
suboptions:
|
||||
repositories:
|
||||
type: list
|
||||
elements: dict
|
||||
default: []
|
||||
description: Alpine APK repositories (ApkRepository).
|
||||
suboptions:
|
||||
url:
|
||||
type: str
|
||||
required: true
|
||||
description: Repository URL, e.g. ending in /main or /community.
|
||||
key:
|
||||
type: str
|
||||
description: The signing key's name, if any (see NamedContent).
|
||||
yum:
|
||||
type: dict
|
||||
default: {}
|
||||
description: ConfigurationsBlock.yum.
|
||||
suboptions:
|
||||
repositories:
|
||||
type: list
|
||||
elements: dict
|
||||
default: []
|
||||
description: YUM/DNF repositories (YumRepository).
|
||||
suboptions:
|
||||
name:
|
||||
type: str
|
||||
required: true
|
||||
description: A short, unique name for this repository.
|
||||
index_url:
|
||||
type: str
|
||||
required: true
|
||||
description: The repository's baseurl.
|
||||
gpg_url:
|
||||
type: str
|
||||
description: The repository's gpgkey URL.
|
||||
keys:
|
||||
type: list
|
||||
elements: dict
|
||||
default: []
|
||||
description: GPG keys imported for the repositories above (NamedContent).
|
||||
suboptions:
|
||||
name:
|
||||
type: str
|
||||
required: true
|
||||
description: A short, unique name for this entry.
|
||||
content:
|
||||
type: str
|
||||
required: true
|
||||
description: PEM-encoded/ASCII-armored key content.
|
||||
python:
|
||||
type: dict
|
||||
default: {}
|
||||
description: ConfigurationsBlock.python.
|
||||
suboptions:
|
||||
repositories:
|
||||
type: list
|
||||
elements: dict
|
||||
default: []
|
||||
description: Python package indexes (PythonRepository).
|
||||
suboptions:
|
||||
name:
|
||||
type: str
|
||||
required: true
|
||||
description: A short, unique name for this index.
|
||||
index_url:
|
||||
type: str
|
||||
required: true
|
||||
description: The package index's URL.
|
||||
java:
|
||||
type: dict
|
||||
default: {}
|
||||
description: ConfigurationsBlock.java.
|
||||
suboptions:
|
||||
repositories:
|
||||
type: list
|
||||
elements: dict
|
||||
default: []
|
||||
description: Maven-compatible repositories (JavaRepository).
|
||||
suboptions:
|
||||
name:
|
||||
type: str
|
||||
required: true
|
||||
description: A short, unique name for this repository.
|
||||
url:
|
||||
type: str
|
||||
required: true
|
||||
description: The repository's URL.
|
||||
php:
|
||||
type: dict
|
||||
default: {}
|
||||
description: ConfigurationsBlock.php.
|
||||
suboptions:
|
||||
repositories:
|
||||
type: list
|
||||
elements: dict
|
||||
default: []
|
||||
description: Composer repositories (PhpRepository).
|
||||
suboptions:
|
||||
name:
|
||||
type: str
|
||||
required: true
|
||||
description: A short, unique name for this repository.
|
||||
url:
|
||||
type: str
|
||||
required: true
|
||||
description: The repository's URL.
|
||||
npm:
|
||||
type: dict
|
||||
default: {}
|
||||
description: ConfigurationsBlock.npm.
|
||||
suboptions:
|
||||
repositories:
|
||||
type: list
|
||||
elements: dict
|
||||
default: []
|
||||
description: npm registries (NpmRepository).
|
||||
suboptions:
|
||||
name:
|
||||
type: str
|
||||
required: true
|
||||
description: A short, unique name for this repository.
|
||||
url:
|
||||
type: str
|
||||
required: true
|
||||
description: The repository's URL.
|
||||
rust:
|
||||
type: dict
|
||||
default: {}
|
||||
description: ConfigurationsBlock.rust.
|
||||
suboptions:
|
||||
repositories:
|
||||
type: list
|
||||
elements: dict
|
||||
default: []
|
||||
description: Cargo registries (RustRepository).
|
||||
suboptions:
|
||||
name:
|
||||
type: str
|
||||
required: true
|
||||
description: A short, unique name for this repository.
|
||||
url:
|
||||
type: str
|
||||
required: true
|
||||
description: The repository's URL.
|
||||
go:
|
||||
type: dict
|
||||
default: {}
|
||||
description: ConfigurationsBlock.go.
|
||||
suboptions:
|
||||
repositories:
|
||||
type: list
|
||||
elements: dict
|
||||
default: []
|
||||
description: Go module proxies (GoRepository).
|
||||
suboptions:
|
||||
name:
|
||||
type: str
|
||||
required: true
|
||||
description: A short, unique name for this repository.
|
||||
url:
|
||||
type: str
|
||||
required: true
|
||||
description: The repository's URL.
|
||||
ruby:
|
||||
type: dict
|
||||
default: {}
|
||||
description: ConfigurationsBlock.ruby.
|
||||
suboptions:
|
||||
repositories:
|
||||
type: list
|
||||
elements: dict
|
||||
default: []
|
||||
description: RubyGems sources (RubyRepository).
|
||||
suboptions:
|
||||
name:
|
||||
type: str
|
||||
required: true
|
||||
description: A short, unique name for this repository.
|
||||
url:
|
||||
type: str
|
||||
required: true
|
||||
description: The repository's URL.
|
||||
nuget:
|
||||
type: dict
|
||||
default: {}
|
||||
description: ConfigurationsBlock.nuget.
|
||||
suboptions:
|
||||
repositories:
|
||||
type: list
|
||||
elements: dict
|
||||
default: []
|
||||
description: NuGet package sources (NuGetRepository).
|
||||
suboptions:
|
||||
name:
|
||||
type: str
|
||||
required: true
|
||||
description: A short, unique name for this repository.
|
||||
url:
|
||||
type: str
|
||||
required: true
|
||||
description: The repository's URL.
|
||||
"""
|
||||
|
||||
EXAMPLES = r"""
|
||||
- name: Apply an id-configurator configuration to this host
|
||||
id_configurator:
|
||||
uuid: 12345678-1234-5678-1234-567812345678
|
||||
organisation: InterDiode
|
||||
certificates:
|
||||
- name: Company Root CA
|
||||
content: "{{ lookup('file', 'files/company-root-ca.pem') }}"
|
||||
apt:
|
||||
sources:
|
||||
- name: internal
|
||||
uris: ["https://apt.internal.example/"]
|
||||
suites: ["stable"]
|
||||
"""
|
||||
|
||||
RETURN = r"""
|
||||
applied_modules:
|
||||
description: Names of the id-configurator modules that were (or, in check
|
||||
mode, would be) applied, in registration order.
|
||||
returned: always
|
||||
type: list
|
||||
elements: str
|
||||
sample: [certificates, apt]
|
||||
"""
|
||||
|
||||
# Every key here must match one of ConfigurationsBlock's own field names
|
||||
# (id_configurator/config.py) - see the module docstring above.
|
||||
_CONFIGURATIONS_BLOCK_FIELDS = (
|
||||
"certificates",
|
||||
"apt",
|
||||
"apk",
|
||||
"yum",
|
||||
"python",
|
||||
"java",
|
||||
"php",
|
||||
"npm",
|
||||
"rust",
|
||||
"go",
|
||||
"ruby",
|
||||
"nuget",
|
||||
)
|
||||
|
||||
_NAMED_CONTENT_OPTIONS = {
|
||||
"name": {"type": "str", "required": True},
|
||||
"content": {"type": "str", "required": True},
|
||||
}
|
||||
|
||||
_NAME_URL_OPTIONS = {
|
||||
"name": {"type": "str", "required": True},
|
||||
"url": {"type": "str", "required": True},
|
||||
}
|
||||
|
||||
|
||||
def _name_url_repository_config() -> dict:
|
||||
"""Argument spec for a `{repositories: [{name, url}]}` config block,
|
||||
shared by java/php/npm/rust/go/ruby/nuget."""
|
||||
return {
|
||||
"type": "dict",
|
||||
"default": {},
|
||||
"options": {
|
||||
"repositories": {
|
||||
"type": "list",
|
||||
"elements": "dict",
|
||||
"default": [],
|
||||
"options": dict(_NAME_URL_OPTIONS),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
ARGUMENT_SPEC = {
|
||||
"uuid": {"type": "str", "required": True},
|
||||
"organisation": {"type": "str", "required": True},
|
||||
"description": {"type": "str", "default": ""},
|
||||
"certificates": {
|
||||
"type": "list",
|
||||
"elements": "dict",
|
||||
"default": [],
|
||||
"options": dict(_NAMED_CONTENT_OPTIONS),
|
||||
},
|
||||
"apt": {
|
||||
"type": "dict",
|
||||
"default": {},
|
||||
"options": {
|
||||
"sources": {
|
||||
"type": "list",
|
||||
"elements": "dict",
|
||||
"default": [],
|
||||
"options": {
|
||||
"name": {"type": "str", "required": True},
|
||||
"types": {"type": "list", "elements": "str", "default": ["deb"]},
|
||||
"uris": {"type": "list", "elements": "str", "required": True},
|
||||
"suites": {"type": "list", "elements": "str", "required": True},
|
||||
"components": {"type": "list", "elements": "str", "default": []},
|
||||
"architectures": {
|
||||
"type": "list",
|
||||
"elements": "str",
|
||||
"default": [],
|
||||
},
|
||||
"languages": {"type": "list", "elements": "str", "default": []},
|
||||
"targets": {"type": "list", "elements": "str", "default": []},
|
||||
"enabled": {"type": "bool", "default": True},
|
||||
"trusted": {"type": "bool"},
|
||||
"signed_by": {"type": "str"},
|
||||
"pdiffs": {"type": "bool"},
|
||||
"by_hash": {"type": "bool"},
|
||||
"allow_insecure": {"type": "bool"},
|
||||
"allow_weak": {"type": "bool"},
|
||||
"allow_downgrade_to_insecure": {"type": "bool"},
|
||||
"check_valid_until": {"type": "bool"},
|
||||
"valid_until_min": {"type": "str"},
|
||||
"valid_until_max": {"type": "str"},
|
||||
"check_date": {"type": "bool"},
|
||||
"date_max_future": {"type": "str"},
|
||||
"description": {"type": "str"},
|
||||
},
|
||||
},
|
||||
"keys": {
|
||||
"type": "list",
|
||||
"elements": "dict",
|
||||
"default": [],
|
||||
"options": dict(_NAMED_CONTENT_OPTIONS),
|
||||
},
|
||||
},
|
||||
},
|
||||
"apk": {
|
||||
"type": "dict",
|
||||
"default": {},
|
||||
"options": {
|
||||
"repositories": {
|
||||
"type": "list",
|
||||
"elements": "dict",
|
||||
"default": [],
|
||||
"options": {
|
||||
"url": {"type": "str", "required": True},
|
||||
"key": {"type": "str"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"yum": {
|
||||
"type": "dict",
|
||||
"default": {},
|
||||
"options": {
|
||||
"repositories": {
|
||||
"type": "list",
|
||||
"elements": "dict",
|
||||
"default": [],
|
||||
"options": {
|
||||
"name": {"type": "str", "required": True},
|
||||
"index_url": {"type": "str", "required": True},
|
||||
"gpg_url": {"type": "str"},
|
||||
},
|
||||
},
|
||||
"keys": {
|
||||
"type": "list",
|
||||
"elements": "dict",
|
||||
"default": [],
|
||||
"options": dict(_NAMED_CONTENT_OPTIONS),
|
||||
},
|
||||
},
|
||||
},
|
||||
"python": {
|
||||
"type": "dict",
|
||||
"default": {},
|
||||
"options": {
|
||||
"repositories": {
|
||||
"type": "list",
|
||||
"elements": "dict",
|
||||
"default": [],
|
||||
"options": {
|
||||
"name": {"type": "str", "required": True},
|
||||
"index_url": {"type": "str", "required": True},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"java": _name_url_repository_config(),
|
||||
"php": _name_url_repository_config(),
|
||||
"npm": _name_url_repository_config(),
|
||||
"rust": _name_url_repository_config(),
|
||||
"go": _name_url_repository_config(),
|
||||
"ruby": _name_url_repository_config(),
|
||||
"nuget": _name_url_repository_config(),
|
||||
}
|
||||
|
||||
|
||||
try:
|
||||
import id_configurator.modules # noqa: F401 (registers the built-in modules)
|
||||
from id_configurator.config import Configuration
|
||||
from id_configurator.context import Context
|
||||
from id_configurator.modules.base import all_modules
|
||||
|
||||
HAS_ID_CONFIGURATOR = True
|
||||
ID_CONFIGURATOR_IMPORT_ERROR = None
|
||||
except ImportError as exc:
|
||||
HAS_ID_CONFIGURATOR = False
|
||||
ID_CONFIGURATOR_IMPORT_ERROR = exc
|
||||
|
||||
|
||||
def _build_configuration_data(params: dict) -> dict:
|
||||
return {
|
||||
"uuid": params["uuid"],
|
||||
"organisation": params["organisation"],
|
||||
"description": params["description"],
|
||||
"configurations": {
|
||||
field: params[field] for field in _CONFIGURATIONS_BLOCK_FIELDS
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _apply_configuration(configuration, check_mode: bool) -> list[str]:
|
||||
"""Apply `configuration` to this host's live root filesystem, returning
|
||||
the names of every module that was (or, in check mode, would be)
|
||||
applied. Uses id-configurator's own module registry and `Context`
|
||||
unchanged - this is a thin caller, not a reimplementation."""
|
||||
root = Path("/")
|
||||
context = Context(config=configuration, dst=root, src=root)
|
||||
applied_modules = []
|
||||
for module_cls in all_modules():
|
||||
config_module = module_cls()
|
||||
if config_module.is_relevant(context):
|
||||
applied_modules.append(config_module.name)
|
||||
if not check_mode:
|
||||
config_module.apply(context)
|
||||
return applied_modules
|
||||
|
||||
|
||||
def main() -> None:
|
||||
module = AnsibleModule(argument_spec=ARGUMENT_SPEC, supports_check_mode=True)
|
||||
|
||||
if not HAS_ID_CONFIGURATOR:
|
||||
module.fail_json(
|
||||
msg=(
|
||||
"id_configurator is not importable with this host's Python "
|
||||
f"interpreter: {ID_CONFIGURATOR_IMPORT_ERROR}"
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
configuration = Configuration.model_validate(
|
||||
_build_configuration_data(module.params)
|
||||
)
|
||||
except Exception as exc: # pydantic.ValidationError, kept generic on purpose
|
||||
module.fail_json(msg=f"invalid id-configurator configuration: {exc}")
|
||||
return
|
||||
|
||||
applied_modules = _apply_configuration(configuration, module.check_mode)
|
||||
module.exit_json(changed=bool(applied_modules), applied_modules=applied_modules)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -237,3 +237,37 @@ def test_apply_writes_bundle_to_rhel_style_symlinked_etccertsdir(tmp_path) -> No
|
||||
# /usr/lib/ssl/cert.pem is Debian/Ubuntu-specific: not OpenSSL's default
|
||||
# CAfile on this (RHEL-style) system, so it must not be created.
|
||||
assert not (dst / "usr/lib/ssl/cert.pem").exists()
|
||||
|
||||
|
||||
def test_apply_is_idempotent_when_src_and_dst_are_the_same_live_root(
|
||||
tmp_path,
|
||||
) -> None:
|
||||
# Regression test: applying directly to a live root (--src / --dst /,
|
||||
# as the Ansible role does) where /usr/lib/ssl/cert.pem already exists
|
||||
# (as it would on any host with ca-certificates already installed) must
|
||||
# not resolve that pre-existing symlink away when recreating it - doing
|
||||
# so would unlink/replace the bundle it points to instead, corrupting it
|
||||
# into a symlink to itself and breaking every subsequent run.
|
||||
root = tmp_path / "root"
|
||||
etc_ssl_certs = root / "etc/ssl/certs"
|
||||
etc_ssl_certs.mkdir(parents=True)
|
||||
(etc_ssl_certs / "ca-certificates.crt").write_bytes(b"preexisting-bundle-bytes")
|
||||
usr_lib_ssl = root / "usr/lib/ssl"
|
||||
usr_lib_ssl.mkdir(parents=True)
|
||||
(usr_lib_ssl / "cert.pem").symlink_to("/etc/ssl/certs/ca-certificates.crt")
|
||||
|
||||
context = Context(config=make_config(True), dst=root, src=root)
|
||||
|
||||
for _ in range(2):
|
||||
CertificatesModule().apply(context)
|
||||
|
||||
bundle = etc_ssl_certs / "ca-certificates.crt"
|
||||
assert not bundle.is_symlink()
|
||||
assert bundle.read_bytes() == b"fake-cert-bytes\n"
|
||||
|
||||
openssl_default = usr_lib_ssl / "cert.pem"
|
||||
assert openssl_default.is_symlink()
|
||||
assert (
|
||||
openssl_default.readlink().as_posix()
|
||||
== "/etc/ssl/certs/ca-certificates.crt"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user