.. dropdown:: Distribution Statement
| # # # This source code is subject to the license referenced at
| # # # https://github.com/NRLMMD-GEOIPS.
.. _ci-infrastructure:
CI and Installation Infrastructure
==================================
GeoIPS uses `Ansible `_ playbooks to manage installation of the
software, its plugins, and test datasets for CI purposes. The same playbooks run on bare-metal developer
machines and inside Docker containers, replacing the legacy hand-written shell scripts
(``base_install.sh``, ``full_install.sh``, ``site_install.sh``,
``check_system_requirements.sh``).
Ansible is a **contributor and CI tool** — end users install GeoIPS via pip or conda, not
through these playbooks.
.. contents:: On this page
:local:
:depth: 2
Overview
--------
The playbooks live in ``tests/ansible/`` alongside a standard role layout::
tests/ansible/
├── ansible.cfg # inventory path, roles path, no host-key checking
├── inventory/
│ └── local.yml # localhost, local connection, all variables live here
├── playbooks/
│ ├── install.yml # software installation
│ └── test_data.yml # test dataset downloads
└── roles/
├── system_deps/
├── python_env/
├── cartopy_shapefiles/
├── settings_repos/
├── source_repos/
├── registries/
└── test_data/
Only ``ansible.builtin`` modules are used — no external Ansible collections are required.
The ``Makefile`` provides convenience wrappers for every command shown below;
see `Make targets`_ for shortcuts you can use instead of typing Ansible commands
directly.
Prerequisites
-------------
Install ``ansible-core`` into the same Python environment used for GeoIPS:
.. code-block:: bash
pip install ansible-core
CI runners pre-install ``ansible-core``; this step is only needed for local
development. If you are running tests through the Docker-based CI (``make test-*``),
the container already includes it.
.. note::
If you installed GeoIPS with ``pip install geoips[test]``, Ansible-core is
already included and the standalone ``pip install ansible-core`` above can be
skipped.
Set the standard GeoIPS environment variables before running any playbook. The
``inventory/local.yml`` file reads these with ``lookup('env', ...)`` and falls back to
reasonable defaults:
.. code-block:: bash
export GEOIPS_PACKAGES_DIR=/path/to/packages
export GEOIPS_OUTDIRS=/path/to/output
export GEOIPS_TESTDATA_DIR=/path/to/testdata
export GEOIPS_REPO_URL=https://github.com/NRLMMD-GEOIPS
Tier model
----------
GeoIPS installation is organized into three additive tiers controlled by Ansible tags.
Always include all lower tiers when running a higher one.
.. list-table::
:header-rows: 1
:widths: 10 90
* - Tag
- What it installs
* - ``base``
- Core GeoIPS package and plugin registries. Sufficient for unit tests and
basic import checks.
* - ``full``
- Everything in ``base``, plus cartopy natural-earth shapefiles, settings repos
(``.vscode``, ``.github``, ``geoips_ci``), and ``doc``/``test`` pip extras.
Required for integration tests that produce output imagery.
* - ``site``
- Everything in ``full``, plus all open-source plugin packages (standard repos and the
ordered fortran chain), ``lint``/``debug`` pip extras, and optionally private repos.
The three-tier design maps directly to Docker image targets (see `Docker integration`_).
Tags are the single mechanism that controls depth — there are no separate playbooks for
each tier.
Running the install playbook
----------------------------
All commands below assume the repository root as the working directory.
Base install (core GeoIPS only)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. code-block:: bash
cd tests/ansible
ansible-playbook playbooks/install.yml --tags base
Full install (base + shapefiles + test extras)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. code-block:: bash
cd tests/ansible
ansible-playbook playbooks/install.yml --tags base,full
Site install (full + all plugin packages)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. code-block:: bash
cd tests/ansible
ansible-playbook playbooks/install.yml --tags base,full,site
Site install with private repos
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. code-block:: bash
cd tests/ansible
ansible-playbook playbooks/install.yml --tags base,full,site \
-e geoips_use_private_plugins=true
Installing specific extra plugin packages
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Pass a comma-separated list of repository names:
.. code-block:: bash
cd tests/ansible
ansible-playbook playbooks/install.yml --tags base \
-e extra_plugin_packages=my_plugin,other_plugin
Configuration variables
-----------------------
Override any variable with ``-e`` on the command line. Defaults live in
``tests/ansible/inventory/local.yml`` and are resolved (in order) from environment variables,
then hardcoded defaults. Copy the inventory file to create a custom setup
(e.g. ``inventory/my-setup.yml``) and run playbooks with ``-i inventory/my-setup.yml``.
.. list-table::
:header-rows: 1
:widths: 30 15 55
* - Variable
- Default
- Description
* - ``editable_pip_install``
- ``true``
- ``true`` uses ``pip install -e`` (editable — the source tree *is* the package).
``false`` builds from source into ``site-packages``, which produces a smaller Docker
image.
* - ``pip_extra_args``
- ``""``
- Additional arguments forwarded to every ``pip install`` call. Docker builds use
``--no-binary :all:`` to compile all dependencies from source inside the container.
* - ``geoips_use_private_plugins``
- ``false``
- Set to ``true`` to include proprietary plugin repos (``ryglickicane``, ``tc_mint``,
``lunarref``, ``true_color``).
* - ``extra_plugin_packages``
- ``""``
- Comma-separated list of additional plugin repository names to clone and install.
Corresponds to the ``EXTRA_PLUGINS`` Docker build argument.
* - ``geoips_modified_branch``
- ``""``
- After cloning each repo, attempt to check out this branch. Falls back silently to
the default branch if it does not exist. See `Branch fallback strategy`_
for details on how this is handled across repos.
* - ``geoips_packages_dir``
- ``/packages``
- Root directory where repos are cloned. Reads ``GEOIPS_PACKAGES_DIR`` env var.
* - ``geoips_testdata_dir``
- ``/geoips_testdata``
- Root directory for test datasets. Reads ``GEOIPS_TESTDATA_DIR`` env var.
Downloading test data
---------------------
Test data is managed by a **separate** playbook (``test_data.yml``) and is **never** baked
into Docker images. It uses the same tier tags as the install playbook.
The ``test_data`` role wraps ``geoips config install``, which is idempotent — datasets
already present on disk are skipped.
Base datasets only
^^^^^^^^^^^^^^^^^^
.. code-block:: bash
cd tests/ansible
ansible-playbook playbooks/test_data.yml --tags base
Base + full datasets
^^^^^^^^^^^^^^^^^^^^
.. code-block:: bash
cd tests/ansible
ansible-playbook playbooks/test_data.yml --tags base,full
All datasets (including site + private)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. code-block:: bash
cd tests/ansible
ansible-playbook playbooks/test_data.yml --tags base,full,site \
-e geoips_use_private_plugins=true
Override the download location
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. code-block:: bash
cd tests/ansible
ansible-playbook playbooks/test_data.yml --tags base \
-e geoips_testdata_dir=/my/custom/path
**Why the separation?** Test data is mounted into Docker containers at runtime via a host
volume — it is never built into an image. Keeping the download in its own playbook enforces
this boundary and allows CI to cache the test-data volume between runs independently of image
rebuilds.
The datasets installed at each tier are defined in
``tests/ansible/roles/test_data/defaults/main.yml``:
- **base**: ``test_data_amsr2``
- **full**: ``test_data_amsub``, ``test_data_arctic_weather_satellite``, ``test_data_atms``,
``test_data_cygnss``, ``test_data_fci``, ``test_data_gfs``, ``test_data_gpm``,
``test_data_modis``, ``test_data_multi_scan_times``, ``test_data_saphir``,
``test_data_sar``, ``test_data_scat``, ``test_data_seviri``, ``test_data_smap``,
``test_data_smos``, ``test_data_tpw``, ``test_data_viirs``
- **site**: ``template_test_data``, ``test_data_clavrx``, ``test_data_fusion``,
``test_data_geocolor``
- **private** (``geoips_use_private_plugins=true``): ``test_data_mint``
Role responsibilities
---------------------
The roles live in ``tests/ansible/roles/`` and each handles one concern.
``system_deps``
Verifies that required system commands (``git``, ``python3``, ``gcc``, ``gfortran``,
``g++``) are available. Does **not** install them — the host OS or Docker base image
is expected to provide them. This role is a fast-fail guard so dependency errors
surface immediately rather than mid-install.
``python_env``
Installs GeoIPS and its pip extras at the tier-appropriate level:
- ``base``: ``requirements.txt`` + ``pip install geoips``
- ``full``: adds ``geoips[doc,test]``
- ``site``: adds ``geoips[lint,debug]``
Controlled by ``editable_pip_install`` and ``pip_extra_args``.
``cartopy_shapefiles``
Clones the `natural-earth-vector `_
repository (depth 1) and symlinks the shapefiles into the directory structure that
cartopy expects. Cultural and physical shapefiles are each symlinked in two passes —
subdirectory contents first, then top-level files — replicating the logic of the old
``check_system_requirements.sh``. Only runs at ``full`` and ``site`` tiers.
``settings_repos``
Clones non-plugin reference repositories (``.vscode``, ``.github``, ``geoips_ci``) into
``$GEOIPS_PACKAGES_DIR``. These repos provide IDE configuration, GitHub workflows, and
CI scripts. Only runs at ``full`` and ``site`` tiers.
``source_repos``
Clones and ``pip install``\s plugin packages. Handles four groups in order:
1. **Standard plugins** (alphabetical): ``data_fusion``, ``geoips_clavrx``,
``geoips_plugin_example``, ``recenter_tc``, ``template_basic_plugin``
2. **Fortran chain** (order critical): ``fortran_utils`` → ``rayleigh`` → ``ancildat``
→ ``synth_green`` → ``geocolor``
3. **Private plugins** (when enabled): ``ryglickicane``, ``tc_mint``
4. **Private fortran repos** (when enabled, order critical): ``lunarref``, ``true_color``
5. **Extra plugins**: any repos passed via ``extra_plugin_packages``
The fortran ordering constraint exists because each package depends on compiled
artifacts produced by the previous one. Extra plugins are available at all tiers
(``base``, ``full``, ``site``) to support CI matrix builds of individual plugins.
``registries``
Runs ``geoips config create-registries`` as the final install step. This command scans
all installed GeoIPS plugin packages and writes the plugin registry files that GeoIPS
reads at startup. The task uses ``changed_when: false`` because the command is
idempotent and does not report changes in its exit code.
``test_data``
Downloads test datasets via ``geoips config install``. Used exclusively by
``test_data.yml`` — it is not part of the install playbook.
Make targets
^^^^^^^^^^^^
The Makefile provides convenience wrappers:
.. code-block:: bash
# Bare-metal install
make ansible-base
make ansible-full
make ansible-site
# Bare-metal test data download
make ansible-testdata-base
make ansible-testdata-full
make ansible-testdata-site
# Docker test data download (downloads to host via mounted volume)
make testdata-full TESTDATA=/path/on/host/geoips-testdata
Docker integration
------------------
The Dockerfile uses a multi-stage build that maps directly to the Ansible tiers:
.. code-block:: text
deps – pip install -r requirements.txt (cached layer)
geoips-base – ansible-playbook ... --tags base
geoips-full – ansible-playbook ... --tags base,full
geoips-site – ansible-playbook ... --tags base,full,site
production – copy site-packages only, no source, no ansible, no git
**Why ``editable_pip_install=false`` in Docker?** The editable install mode makes the source
directory itself the package. In a container built for deployment this is undesirable —
the source tree may not be present at runtime. Building with ``editable_pip_install=false``
installs the package into ``site-packages`` like a normal wheel, and the ``production``
stage then copies only that directory, producing a minimal image.
The ``dev`` and ``dev-quick`` Docker targets use ``editable_pip_install=true`` to enable
live source editing through the workspace bind-mount.
**Why ``--no-binary :all:``?** Pre-built wheels are compiled for a generic architecture.
Compiling from source inside the container produces binaries optimized for the target CPU
and avoids wheel-cache bloat. The ``deps`` stage also applies this flag, and because
``requirements.txt`` changes infrequently, Docker caches that layer across most rebuilds.
**Test data volume pattern:**
.. code-block:: bash
# Download data to the host via the ansible playbook inside the container
make testdata-full TESTDATA=/path/on/host/geoips-testdata
# Run tests with that data mounted at the expected path
docker run --rm -v /path/on/host/geoips-testdata:/geoips_testdata geoips:full \
pytest -m "base and integration"
Test data is always a runtime mount, never a build-time layer.
Branch fallback strategy
------------------------
Every repo clone — in both ``settings_repos`` and ``source_repos`` — follows a two-step
pattern:
1. Attempt to clone (or checkout) the branch named by ``geoips_modified_branch``. If that
variable is empty, this step is skipped entirely.
2. If the branch clone failed (branch does not exist in the remote), fall back to the
remote's default branch.
This lets CI pipelines pass a single branch name across all repos without needing to know
which repos actually carry that branch. Repos that do not have the branch simply land on
their default branch without error.
The ``source_repos`` role implements this per-repo in
``roles/source_repos/tasks/clone_and_install.yml``. The ``settings_repos`` role uses a
batch approach: clone all repos in one loop with ``failed_when: false``, then re-clone only
the ones that failed.
Idempotency
-----------
The playbooks are designed to be re-run safely at any point:
- ``ansible.builtin.git`` uses ``update: false`` on the fallback clone so it does not
overwrite uncommitted changes.
- ``pip install`` with ``state: present`` is a no-op when the package is already installed
at the correct version.
- The ``test_data`` role uses ``creates: "{% raw %}{{ geoips_testdata_dir }}/{{ item }}{% endraw %}"`` so
Ansible skips the ``geoips config install`` call entirely when the dataset directory
already exists.
- ``geoips config create-registries`` is idempotent by design.
If a run fails partway through, fix the underlying issue and re-run the same command.
Completed tasks will execute as fast no-ops. Multiple runs will not hurt anything.
Adding new repositories
-----------------------
Repositories and which install group they belong to are managed by
``tests/ansible/inventory/local.yml``. To add a new repo or move a repo to a
different install group, edit ``tests/ansible/inventory/local.yml`` as follows:
**Standard plugin repos** (no ordering constraint):
Add the repo name to ``plugin_repos`` in ``tests/ansible/inventory/local.yml``.
**Fortran plugin repos** (ordering constraint applies):
Add the repo name to ``fortran_repos_ordered`` in ``inventory/local.yml`` in the
correct position in the dependency chain. The comment in that file documents the
required order.
**Private repos**:
Add to ``private_plugin_repos`` or ``private_fortran_repos`` (for the fortran chain)
in ``inventory/local.yml``.
**Test datasets**:
Add the dataset name to the appropriate tier list in
``tests/ansible/roles/test_data/defaults/main.yml``. The name must be recognized by
``geoips config install``.
Troubleshooting
---------------
Increase verbosity
^^^^^^^^^^^^^^^^^^
Add ``-vvv`` to any ``ansible-playbook`` command for detailed task output:
.. code-block:: bash
ansible-playbook playbooks/install.yml --tags base -vvv
Dry run
^^^^^^^
Use ``--check`` to see what Ansible *would* do without making any changes:
.. code-block:: bash
ansible-playbook playbooks/install.yml --tags base --check
Re-running after a failure
^^^^^^^^^^^^^^^^^^^^^^^^^^
Ansible is idempotent. Fix the underlying issue and re-run the same command. Completed
tasks will be fast no-ops.