Showing preview only (471K chars total). Download the full file or copy to clipboard to get everything.
Repository: coreos/zincati
Branch: main
Commit: b15c343cec90
Files: 101
Total size: 442.2 KB
Directory structure:
gitextract_ibeb0zdk/
├── .cci.jenkinsfile
├── .gemini/
│ └── config.yaml
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug.md
│ │ ├── feature.md
│ │ └── release-checklist.md
│ ├── dependabot.yml
│ └── workflows/
│ ├── containers.yml
│ └── rust.yml
├── .gitignore
├── .packit.yaml
├── COPYRIGHT
├── Cargo.toml
├── DCO
├── LICENSE
├── Makefile
├── README.md
├── contrib/
│ └── monitoring-mixins/
│ ├── README.md
│ ├── dashboards/
│ │ └── dashboards.libsonnet
│ ├── jsonnetfile.json
│ └── mixin.libsonnet
├── dist/
│ ├── bin/
│ │ └── zincati-update-now
│ ├── config.d/
│ │ ├── 10-agent.toml
│ │ ├── 10-auto-updates.toml
│ │ ├── 10-identity.toml
│ │ ├── 30-updates-strategy.toml
│ │ └── 50-fedora-coreos-cincinnati.toml
│ ├── dbus-1/
│ │ └── system.d/
│ │ └── org.coreos.zincati.conf
│ ├── polkit-1/
│ │ ├── actions/
│ │ │ └── org.coreos.zincati.deadend.policy
│ │ └── rules.d/
│ │ └── zincati.rules
│ ├── systemd/
│ │ └── system/
│ │ └── zincati.service
│ ├── sysusers.d/
│ │ └── 50-zincati.conf
│ └── tmpfiles.d/
│ └── zincati.conf
├── docs/
│ ├── _config.yml
│ ├── _sass/
│ │ └── color_schemes/
│ │ └── coreos.scss
│ ├── contributing.md
│ ├── development/
│ │ ├── agent-actor-system.md
│ │ ├── cincinnati/
│ │ │ ├── protocol.md
│ │ │ └── response.json
│ │ ├── fleetlock/
│ │ │ └── protocol.md
│ │ ├── os-metadata.md
│ │ ├── quickstart.md
│ │ ├── testing.md
│ │ └── update-strategy-periodic.md
│ ├── development.md
│ ├── images/
│ │ ├── zincati-actors.dot
│ │ ├── zincati-fleetlock.msc
│ │ └── zincati-fsm.dot
│ ├── index.md
│ ├── usage/
│ │ ├── agent-identity.md
│ │ ├── auto-updates.md
│ │ ├── configuration.md
│ │ ├── logging.md
│ │ ├── metrics.md
│ │ └── updates-strategy.md
│ └── usage.md
├── src/
│ ├── cincinnati/
│ │ ├── client.rs
│ │ ├── mock_tests.rs
│ │ └── mod.rs
│ ├── cli/
│ │ ├── agent.rs
│ │ ├── deadend.rs
│ │ ├── ex.rs
│ │ └── mod.rs
│ ├── config/
│ │ ├── fragments.rs
│ │ ├── inputs.rs
│ │ └── mod.rs
│ ├── dbus/
│ │ ├── experimental.rs
│ │ └── mod.rs
│ ├── fleet_lock/
│ │ ├── mock_tests.rs
│ │ └── mod.rs
│ ├── identity/
│ │ ├── mod.rs
│ │ └── platform.rs
│ ├── main.rs
│ ├── metrics/
│ │ └── mod.rs
│ ├── rpm_ostree/
│ │ ├── actor.rs
│ │ ├── cli_deploy.rs
│ │ ├── cli_finalize.rs
│ │ ├── cli_status.rs
│ │ ├── mock_tests.rs
│ │ └── mod.rs
│ ├── strategy/
│ │ ├── fleet_lock.rs
│ │ ├── immediate.rs
│ │ ├── mod.rs
│ │ └── periodic.rs
│ ├── update_agent/
│ │ ├── actor.rs
│ │ └── mod.rs
│ ├── utils.rs
│ └── weekly/
│ ├── mod.rs
│ └── utils.rs
└── tests/
├── fixtures/
│ ├── 00-config-sample.toml
│ ├── 20-periodic-sample.toml
│ ├── 30-periodic-sample-non-utc.toml
│ ├── 31-periodic-sample-non-utc.toml
│ ├── rpm-ostree-staged.json
│ ├── rpm-ostree-status-annotation.json
│ └── rpm-ostree-status.json
└── kola/
├── common/
│ └── libtest.sh
├── dbus/
│ └── test-experimental.sh
├── misc/
│ └── test-status.sh
└── server/
├── config.fcc
├── test-deadend-release.sh
└── test-stream.sh
================================================
FILE CONTENTS
================================================
================================================
FILE: .cci.jenkinsfile
================================================
// Documentation: https://github.com/coreos/coreos-ci/blob/main/README-upstream-ci.md
properties([
// abort previous runs when a PR is updated to save resources
disableConcurrentBuilds(abortPrevious: true)
])
buildPod {
checkout scm
stage("Build") {
shwrap("make build RELEASE=1")
}
stage("Unit Test") {
shwrap("make check RELEASE=1")
}
stage("Install") {
shwrap("make install RELEASE=1 DESTDIR=install")
stash name: 'build', includes: 'install/**'
}
}
cosaPod(buildroot: true) {
checkout scm
unstash name: 'build'
cosaBuild(overlays: ["install"])
}
================================================
FILE: .gemini/config.yaml
================================================
# This config mainly overrides `summary: false` by default
# as it's really noisy.
have_fun: true
code_review:
disable: false
comment_severity_threshold: "MEDIUM"
max_review_comments: -1
pull_request_opened:
help: false
# Turned off by default
summary: false
code_review: true
ignore_patterns: []
================================================
FILE: .github/ISSUE_TEMPLATE/bug.md
================================================
---
name: Bug report
about: Report an issue
---
# Bug Report #
## Environment ##
What hardware/cloud provider/hypervisor is being used?
## Expected Behavior ##
## Actual Behavior ##
## Reproduction Steps ##
1. ...
2. ...
## Other Information ##
================================================
FILE: .github/ISSUE_TEMPLATE/feature.md
================================================
---
name: Feature request
about: Suggest an enhancement
---
# Feature Request #
## Desired Feature ##
## Example Usage ##
## Other Information ##
================================================
FILE: .github/ISSUE_TEMPLATE/release-checklist.md
================================================
---
name: release checklist
about: release checklist template
title: New release for zincati
labels: jira,kind/release
warning: |
⚠️ Template generated by https://github.com/coreos/repo-templates; do not edit downstream
---
# Release process
This project uses [cargo-release][cargo-release] in order to prepare new releases, tag and sign the relevant git commit, and publish the resulting artifacts to [crates.io][crates-io].
The release process follows the usual PR-and-review flow, allowing an external reviewer to have a final check before publishing.
In order to ease downstream packaging of Rust binaries, an archive of vendored dependencies is also provided (only relevant for offline builds).
## Requirements
This guide requires:
* A web browser (and network connectivity)
* `git`
* [GPG setup][GPG setup] and personal key for signing
* `cargo` (suggested: latest stable toolchain from [rustup][rustup])
* `cargo-release` (suggested: `cargo install -f cargo-release`)
* `cargo vendor-filterer` (suggested: `cargo install -f cargo-vendor-filterer`)
* Write access to this GitHub project
* A verified account on crates.io
* Membership in the [Fedora CoreOS Crates Owners group](https://github.com/orgs/coreos/teams/fedora-coreos-crates-owners/members), which will give you upload access to crates.io
## Release checklist
These steps show how to release version `x.y.z` on the `origin` remote (this can be checked via `git remote -av`).
Push access to the upstream repository is required in order to publish the new tag and the PR branch.
:warning:: if `origin` is not the name of the locally configured remote that points to the upstream git repository (i.e. `git@github.com:coreos/zincati.git`), be sure to assign the correct remote name to the `UPSTREAM_REMOTE` variable.
- prepare environment:
- [ ] `RELEASE_VER=x.y.z`
- [ ] `UPSTREAM_REMOTE=origin`
- [ ] `git checkout -b pre-release-${RELEASE_VER}`
- check `Cargo.toml` for unintended increases of lower version bounds:
- [ ] `git diff $(git describe --abbrev=0) Cargo.toml`
- update all dependencies:
- [ ] `cargo update`
- [ ] `git add Cargo.lock && git commit -m "cargo: update dependencies"`
- land the changes:
- [ ] PR the changes, get them reviewed, approved and merged
- make sure the project is clean:
- [ ] Make sure `cargo-release` and `cargo-vendor-filterer` are up to date: `cargo install cargo-release cargo-vendor-filterer`
- [ ] `git checkout main && git pull ${UPSTREAM_REMOTE} main`
- [ ] `cargo vendor-filterer target/vendor`
- [ ] `cargo test --all-features --config 'source.crates-io.replace-with="vv"' --config 'source.vv.directory="target/vendor"'`
- [ ] `cargo clean`
- [ ] `git clean -fd`
- create release commit on a dedicated branch and tag it (the commit and tag will be signed with the GPG signing key you configured):
- [ ] `git checkout -b release-${RELEASE_VER}`
- [ ] `cargo release --execute ${RELEASE_VER}` (and confirm the version when prompted)
- open and merge a PR for this release:
- [ ] `git push ${UPSTREAM_REMOTE} release-${RELEASE_VER}`
- [ ] open a web browser and create a PR for the branch above
- [ ] make sure the resulting PR contains exactly one commit
- [ ] in the PR body, write a short changelog with relevant changes since last release
- [ ] get the PR reviewed, approved and merged
- publish the artifacts (tag and crate):
- [ ] `git checkout v${RELEASE_VER}`
- [ ] verify that `grep "^version = \"${RELEASE_VER}\"$" Cargo.toml` produces output
- [ ] `git push ${UPSTREAM_REMOTE} v${RELEASE_VER}`
- [ ] `cargo publish`
- assemble vendor archive:
- [ ] `cargo vendor-filterer --format=tar.gz --prefix=vendor target/zincati-${RELEASE_VER}-vendor.tar.gz`
- publish this release on GitHub:
- [ ] find the new tag in the [GitHub tag list](https://github.com/coreos/zincati/tags), click the triple dots menu, and create a release for it
- [ ] copy in the changelog from the release PR
- [ ] upload `target/zincati-${RELEASE_VER}-vendor.tar.gz`
- [ ] record digests of local artifacts:
- `sha256sum target/package/zincati-${RELEASE_VER}.crate`
- `sha256sum target/zincati-${RELEASE_VER}-vendor.tar.gz`
- [ ] publish release
- clean up the local environment (optional, but recommended):
- [ ] `cargo clean`
- [ ] `git checkout main`
- [ ] `git pull ${UPSTREAM_REMOTE} main`
- [ ] `git push ${UPSTREAM_REMOTE} :pre-release-${RELEASE_VER} :release-${RELEASE_VER}`
- [ ] `git branch -d pre-release-${RELEASE_VER} release-${RELEASE_VER}`
- Fedora packaging:
- [ ] Review the proposed changes in the PR submitted by Packit in [Fedora](https://src.fedoraproject.org/rpms/rust-zincati/pull-requests).
- [ ] once the PR merges to rawhide, merge rawhide into the other relevant branches (e.g. f43) then push those, for example:
```bash
git checkout rawhide
git pull --ff-only
git checkout f43
git merge --ff-only rawhide
git push origin f43
```
- [ ] on each of those branches run `fedpkg build`
- [ ] once the builds have finished, submit them to [bodhi](https://bodhi.fedoraproject.org/updates/new), filling in:
- `rust-zincati` for `Packages`
- selecting the build(s) that just completed, except for the rawhide one (which gets submitted automatically)
- writing brief release notes like "New upstream release; see release notes at `link to GitHub release`"
- leave `Update name` blank
- `Type`, `Severity` and `Suggestion` can be left as `unspecified` unless it is a security release. In that case select `security` with the appropriate severity.
- `Stable karma` and `Unstable` karma can be set to `2` and `-1`, respectively.
- [ ] [submit a fast-track](https://github.com/coreos/fedora-coreos-config/actions/workflows/add-override.yml) for FCOS testing-devel
- [ ] [submit a fast-track](https://github.com/coreos/fedora-coreos-config/actions/workflows/add-override.yml) for FCOS next-devel if it is [open](https://github.com/coreos/fedora-coreos-pipeline/blob/main/next-devel/README.md)
[cargo-release]: https://github.com/sunng87/cargo-release
[rustup]: https://rustup.rs/
[crates-io]: https://crates.io/
[GPG setup]: https://docs.github.com/en/github/authenticating-to-github/managing-commit-signature-verification
================================================
FILE: .github/dependabot.yml
================================================
# Maintained in https://github.com/coreos/repo-templates
# Do not edit downstream.
# Updates are grouped together by ecosystem in a single PR. An update can be
# removed from a combined update PR via comments to dependabot:
# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/managing-pull-requests-for-dependency-updates#managing-dependabot-pull-requests-for-grouped-updates-with-comment-commands
version: 2
updates:
- package-ecosystem: cargo
directory: /
schedule:
interval: monthly
open-pull-requests-limit: 10
labels:
- area/dependencies
groups:
build:
patterns:
- "*"
================================================
FILE: .github/workflows/containers.yml
================================================
---
name: Containers
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
contents: read
jobs:
build-fcos:
name: "Build in FCOS buildroot"
runs-on: ubuntu-latest
container: quay.io/coreos-assembler/fcos-buildroot:testing-devel
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Build and test
run: make
================================================
FILE: .github/workflows/rust.yml
================================================
# Maintained in https://github.com/coreos/repo-templates
# Do not edit downstream.
name: Rust
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
contents: read
# don't waste job slots on superseded code
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
CARGO_TERM_COLOR: always
# Pinned toolchain for linting
ACTIONS_LINTS_TOOLCHAIN: 1.90.0
jobs:
tests-stable:
name: Tests, stable toolchain
runs-on: ubuntu-latest
container: quay.io/coreos-assembler/fcos-buildroot:testing-devel
steps:
- name: Check out repository
uses: actions/checkout@v6
- name: Install toolchain
uses: dtolnay/rust-toolchain@v1
with:
toolchain: stable
- name: Cache build artifacts
uses: Swatinem/rust-cache@v2
- name: cargo build
run: cargo build --all-targets
- name: cargo test
run: cargo test --all-targets
- name: cargo test (failpoints)
run: cargo test --all-targets --features failpoints
tests-release-stable:
name: Tests (release), stable toolchain
runs-on: ubuntu-latest
container: quay.io/coreos-assembler/fcos-buildroot:testing-devel
steps:
- name: Check out repository
uses: actions/checkout@v6
- name: Install toolchain
uses: dtolnay/rust-toolchain@v1
with:
toolchain: stable
- name: Cache build artifacts
uses: Swatinem/rust-cache@v2
- name: cargo build (release)
run: cargo build --all-targets --release
- name: cargo test (release)
run: cargo test --all-targets --release
tests-release-msrv:
name: Tests (release), minimum supported toolchain
runs-on: ubuntu-latest
container: quay.io/coreos-assembler/fcos-buildroot:testing-devel
steps:
- name: Check out repository
uses: actions/checkout@v6
- name: Detect crate MSRV
run: |
msrv=$(cargo metadata --format-version 1 --no-deps | \
jq -r '.packages[0].rust_version')
echo "Crate MSRV: $msrv"
echo "MSRV=$msrv" >> $GITHUB_ENV
- name: Install toolchain
uses: dtolnay/rust-toolchain@v1
with:
toolchain: ${{ env.MSRV }}
- name: Cache build artifacts
uses: Swatinem/rust-cache@v2
- name: cargo build (release)
run: cargo build --all-targets --release
- name: cargo test (release)
run: cargo test --all-targets --release
linting:
name: Lints, pinned toolchain
runs-on: ubuntu-latest
container: quay.io/coreos-assembler/fcos-buildroot:testing-devel
steps:
- name: Check out repository
uses: actions/checkout@v6
- name: Install toolchain
uses: dtolnay/rust-toolchain@v1
with:
toolchain: ${{ env.ACTIONS_LINTS_TOOLCHAIN }}
components: rustfmt, clippy
- name: Cache build artifacts
uses: Swatinem/rust-cache@v2
- name: cargo fmt (check)
run: cargo fmt -- --check -l
- name: cargo clippy (warnings)
run: cargo clippy --all-targets -- -D warnings
tests-other-channels:
name: Tests, unstable toolchain
runs-on: ubuntu-latest
container: quay.io/coreos-assembler/fcos-buildroot:testing-devel
continue-on-error: true
strategy:
matrix:
channel: [beta, nightly]
steps:
- name: Check out repository
uses: actions/checkout@v6
- name: Install toolchain
uses: dtolnay/rust-toolchain@v1
with:
toolchain: ${{ matrix.channel }}
- name: Cache build artifacts
uses: Swatinem/rust-cache@v2
- name: cargo build
run: cargo build --all-targets
- name: cargo test
run: cargo test --all-targets
================================================
FILE: .gitignore
================================================
/target
**/*.rs.bk
vendor
jsonnetfile.lock.json
dashboard_out
================================================
FILE: .packit.yaml
================================================
# See the documentation for more information:
# https://packit.dev/docs/configuration/
actions:
changelog-entry:
- bash -c 'echo "- New upstream release"'
post-upstream-clone:
- wget https://src.fedoraproject.org/rpms/rust-zincati/raw/rawhide/f/rust-zincati.spec
specfile_path: rust-zincati.spec
downstream_package_name: rust-zincati
upstream_package_name: zincati
upstream_tag_template: v{version}
# add or remove files that should be synced
files_to_sync:
- rust-zincati.spec
- .packit.yaml
jobs:
- job: propose_downstream
trigger: release
# https://packit.dev/docs/configuration#aliases
dist_git_branches:
- fedora-development
- fedora-latest-stable
- job: koji_build
trigger: commit
dist_git_branches:
- fedora-development
- fedora-latest-stable
- job: bodhi_update
trigger: commit
dist_git_branches:
- fedora-development
- fedora-latest-stable
================================================
FILE: COPYRIGHT
================================================
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: zincati
Source: https://www.github.com/coreos/zincati
Files: *
Copyright: 2019 Red Hat Inc.
License: Apache-2.0
================================================
FILE: Cargo.toml
================================================
[package]
name = "zincati"
version = "0.0.32"
description = "Update agent for Fedora CoreOS"
homepage = "https://coreos.github.io/zincati"
license = "Apache-2.0"
keywords = ["cincinnati", "coreos", "fedora", "rpm-ostree"]
authors = ["Luca Bruno <luca.bruno@coreos.com>"]
repository = "https://github.com/coreos/zincati"
edition = "2021"
rust-version = "1.90.0"
[dependencies]
actix = "0.13"
anyhow = "1.0"
cfg-if = "1.0"
chrono = { version = "0.4.42", features = ["serde"] }
clap = { version = "4.5", features = ["cargo", "derive"] }
coreos-stream-metadata = "0.1.0"
env_logger = "0.11"
envsubst = "0.2"
fail = "0.5"
filetime = "0.2"
fn-error-context = "0.2"
futures = "0.3"
glob = "0.3"
intervaltree = "0.2.7"
lazy_static = "1.4"
libc = "0.2"
liboverdrop = "0.1.0"
libsystemd = "0.7"
log = "0.4"
maplit = "1.0"
num-traits = "0.2"
once_cell = ">= 1.19, < 1.30"
ordered-float = { version = "5.1", features = ["serde"] }
ostree-ext = "0.15.3"
prometheus = { version = "0.14", default-features = false }
rand = ">=0.9, < 0.10"
regex = "1.12"
reqwest = { version = "0.12", features = ["json"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tempfile = ">= 3.7, < 4.0"
thiserror = "2.0"
tokio = { version = "1.48", features = ["signal", "rt", "rt-multi-thread"] }
toml = ">= 0.8, < 0.10"
tzfile = "0.1.3"
url = { version = "2.5", features = ["serde"] }
users = "0.11.0"
zbus = "5.12.0"
[dev-dependencies]
http = "1.4"
mockito = "1.7"
proptest = "1.9"
tempfile = ">= 3.7, < 4.0"
[features]
failpoints = [ "fail/failpoints" ]
[package.metadata.release]
publish = false
push = false
pre-release-commit-message = "cargo: zincati release {{version}}"
sign-commit = true
sign-tag = true
tag-message = "zincati {{version}}"
# See https://github.com/coreos/cargo-vendor-filterer
[package.metadata.vendor-filter]
platforms = ["*-unknown-linux-gnu"]
tier = "2"
all-features = true
================================================
FILE: DCO
================================================
Developer Certificate of Origin
Version 1.1
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
660 York Street, Suite 102,
San Francisco, CA 94110 USA
Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.
Developer's Certificate of Origin 1.1
By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
(b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license (unless I am
permitted to submit under a different license), as indicated
in the file; or
(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
(d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: Makefile
================================================
RELEASE ?= 0
TARGETDIR ?= target
ifeq ($(RELEASE),1)
PROFILE ?= release
CARGO_ARGS = --release
else
PROFILE ?= debug
CARGO_ARGS =
endif
.PHONY: all
all: build check
.PHONY: build
build:
cargo build "--target-dir=${TARGETDIR}" ${CARGO_ARGS}
.PHONY: install
install: build
install -D -t ${DESTDIR}/usr/libexec "${TARGETDIR}/${PROFILE}/zincati"
install -D -m 644 -t ${DESTDIR}/usr/lib/zincati/config.d dist/config.d/*.toml
install -D -m 644 -t ${DESTDIR}/usr/lib/systemd/system dist/systemd/system/*.service
install -D -m 644 -t ${DESTDIR}/usr/lib/sysusers.d dist/sysusers.d/*.conf
install -D -m 644 -t ${DESTDIR}/usr/lib/tmpfiles.d dist/tmpfiles.d/*.conf
install -D -m 644 -t ${DESTDIR}/usr/share/polkit-1/rules.d dist/polkit-1/rules.d/*.rules
install -D -m 644 -t ${DESTDIR}/usr/share/polkit-1/actions dist/polkit-1/actions/*.policy
install -D -m 644 -t ${DESTDIR}/usr/share/dbus-1/system.d dist/dbus-1/system.d/*.conf
install -D -m 644 -t ${DESTDIR}/usr/bin dist/bin/*
.PHONY: check
check:
cargo test "--target-dir=${TARGETDIR}" ${CARGO_ARGS}
================================================
FILE: README.md
================================================
# Zincati
[](https://crates.io/crates/zincati)
Zincati is an auto-update agent for Fedora CoreOS hosts.
It works as a client for [Cincinnati] and [rpm-ostree], taking care of automatically updating/rebooting machines.
Features:
* Agent for [continuous auto-updates][auto-updates], with support for phased rollouts
* [Configuration][configuration] via TOML dropins and overlaid directories
* Multiple [update strategies][updates-strategy] for finalization/reboot
* Local [maintenance windows][strategy-periodic] on a weekly schedule for planned upgrades
* Internal [metrics][metrics] exposed over a local endpoint in Prometheus format
* [Logging][logging] with configurable priority levels
* Support for complex update-graphs via [Cincinnati protocol][cincinnati-protocol] (with rollout wariness, barriers, dead-ends and more)
* Support for [cluster-wide reboot orchestration][strategy-fleetlock], via an external lock-manager

[Cincinnati]: https://github.com/openshift/cincinnati
[rpm-ostree]: https://github.com/coreos/rpm-ostree
[auto-updates]: ./docs/usage/auto-updates.md
[configuration]: ./docs/usage/configuration.md
[updates-strategy]: ./docs/usage/updates-strategy.md
[strategy-periodic]: ./docs/usage/updates-strategy.md#periodic-strategy
[metrics]: ./docs/usage/metrics.md
[logging]: ./docs/usage/logging.md
[cincinnati-protocol]: ./docs/development/cincinnati/protocol.md
[strategy-fleetlock]: ./docs/usage/updates-strategy.md#lock-based-strategy
================================================
FILE: contrib/monitoring-mixins/README.md
================================================
# Requirements
In order to customize and generate monitoring artifacts, the following tools are required:
* `jb` available at <https://github.com/jsonnet-bundler/jsonnet-bundler>.
* `jsonnet` available at <https://github.com/google/jsonnet>.
* `mixtool` available at <https://github.com/monitoring-mixins/mixtool>.
For more information, see <https://monitoring.mixins.dev/>.
# Artifacts generation
Monitoring artifacts can be generated from mixins in a few steps:
```sh
# Clean stale artifacts.
rm -rf vendor/ generated/ jsonnetfile.lock.json
# Fetch jsonnet libraries.
jb install
# Generate Grafana dashboards.
mixtool generate dashboards -d generated/dashboards/ mixin.libsonnet
```
================================================
FILE: contrib/monitoring-mixins/dashboards/dashboards.libsonnet
================================================
local grafana = import 'github.com/grafana/grafonnet-lib/grafonnet/grafana.libsonnet';
local dashboard = grafana.dashboard;
local row = grafana.row;
local prometheus = grafana.prometheus;
local graphPanel = grafana.graphPanel;
{
grafanaDashboards+:: {
'dashboard.json':
dashboard.new(
'Fedora CoreOS updates (Zincati)',
time_from='now-7d',
).addTemplate(
{
current: {
text: 'Prometheus',
value: 'Prometheus',
},
hide: 0,
label: null,
name: 'datasource',
options: [],
query: 'prometheus',
refresh: 1,
regex: '',
type: 'datasource',
},
)
.addRow(
row.new(
title='Agent identity',
)
.addPanel(
graphPanel.new(
'OS versions',
datasource='$datasource',
decimalsY1=0,
format='short',
legend_alignAsTable=true,
legend_current=true,
legend_show=true,
legend_values=true,
min=0,
span=6,
stack=true,
)
.addTarget(prometheus.target(
'sum by(os_version) (zincati_identity_os_info)',
legendFormat='{{os_version}}'
))
)
.addPanel(
graphPanel.new(
'Static rollout wariness',
datasource='$datasource',
format='short',
legend_show=true,
min=0,
span=6,
)
.addTarget(prometheus.target(
'zincati_identity_rollout_wariness != 0',
legendFormat='{{instance}}'
))
)
)
.addRow(
row.new(
title='Agent details',
)
.addPanel(
graphPanel.new(
'Agent refresh period (p99)',
datasource='$datasource',
formatY1='s',
span=6,
min=0,
)
.addTarget(prometheus.target(
'quantile_over_time(0.99, (time() - zincati_update_agent_last_refresh_timestamp)[15m:])',
legendFormat='{{instance}}'
))
)
.addPanel(
graphPanel.new(
'Cincinnati client error-rate',
datasource='$datasource',
span=6,
min=0,
)
.addTarget(prometheus.target(
'sum by (kind) (rate(zincati_cincinnati_update_checks_errors_total[5m]))',
legendFormat='kind: {{kind}}'
))
)
.addPanel(
graphPanel.new(
'Deadends detected',
datasource='$datasource',
decimalsY1=0,
format='short',
legend_alignAsTable=true,
legend_current=true,
legend_show=true,
legend_values=true,
min=0,
span=6,
stack=true,
)
.addTarget(prometheus.target(
'sum by (os_version) ((zincati_cincinnati_booted_release_is_deadend) + on (instance) group_left(os_version) (0*zincati_identity_os_info))',
legendFormat='{{os_version}}'
))
)
),
},
}
================================================
FILE: contrib/monitoring-mixins/jsonnetfile.json
================================================
{
"version": 1,
"dependencies": [
{
"source": {
"git": {
"remote": "https://github.com/grafana/grafonnet-lib.git",
"subdir": "grafonnet"
}
},
"version": "8fb95bd89990e493a8534205ee636bfcb8db67bd"
}
],
"legacyImports": false
}
================================================
FILE: contrib/monitoring-mixins/mixin.libsonnet
================================================
(import 'dashboards/dashboards.libsonnet')
================================================
FILE: dist/bin/zincati-update-now
================================================
#!/bin/bash
set -euo pipefail
echo "WARN: This command is experimental and subject to change." >&2
if [ "$EUID" != "0" ]; then
echo "ERROR: Must be root to run zincati-update-now" >&2
exit 1
fi
# this should exist already, but in case
mkdir -p /run/zincati/config.d
cat > /run/zincati/config.d/99-update-now.toml << EOF
[identity]
rollout_wariness = 0.0
[updates]
strategy = "immediate"
EOF
touch /run/zincati/override-interactive-check
systemctl daemon-reload
systemctl restart zincati --no-block
echo "INFO: Streaming Zincati and RPM-OSTree logs..." >&2
exec journalctl -f -u zincati -u rpm-ostreed --since now
================================================
FILE: dist/config.d/10-agent.toml
================================================
# Configure agent timing.
[agent.timing]
# Pausing interval between updates checks in steady mode, in seconds.
steady_interval_secs = 300
================================================
FILE: dist/config.d/10-auto-updates.toml
================================================
# Enable auto-updates.
[updates]
# Boolean to enable auto-update logic.
# There is almost no case where disabling this is a good idea.
enabled = true
# Boolean to allow downgrading via updates logic.
# This provides an additional safety net against rogue servers,
# and allowing downgrades via Zincati is generally not recommended.
allow_downgrade = false
================================================
FILE: dist/config.d/10-identity.toml
================================================
# Configure agent identity.
[identity]
# Node group, used for cluster-wide reboot orchestration (e.g. Airlock).
group = "default"
# Node ID, in sd-id128(3) format.
# By default it is automatically computed as
# `systemd-id128 machine-id -a de35106b6ec24688b63afddaa156679b`
#node_uuid = "<ID128>"
# Client wariness for throttled rollouts, as a floating point number.
# Allowed values are within the range from `0.0` (very bold clients) to `1.0` (cautious clients), limits included.
# By default, clients are arbitrarily throttled by the Cincinnati server.
#rollout_wariness = 0.5
================================================
FILE: dist/config.d/30-updates-strategy.toml
================================================
# How to finalize updates.
[updates]
# String to customize update strategy.
# Default strategy is to immediately finalize updates as soon as available,
# and reboot the node.
strategy = "immediate"
# Update strategy which uses an external reboot coordinator (FleetLock protocol).
#strategy = "fleet_lock"
# Base URL for the FleetLock service.
#fleet_lock.base_url = "https://fleet-lock.example.com/"
# Update strategy which uses a periodic schedule for reboot/maintenance
# windows, on a weekly basis.
#strategy = "periodic"
# Example of reboot windows: weekend days, between 01:00 and 02:00 UTC.
#[[updates.periodic.window]]
#days = [ "Sat", "Sun" ]
#start_time = "01:00"
#length_minutes = 60
================================================
FILE: dist/config.d/50-fedora-coreos-cincinnati.toml
================================================
# Fedora CoreOS Cincinnati backend
[cincinnati]
base_url= "https://updates.coreos.fedoraproject.org"
================================================
FILE: dist/dbus-1/system.d/org.coreos.zincati.conf
================================================
<?xml version="1.0" encoding="UTF-8"?> <!-- -*- XML -*- -->
<!DOCTYPE busconfig PUBLIC
"-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<busconfig>
<!-- Generally allow access only for introspection -->
<policy context="default">
<allow send_destination="org.coreos.zincati"
send_interface="org.freedesktop.DBus.Introspectable"/>
<allow send_destination="org.coreos.zincati"
send_interface="org.freedesktop.DBus.Peer"/>
<allow send_destination="org.coreos.zincati"
send_interface="org.freedesktop.DBus.Properties"/>
</policy>
<!-- User 'zincati' is the service owner -->
<policy user="zincati">
<allow own_prefix="org.coreos.zincati"/>
<allow send_destination="org.coreos.zincati"/>
<allow receive_sender="org.coreos.zincati"/>
</policy>
<!-- User 'root' is allowed to call into the service -->
<policy user="root">
<allow send_destination="org.coreos.zincati"/>
<allow receive_sender="org.coreos.zincati"/>
</policy>
</busconfig>
================================================
FILE: dist/polkit-1/actions/org.coreos.zincati.deadend.policy
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE policyconfig PUBLIC
"-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
"http://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd">
<policyconfig>
<action id="org.coreos.zincati.deadend">
<description>Write dead-end release information as an MOTD fragment via Zincati</description>
<defaults>
<allow_any>no</allow_any>
<allow_inactive>no</allow_inactive>
<allow_active>no</allow_active>
</defaults>
<annotate key="org.freedesktop.policykit.exec.path">/usr/libexec/zincati</annotate>
<annotate key="org.freedesktop.policykit.exec.argv1">deadend-motd</annotate>
</action>
</policyconfig>
================================================
FILE: dist/polkit-1/rules.d/zincati.rules
================================================
// Allow Zincati to deploy, finalize, and cleanup a staged deployment through rpm-ostree.
polkit.addRule(function(action, subject) {
if (action.id == "org.projectatomic.rpmostree1.deploy" ||
action.id == "org.projectatomic.rpmostree1.rebase" ||
action.id == "org.projectatomic.rpmostree1.finalize-deployment" ||
action.id == "org.projectatomic.rpmostree1.cleanup") {
if (subject.user == "zincati") {
return polkit.Result.YES;
}
}
});
// Allow Zincati to write dead-end release information as an MOTD fragment.
polkit.addRule(function(action, subject) {
if (action.id == "org.coreos.zincati.deadend" &&
subject.user == "zincati") {
return polkit.Result.YES;
}
});
================================================
FILE: dist/systemd/system/zincati.service
================================================
[Unit]
Description=Zincati Update Agent
Documentation=https://github.com/coreos/zincati
# Skip live systems not meant to be auto-updated (e.g. live PXE, live ISO)
ConditionPathExists=!/run/ostree-live
After=network.target
# Wait for the boot to be marked as successful. In cluster contexts,
# this prevents rolling out broken updates to all nodes in the fleet.
Requires=boot-complete.target
After=multi-user.target boot-complete.target
# Make sure we don't inadvertently reboot the system before a machine-id is
# created so that we don't cause ConditionFirstBoot=true units to run twice
# See discussions in https://github.com/systemd/systemd/issues/4511.
After=systemd-machine-id-commit.service
[Service]
User=zincati
Group=zincati
SupplementaryGroups=tty
Environment=ZINCATI_VERBOSITY="-v"
Type=notify
ExecStart=/usr/libexec/zincati agent ${ZINCATI_VERBOSITY}
Restart=on-failure
RestartSec=10s
[Install]
WantedBy=multi-user.target
================================================
FILE: dist/sysusers.d/50-zincati.conf
================================================
# Zincati - https://github.com/coreos/zincati
# Type Name ID GECOS
u zincati - "Zincati user for auto-updates"
================================================
FILE: dist/tmpfiles.d/zincati.conf
================================================
#Type Path Mode User Group Age Argument
d /run/zincati 0775 zincati zincati - -
# Runtime configuration fragments
d /run/zincati/config.d 0775 zincati zincati - -
# Runtime state, unstable/private implementation details
d /run/zincati/private 0770 zincati zincati - -
# Runtime public interfaces
d /run/zincati/public 0775 zincati zincati - -
# Legacy symlink to metrics socket
L+ /run/zincati/private/metrics.promsock - - - - ../public/metrics.promsock
================================================
FILE: docs/_config.yml
================================================
# Template generated by https://github.com/coreos/repo-templates; do not edit downstream
# To test documentation changes locally or using GitHub Pages, see:
# https://github.com/coreos/fedora-coreos-tracker/blob/main/docs/testing-project-documentation-changes.md
title: Zincati
description: Zincati documentation
baseurl: "/zincati"
url: "https://coreos.github.io"
permalink: /:title/
markdown: kramdown
kramdown:
typographic_symbols:
ndash: "--"
mdash: "---"
remote_theme: just-the-docs/just-the-docs@v0.12.0
plugins:
- jekyll-remote-theme
color_scheme: coreos
# Aux links for the upper right navigation
aux_links:
"Zincati on GitHub":
- "https://github.com/coreos/zincati"
footer_content: "Copyright © <a href=\"https://www.redhat.com\">Red Hat, Inc.</a> and <a href=\"https://github.com/coreos\">others</a>."
# Footer last edited timestamp
last_edit_timestamp: true
last_edit_time_format: "%b %e %Y at %I:%M %p"
# Footer "Edit this page on GitHub" link text
gh_edit_link: true
gh_edit_link_text: "Edit this page on GitHub"
gh_edit_repository: "https://github.com/coreos/zincati"
gh_edit_branch: "main"
gh_edit_source: docs
gh_edit_view_mode: "tree"
compress_html:
clippings: all
comments: all
endings: all
startings: []
blanklines: false
profile: false
================================================
FILE: docs/_sass/color_schemes/coreos.scss
================================================
$link-color: #53a3da;
================================================
FILE: docs/contributing.md
================================================
---
nav_order: 9
---
# Contributing
## Project architecture
[Development doc-pages][devdocs] cover several aspects of this project, both at low-level (code and logic) and high-level (architecture and design).
[devdocs]: development.md
## Release process
Releases can be performed by [creating a new release ticket][new-release-ticket] and following the steps in the checklist there.
[new-release-ticket]: https://github.com/coreos/zincati/issues/new?labels=kind/release&template=release-checklist.md
================================================
FILE: docs/development/agent-actor-system.md
================================================
---
nav_order: 2
parent: Development
---
# Actor model and agent subsystems
The Zincati `agent` command provides a long-running background service which drives the OS through auto-updates.
It comprises several logical subsystems which can run in parallel and are arranged following the [actor model][wiki-actors].
The goal of this design is manifold:
* it allows splitting logical subsystems into separate failure domains.
* it models asynchronous operations and parallel components in an explicit way.
* it minimizes the amount of shared state and locks, minimizing the chances of deadlocks and concurrency bugs.
[wiki-actors]: https://en.wikipedia.org/wiki/Actor_model
## Actor system
The core of the agent service is built on top of [Actix][actix], an asynchronous actor framework.
The only exception is the initialization logic (i.e. CLI flags and configuration files parsing), which is performed in a synchronous way and only once at service startup.
[actix]: https://github.com/actix/actix
A general overview on such actor framework is provided by the [Actix book][actix-book], and further API details are covered by its [documentation](docs-rs-actix).
There is also a companion web-framework called `actix-web`, which is however not relevant in this context.
[actix-book]: https://actix.rs/book/actix/
[docs-rs-actix]: https://docs.rs/actix
The agent is split into several actors, which encapsulate state and behavior, as shown in the diagram below.
They can run in parallel and communicate by exchanging messages.

### Metrics service
The "metrics service" actor is responsible for serving client requests on the local [metrics endpoint][usage-metrics].
It does not exchange message with other actors, and it asynchronously processes the stream of incoming connections.
Once accepted, each new client connection is represented as a `Connection` message.
[usage-metrics]: ../usage/metrics.md
### Update agent
The "update agent" actor contains the core logic of Zincati agent.
This actor manages the Finite State Machine (FSM) which supervises the auto-update flow.
A time-based ticker drives the state machine. Timer ticks are represented by `RefreshTick` messages, which are sent by the actor to itself (with a delay) at each iteration.
This actor interacts with some local subsystems (e.g. the underlying Operating System) and also with remote ones (e.g. the Cincinnati service).
In general, operations which cannot be performed in a non-blocking way are delegated to other dedicated actors (e.g. rpm-ostree tasks).
### Rpm-ostree client
The "rpm-ostree client" actor is responsible for shelling out to the `rpm-ostree` command, in order to interact with the rpm-ostree daemon.
As client commands require blocking and may take a long time to complete, this entity is implemented as a synchronous actor.
This actor bridges incoming requests (messages) to rpm-ostree actions (CLI commands):
* `QueryLocalDeployments` maps to `rpm-ostree status`.
* `FinalizeDeployment` maps to `rpm-ostree finalize-deployment`.
* `StageDeployment` maps to `rpm-ostree deploy --lock-finalization`.
Those actions are generally requested by the core "update agent" actor via the relevant message, and (processed) results are sent back to it once the task has completed.
================================================
FILE: docs/development/cincinnati/protocol.md
================================================
---
title: Cincinnati for Fedora CoreOS
parent: Development
nav_order: 3
---
# Cincinnati for Fedora CoreOS
Cincinnati is a protocol to provide "update hints" to clients, and it builds upon experiences with the [Omaha update protocol][google-omaha].
It describes a particular method for representing transitions between releases of a project, allowing clients to apply updates in the right order.
[google-omaha]: https://github.com/google/omaha/blob/v1.3.33.7/doc/ServerProtocolV3.md
## Update Graph
Cincinnati uses a [directed acyclic graph][dag] (DAG) to represent the complete set of valid update-paths.
Each node in the graph is a release (with payload details) and each directed edge is a valid transition.
[dag]: https://en.wikipedia.org/wiki/Directed_acyclic_graph
## Clients
Cincinnati clients are the final consumers of the update graph and payloads.
A client periodically queries the Cincinnati service in order to fetch updates hints.
Once it discovers at least a valid update edge, it may or may not decide to apply it locally (based on its configuration and heuristic).
## Graph API
### Request
HTTP `GET` requests are used to fetch the DAG (as a JSON object) from the Graph API endpoint.
Requests SHOULD be sent to the Graph API endpoint at `/v1/graph` and MUST include the following header:
```
Accept: application/json
```
Fedora CoreOS clients MUST provide additional details as URL query parameters in the request.
| Key | Optional | Description |
|------------------|----------|-------------------------------------------------------|
| basearch | required | base architecture (non-empty string) |
| stream | required | client-selected update stream (non-empty string) |
| node_uuid | optional | application-specific unique-identifier for the client |
| os_version | optional | current OS version |
| os_checksum | optional | current OS checksum |
| group | optional | update group |
| rollout_wariness | optional | client wariness to update rollout |
| platform | optional | client platform |
### Response
A positive response to the `/v1/graph` endpoint MUST be a JSON representation of the update graph.
Each known release is represented by an entry in the top-level `nodes` array.
Each of these entries includes the release version label, a payload identifier and any additional metadata. Each entry follows this schema:
| Key | Optional | Description |
|----------|----------|-----------------------------------------------------------------------------------------|
| version | required | the version of the release, as a unique (across "nodes" array) non-empty JSON string |
| payload | required | payload identifier, as a JSON string |
| metadata | required | a string-\>string map conveying arbitrary information about the release |
Allowed transitions between releases are represented as a top-level `edges` array, where each entry is an array-tuple.
Each of these tuples has two fields: the index of the starting node, and the index of the target node. Both are non-negative integers, ranging from 0 to `len(nodes)-1`.
For an example of a valid JSON document from a graph response, see [response.json](./response.json).
### Errors
Errors on the `/v1/graph` endpoint SHOULD be returned to the client as JSON objects, with a 4xx or 5xx HTTP status code.
Error values carry a type-identifier and a textual description, according to the following schema:
| Key | Optional | Description |
|--------|----------|--------------------------------------------------------------|
| kind | required | error type identifier, as a non-empty JSON string |
| value | required | human-friendly error description, as a non-empty JSON string |
================================================
FILE: docs/development/cincinnati/response.json
================================================
{
"nodes": [
{
"version": "32.20200517.1.0",
"metadata": {
"org.fedoraproject.coreos.releases.age_index": "0",
"org.fedoraproject.coreos.scheme": "checksum"
},
"payload": "7c23c4735fb3c541586f0a4d3ca956ef93ef7d76f00a19bccf51460bafa7ee97"
},
{
"version": "32.20200601.1.0",
"metadata": {
"org.fedoraproject.coreos.scheme": "checksum",
"org.fedoraproject.coreos.releases.age_index": "1"
},
"payload": "8cffe35be831fa2601d315002cb39fb22509a4e7d3db10e61f880523f69b3bf6"
},
{
"version": "32.20200601.1.1",
"metadata": {
"org.fedoraproject.coreos.releases.age_index": "2",
"org.fedoraproject.coreos.scheme": "checksum",
"org.fedoraproject.coreos.updates.start_epoch": "1591279200",
"org.fedoraproject.coreos.updates.start_value": "0",
"org.fedoraproject.coreos.updates.rollout": "true",
"org.fedoraproject.coreos.updates.duration_minutes": "2880"
},
"payload": "08040bebbab87a3343a281f94bb68010df618eb6ce6ac3d4230d2595959b5da1"
}
],
"edges": [
[
0,
2
],
[
1,
2
]
]
}
================================================
FILE: docs/development/fleetlock/protocol.md
================================================
---
parent: Development
nav_order: 4
---
# FleetLock protocol
This document describes an HTTP-based protocol for orchestrating fleet-wide reboots, used by Zincati.
It is modeled after a distributed counting semaphore with recursive locking and lock-ownership.
## Overview
The FleetLock protocol is a request-response protocol where operations are always initiated by the client (i.e. Zincati).
Each operation consists of a JSON payload sent as a POST request to the server.
At an high level, the client can perform two operations:
* `RecursiveLock`: try to reserve (lock) a slot for rebooting
* `UnlockIfHeld`: try to release (unlock) a slot that it was previously holding
Semaphore locks are owned, so that only the client that created a lock can release it.
All operations are recursive, meaning that multiple unbalanced lock/unlock actions by a client are allowed.
## Client state-machine
Clients start off in one of two states based on the system condition: "initialization" or "finalization". There are a number of states between "initialization" and "finalization" as well.
In the "**initialization**" state, the client tries to release any reboot slot it may have previously held.
A successful unlock operation means that the client can proceed into its "**steady**" state and look for further updates.
When an update is found and locally staged, the client proceed into its "**pre-reboot**" state and tries to lock a reboot slot.
A successful lock operation means that the client can proceed into its "**finalization**" state and finalize a pending update, then reboot.
## Requests
### Endpoints
All endpoints defined below are relative to a common deployment-specific base URL:
* `/v1/pre-reboot`: reserve/lock a reboot slot
* `/v1/steady-state`: release/unlock a reboot slot
### Body
All POST requests contain well-formed JSON body according to the following schema:
* `client_params` (object, mandatory)
* `id` (string, mandatory, non-empty): client identifier (e.g. node name or UUID)
* `group` (string, mandatory, non-empty): reboot-group of the client
Client ID is a case-sensitive textual label that uniquely identifies a lock holder. It is generated and persisted by each client.
Client group is a mandatory textual label, conforming to the regexp `^[a-zA-Z0-9.-]+$`. This labels can be configured on each client. A server SHOULD check this value and MAY use it to provide multiple reboot buckets (sorting a fleet of nodes into reboot tiers).
By default, Zincati uses the group name "`default`" unless explicitly configured otherwise.
### Headers
Locking and unlocking requests must contain a `fleet-lock-protocol` header with a fixed value of `true` to ensure that the actual request was directly intended and not a part of unintentional redirection.
### Response
If the operation is succesful, a 200 status code is returned. Every other code is considered as a failed operation.
### Example
A client with UUID `c988d2509fdf4cdcbed39037c56406fb` and group `workers` can try to acquire a reboot slot from `https://example.com/base` in a way which is conceptually similar to the following:
Request body:
```json
{
"client_params": {
"group": "workers",
"id": "c988d2509fdf5cdcbed39037c56406fb"
}
}
```
POST request:
```shell
curl -H "fleet-lock-protocol: true" -d @body.json http://example.com/base/v1/pre-reboot
```
### Errors
Errors on the service endpoints SHOULD be returned to the client as JSON objects, with a 4xx or 5xx HTTP status code.
Error values carry a type-identifier and a textual description, according to the following schema:
| Key | Optional | Description |
|--------|----------|--------------------------------------------------------------|
| kind | required | error type identifier, as a non-empty JSON string |
| value | required | human-friendly error description, as a non-empty JSON string |
This allows clients to show more specific error details to cluster administrators, instead of generic HTTP errors.
For example, an error value like the following could be returned on `/v1/pre-reboot` when all available slots are already in use:
```json
{
"kind": "failed_lock_semaphore_full",
"value": "semaphore currently full, all slots are locked already"
}
```
Zincati will log this error using the content of `value`, and it will track the `kind` label in metrics.
A server MUST ensure that possible values for `kind` have a bounded/small cardinality.
================================================
FILE: docs/development/os-metadata.md
================================================
---
nav_order: 5
parent: Development
---
# OS metadata and agent identity
The agent needs to derive its own identity from several aspects of the underlying OS.
In order to do so, at startup it performs run-time introspection of current machine state and OS metadata.
The following details are derived from the host environment:
* application-specific node UUID
* base architecture
* update stream
* OS platform
* OS version
* OSTree revision
It is thus required that the OS provides those values in the locations described below.
### Kernel command-line
Kernel command-line must contain a `ignition.platform.id=<VALUE>` argument. The literal value is used as the "OS platform".
### rpm-ostree deployment status
Booted deployment must provide several mandatory metadata entries:
* `checksum`: OSTree commit revision
* `version`: OS version
* under `base-commit-meta`:
* `fedora-coreos.stream`: update stream
All those metadata entries must exist with a non-empty string value.
### Filesystem
Filesystem must provide a `/etc/machine-id` file, as specified by [machine-id spec][machine-id]. Its value is used to derive the application-specific node UUID.
[machine-id]: https://www.freedesktop.org/software/systemd/man/machine-id.html
================================================
FILE: docs/development/quickstart.md
================================================
---
nav_order: 1
parent: Development
---
# Development quickstart
This is quick start guide for developing and building this project from source on a Linux machine.
## Get the source
The canonical development location of this project is on GitHub. You can fetch the full source with history via `git`:
```sh
git clone https://github.com/coreos/zincati.git
cd zincati
```
It is recommend to fork a copy of the project to your own GitHub account, and add it as an additional remote:
```sh
git remote add my-fork git@github.com:<YOURUSER>/zincati.git
```
## Install Rust toolchain
This project is written in Rust, and requires a stable toolchain to build. Additionally, `clippy` and `rustfmt` are used by CI jobs to ensure that patches are properly formatted and linted.
You can obtain a Rust toolchain via many distribution methods, but the simplest way is via [rustup](https://rustup.rs/):
```sh
rustup component add clippy
rustup component add rustfmt
rustup install stable
```
## Build and test
Building and testing is handled via `cargo` and `make`:
```sh
make build
make check
```
If you prefer running builds in a containerized environment, you can use the FCOS buildroot image at `quay.io/coreos-assembler/fcos-buildroot:testing-devel`:
```sh
docker pull quay.io/coreos-assembler/fcos-buildroot:testing-devel
docker run --rm -v "$(pwd):/source:z" quay.io/coreos-assembler/fcos-buildroot:testing-devel bash -c "cd source; make"
```
The FCOS buildroot image is the same image that is used by integration jobs in CI.
It contains all the required dependencies and can be used to build other CoreOS projects too (not only Zincati).
## Assemble custom OS images
`coreos-assembler` ([`cosa`](https://github.com/coreos/coreos-assembler)) makes it very handy to embed build artifacts in a custom OS image, in order to test patches in the final environment.
Once a new `cosa` workspace has been initialized, you can place the binaries in the `overrides/` directory before building your custom image:
```sh
pushd /tmp
mkdir test-image
cd test-image
cosa init https://github.com/coreos/fedora-coreos-config
popd
docker run --rm -v "$(pwd):/source:z" -v "/tmp/test-image:/assembler:z" \
-e DESTDIR="/assembler/overrides/rootfs" -e TARGETDIR="/assembler/tmp/zincati/target" \
quay.io/coreos-assembler/fcos-buildroot:testing-devel bash -c "cd source; make install"
pushd /tmp/test-image
cosa fetch
cosa build
```
For more details, see `coreos-assembler` [overrides documentation](https://coreos.github.io/coreos-assembler/working/#using-overrides).
### `build-fast` for faster iteration
It is possible to use the CoreOS Assembler's [`build-fast`][build-fast-cmd] command for faster iteration.
See [here][build-fast-instructions] for instructions on fast-building a qemu image for testing.
[build-fast-cmd]: https://github.com/coreos/coreos-assembler/blob/main/src/cmd-build-fast
[build-fast-instructions]: https://github.com/coreos/coreos-assembler/blob/2f834d37353ca5f40b460eae2aea73ef995bc710/docs/kola/external-tests.md#fast-build-and-iteration-on-your-projects-tests
================================================
FILE: docs/development/testing.md
================================================
---
nav_order: 7
parent: Development
---
# Testing
## Unit Tests
Unit tests can be run using `make check` (via `cargo test`).
## External Kola Tests
[External Kola tests][kola-ext-tests] can be found in the `tests/kola/` directory.
### `server` Tests
The `tests/kola/server/` test directory contains tests that require access to a mock Cincinnati server. This test directory contains a Fedora CoreOS config that does the following:
- creates a `/var/www/` directory
- [sets up an HTTP server][kolet-httpd] at `localhost` listening on port `80` serving files from the `/var/www/` directory
- configures Zincati to use `localhost` as its Cincinnati base URL
- adds a systemd dropin to set Zincati's journal log verbosity to max (`-vvvv`)
Tests place mock release graphs in `/var/www/` for Zincati to fetch.
### Running the Tests
A built Fedora CoreOS image is required; it is recommended to use the CoreOS Assembler's [`build-fast` command][cosa-build-fast] for faster iteration.
To run the tests, specify the path to your Zincati project directory and which tests to run using `kola run`'s `-E` option.
Example (run all tests):
```
kola run --qemu-image fastbuild-fedora-coreos-zincati-qemu.qcow2 -E /path/to/zincati/ 'ext.zincati.*'
```
Example (run only the `server` tests):
```
kola run --qemu-image fastbuild-fedora-coreos-zincati-qemu.qcow2 -E /path/to/zincati/ 'ext.zincati.server.*'
```
### Adding Tests
Refer to kola external tests' [README][kola-ext-quick-start] for instructions on adding additional tests
[kolet-httpd]: https://github.com/coreos/coreos-assembler/blob/main/docs/kola/external-tests.md#http-server
[cosa-build-fast]: https://coreos.github.io/coreos-assembler/kola/external-tests/#fast-build-and-iteration-on-your-projects-tests
[kola-ext-tests]: https://coreos.github.io/coreos-assembler/kola/external-tests/
[kola-ext-quick-start]: https://coreos.github.io/coreos-assembler/kola/external-tests/#quick-start
================================================
FILE: docs/development/update-strategy-periodic.md
================================================
---
nav_order: 6
parent: Development
---
# Periodic update strategy
The agent supports a `periodic` strategy, which allows gating reboots based on "reboot windows", defined on weekly basis.
This strategy is a port of [locksmith reboot windows][locksmith], with a few differences:
* multiple disjoint reboot windows are supported
* multiple configuration entries are assembled into a single weekly calendar
* weekdays need to be specified, in either long or abbreviated form
* length duration is always specified in minutes
[locksmith]: https://github.com/coreos/locksmith/tree/v0.6.2#reboot-windows
# Timing and configuration
Window granularity is at the "minutes" level. For this reason, the configuration parameter `length_minutes` is a plain non-zero integer (instead of a free-form duration string).
In order to ease the case where the same time-window has to be applied on multiple specific days, the `days` parameter accepts a set of weekdays (instead of a single day).
The start of a reboot window is a single point in time, specified in 24h format with minutes granularity (e.g. `22:30`) via the `start_time` parameter.
By default, all times and dates are UTC-based.
UTC times must be used to avoid:
* shortening or skipping reboot windows due to Daylight Saving Time time-change
* lengthening reboot windows due to Daylight Saving Time time-change
* mixups due to short-notice law changes in time-zone definitions
* errors due to stale `tzdata` entries
* human confusion on machines with different local-timezone configurations
Overall, the use of the default UTC times guarantee that the total weekly length for reboot windows is respected, regardless of local time zone laws.
As a side-effect, this also helps when cross-checking configurations across multiple machines located in different places.
Nevertheless, user-specified non-UTC time zones can still be configured, but with [caveats][time-zone-caveats].
[time-zone-caveats]: ../usage/updates-strategy.md#time-zone-caveats
# Implementation details
Configuration fragments are merged into a single weekly calendar.
In order to avoid too many unwieldy datetime operations to be performed in "modulo 7 days", all times are converted to "minutes since beginning of week".
This means that all datetimes are mapped to the range that goes from `0` (00:00 on Monday morning) to `MAX_WEEKLY_MINS` (23:59 on Sunday night).
A reboot window which is specified across week boundary (e.g. starting on Sunday and ending on Monday) gets split into two sub-windows in order to respect the range above.
Reboot windows are internally stored within an [Augmented Interval Tree](https://en.wikipedia.org/wiki/Interval_tree#Augmented_tree) data-structure.
================================================
FILE: docs/development.md
================================================
---
nav_order: 3
has_children: true
---
# Development
================================================
FILE: docs/images/zincati-actors.dot
================================================
# Render with: `dot -T png -o zincati-actors.png zincati-actors.dot`
digraph actors_messages {
newrank = true;
fontsize=11;
node [shape=box, style="rounded", color=lightgrey; fontname="Arial"; fontsize=11;];
edge[arrowhead="vee"; fontcolor=darkgoldenrod; fontsize=8;];
subgraph cluster_metrics_service {
label = "Async Actor:\nmetrics service";
style = dashed;
color = deepskyblue;
ConnectionStream [label=<StreamHandler<br/><<b>Connection</b>>>;];
# Invisble placeholders.
InvisMetricsClient:s [style=invis];
InvisBottomMetrics [style=invis];
ConnectionStream:s -> InvisBottomMetrics:n [style=invis];
}
subgraph cluster_dbus_server {
label = "Sync Actor:\nD-Bus server";
style = dashed;
color = deepskyblue;
SyncLastRefersh [label="sync fn\nlast_refresh_time()"];
# Invisble placeholders.
InvisBottomDbus [style=invis];
}
subgraph cluster_update_agent {
label = "Async Actor:\nupdate agent";
style = dashed;
color = deepskyblue;
AsyncLocalDeployments [label="async fn\nlocal_deployments()"];
AsyncAttemptDeploy [label="async fn\nattempt_deploy()"];
AsyncFinalizeDeployment [label="async fn\nfinalize_deployment()"];
RefreshTick [label=<Handler<br/><<b>RefreshTick</b>>>];
LastRefresh [label=<Handler<br/><<b>LastRefresh</b>>>]
}
subgraph cluster_rpm_ostree_client {
label = "Sync Actor:\nrpm-ostree client";
style = dashed;
color = deepskyblue;
QueryLocalDeployments [label=<Handler<br/><<b>QueryLocalDeployments</b>>>];
StageDeployment [label=<Handler<br/><<b>StageDeployment</b>>>];
FinalizeDeployment [label=<Handler<br/><<b>FinalizeDeployment</b>>>];
# Invisble placeholders.
QueryLocalDeployments:s -> StageDeployment:n [style=invis];
StageDeployment:s -> FinalizeDeployment:n [style=invis];
}
# Organize nodes in rows.
{ rank = same; InvisMetricsClient; SyncLastRefersh; LastRefresh; AsyncLocalDeployments; QueryLocalDeployments }
{ rank = same; ConnectionStream; RefreshTick; AsyncAttemptDeploy; StageDeployment }
{ rank = same; InvisBottomMetrics; InvisBottomDbus; AsyncFinalizeDeployment; FinalizeDeployment; }
# Edges.
InvisMetricsClient:s -> ConnectionStream:n [label="Metrics\nsocket\n connection"];
RefreshTick:ne -> RefreshTick:se;
{ rank = same; SyncLastRefersh:ne -> LastRefresh:nw; LastRefresh:sw -> SyncLastRefersh:se; }
{ rank = same; AsyncLocalDeployments:ne -> QueryLocalDeployments:nw; QueryLocalDeployments:sw -> AsyncLocalDeployments:se; }
{ rank = same; AsyncAttemptDeploy:ne -> StageDeployment:nw; StageDeployment:sw -> AsyncAttemptDeploy:se; }
{ rank = same; AsyncFinalizeDeployment:ne -> FinalizeDeployment:nw; FinalizeDeployment:sw -> AsyncFinalizeDeployment:se; }
}
================================================
FILE: docs/images/zincati-fleetlock.msc
================================================
# Render with: `mscgen -T svg -i zincati-fleetlock.msc`
msc {
"OS", "Zincati agent", "FleetLock service", "rpm-ostree daemon";
...;
|||;
"Zincati agent" rbox "FleetLock service" [label="Reboot slot locked\n(possibly, from previous update)\n", textbgcolour="#cecece"],
|||;
|||;
--- [label="System boot"],
"OS" -> "Zincati agent" [label="Start zincati.service", arcskip=1];
|||;
"Zincati agent" => "FleetLock service" [label="POST /v1/steady-state", arcskip=1];
|||;
"Zincati agent" note "FleetLock service" [label="(Keep trying until steady-state is acknowledged...)\n"];
"FleetLock service" >> "Zincati agent" [label="OK", arcskip=1];
|||;
"Zincati agent" rbox "Zincati agent" [label="Released any owned reboot slot", textbgcolour="#46b8e3"],
"FleetLock service" rbox "FleetLock service" [label="Locked reboot slots: 0", textbgcolour="#ff7f7f"],
|||;
|||;
... [label="New update target found (TargetRevision)"];
|||;
"Zincati agent" => "rpm-ostree daemon" [label="Deploy(TargetRevision)", arcskip=1];
|||;
"rpm-ostree daemon" >> "Zincati agent" [label="OK", arcskip=1];
|||;
"Zincati agent" => "FleetLock service" [label="POST /v1/pre-reboot", arcskip=1];
|||;
"Zincati agent" note "FleetLock service" [label="(Keep trying until a reboot slot is available...)\n"];
"FleetLock service" >> "Zincati agent" [label="OK", arcskip=1];
|||;
"Zincati agent" rbox "Zincati agent" [label="Owning a reboot slot", textbgcolour="#46b8e3"],
"FleetLock service" rbox "FleetLock service" [label="Locked reboot slots: 1", textbgcolour="#ff7f7f"],
|||;
|||;
"Zincati agent" => "rpm-ostree daemon" [label="Finalize(TargetRevision)", arcskip=1];
|||;
"rpm-ostree daemon" >> "Zincati agent" [label="OK", arcskip=1],
|||;
"rpm-ostree daemon" => "OS" [label="Reboot", arcskip=2];
|||;
--- [label="System reboot"];
|||;
"Zincati agent" rbox "FleetLock service" [label="Reboot slot locked\n(for current update)\n", textbgcolour="#cecece"];
...;
}
================================================
FILE: docs/images/zincati-fsm.dot
================================================
# Render with: `dot -T png -o zincati-fsm.png zincati-fsm.dot`
# The `dot` program is included in Graphviz: https://graphviz.org/download/
digraph finite_state_machine {
rankdir=LR;
node [shape=circle, fontsize=10, fixedsize=true, width=1.1];
edge [fontsize=10, fixedsize=true];
node [label="StartState"] StartState;
node [label="Initialized"] Initialized;
node [label="ReportedSteady"] ReportedSteady;
node [label="NoNewUpdate"] NoNewUpdate;
node [label="UpdateAvailable"] UpdateAvailable;
node [label="UpdateStaged"] UpdateStaged;
node [label="UpdateFinalized"] UpdateFinalized;
node [shape = doublecircle, label="EndState"] EndState;
StartState -> Initialized [label="initialized()"];
StartState -> EndState [label="end()"];
Initialized -> ReportedSteady [label="reported_steady()"];
ReportedSteady -> NoNewUpdate [label="no_new_update()"];
ReportedSteady -> UpdateAvailable [label="update_available()"];
NoNewUpdate -> NoNewUpdate [label="no_new_update()"];
NoNewUpdate -> UpdateAvailable [label="update_available()"];
UpdateAvailable -> UpdateAvailable [label="deploy_failed()"];
UpdateAvailable -> NoNewUpdate [label="update_abandoned()"];
UpdateAvailable -> UpdateStaged [label="update_staged()"];
UpdateStaged -> UpdateFinalized [label="update_finalized()"];
UpdateStaged -> UpdateStaged [label="reboot_postponed()"];
UpdateFinalized -> EndState [label="end()"];
}
================================================
FILE: docs/index.md
================================================
---
nav_order: 1
---
# Zincati
[](https://crates.io/crates/zincati)
Zincati is an auto-update agent for Fedora CoreOS hosts.
It works as a client for [Cincinnati] and [rpm-ostree], taking care of automatically updating/rebooting machines.
Features:
* Agent for [continuous auto-updates][auto-updates], with support for phased rollouts
* [Configuration][configuration] via TOML dropins and overlaid directories
* Multiple [update strategies][updates-strategy] for finalization/reboot
* Local [maintenance windows][strategy-periodic] on a weekly schedule for planned upgrades
* Internal [metrics][metrics] exposed over a local endpoint in Prometheus format
* [Logging][logging] with configurable priority levels
* Support for complex update-graphs via [Cincinnati protocol][cincinnati-protocol] (with rollout wariness, barriers, dead-ends and more)
* Support for [cluster-wide reboot orchestration][strategy-fleetlock], via an external lock-manager
{:width="720px"}
[Cincinnati]: https://github.com/openshift/cincinnati
[rpm-ostree]: https://github.com/coreos/rpm-ostree
[auto-updates]: usage/auto-updates
[configuration]: usage/configuration
[updates-strategy]: usage/updates-strategy
[strategy-periodic]: usage/updates-strategy#periodic-strategy
[metrics]: usage/metrics
[logging]: usage/logging
[cincinnati-protocol]: development/cincinnati/protocol
[strategy-fleetlock]: usage/updates-strategy#lock-based-strategy
================================================
FILE: docs/usage/agent-identity.md
================================================
---
parent: Usage
---
# Agent identity
Zincati agent tries to derive a unique identity for the machine it is running on by introspecting the underlying OS and reading user configuration.
This includes assigning an ID and a group label specific to the agent, so that cluster-wide upgrades can be orchestrated via [phased rollouts][phased] and [lock-based][fleetlock-strategy] reboots.
[phased]: auto-updates.md#phased-rollouts-client-wariness-canaries
[fleetlock-strategy]: updates-strategy.md#lock-based-strategy
## Identity configuration
All agent identity values are normally auto-detected at startup and do not require user intervention.
However, the following settings can be overridden through configuration fragments in the `identity` section:
* `group`: group label, used for graph fetching ([Cincinnati][cincinnati]) and reboot orchestration ([FleetLock][fleetlock])
* `node_uuid`: agent ID, used for graph fetching ([Cincinnati][cincinnati]) and reboot orchestration ([FleetLock][fleetlock])
* `rollout_wariness`: agent wariness to [phased rollouts][phased], used for graph fetching ([Cincinnati][cincinnati]).
The following are defaults for each setting:
- `group` (group label) is set to `default`
- `node_uuid` (agent ID) is automatically generated, by hashing `/etc/machine-id` content
- `rollout_wariness` is unset and the Cincinnati backend will assign a dynamic value to each request
When the agent ID is not customized via configuration fragments, its default value is dynamically generated starting from `/etc/machine-id` content and from a Zincati specific application ID.
For more details about such application-specific machine IDs, see [machine-id][machine-id] documentation.
[machine-id]: https://www.freedesktop.org/software/systemd/man/machine-id.html
## Example
As an example, users can specify custom identity parameters by writing a configuration fragment to `/etc/lib/zincati/config.d/90-custom-identity.toml`:
```toml
[identity]
group = "workers"
```
The fragment above will steer the node into the "workers" reboot group.
[cincinnati]: ../development/cincinnati/protocol.md
[fleetlock]: ../development/fleetlock/protocol.md
================================================
FILE: docs/usage/auto-updates.md
================================================
---
parent: Usage
---
# Auto-updates
Available updates are discovered by periodically polling a [Cincinnati] server.
Once available, they are automatically applied via [rpm-ostree] and a machine reboot.
[Cincinnati]: https://github.com/openshift/cincinnati
[rpm-ostree]: https://github.com/projectatomic/rpm-ostree
## Phased rollouts, client wariness, canaries
Once a new update payload is officially released, Zincati will eventually detect and apply the update automatically.
However, there is no strict guarantee on the timing for an individual node to detect a new release, as the server will try to spread updates over a controlled timeframe.
This mechanism is called "phased rollout" and is meant to help release engineers and administrators in performing gradual updates and catching last-minute issues before they propagate to a large number of machines.
Phased rollouts are orchestrated by the Cincinnati backend, by adjusting over time the percentage of clients to which an update is offered.
Clients do not usually need any additional setup to leverage phased rollouts.
By default, the Cincinnati backend dynamically assigns a specific rollout score to each client.
However, clients can provide a "rollout wariness" hint to the server, in order to specify how eager they are to receive new updates.
The rollout wariness hint is configurable through the `rollout_wariness` parameter, as a floating point number going from `1.0` (very cautious) to `0.0` (very eager).
For example, a mildly cautious node can be configured using a configuration snippet like this:
```toml
[identity]
rollout_wariness = 0.5
```
A common case is to have few dedicated nodes, also known as "canaries", that are configured to be very eager to receive updates, with a rollout wariness equal to `0.0`.
Those nodes are meant to receive updates as soon as they are available, can afford some downtime, and are specifically monitored in order to detect issues before they start affecting a larger fleet of machines.
It is recommended to setup and monitor canary nodes, but otherwise normal worker nodes should not have zero wariness.
The default and recommended configuration does not set any static wariness value on Zincati side, leaving rollout decisions to Cincinnati backend.
## Strategies for updates finalization
Zincati actively tries to detect and stage new updates whenever they become available.
Once a new payload has been locally staged, a machine reboot is required in order to atomically apply the update to the system as a whole.
Rebooting a machine does affect any workloads running on the machine at that time, and can potentially impact services across a whole cluster of nodes.
For such reason, Zincati allows the user to control when a node is allowed to reboot to finalize an auto-update.
The following finalization strategies are currently available:
* immediately reboot to apply an update, as soon as it is downloaded and staged locally (`immediate` strategy, see [relevant documentation][strategy-immediate]).
* use an external lock-manager to reboot a fleet of machines in a coordinated way (`fleet_lock` strategy, see [relevant documentation][strategy-fleet_lock]).
* allow reboots only within locally configured maintenance windows, defined on a weekly basis (`periodic` strategy, see [relevant documentation][strategy-periodic]).
By default, the `immediate` strategy is used in order to proactively keep machines up-to-date.
For further documentation on configurations, check the [updates strategy][updates-strategy] documentation.
[strategy-immediate]: updates-strategy.md#immediate-strategy
[strategy-fleet_lock]: updates-strategy.md#lock-based-strategy
[strategy-periodic]: updates-strategy.md#periodic-strategy
[updates-strategy]: updates-strategy.md
## Updates ordering and downgrades
OS updates have a strict ascending ordering called "age index", which is based on the date and time of release.
Versions that have been released earlier in time have a lower index than recent ones.
Zincati uses this absolute ordering to prefer newer releases (i.e. with higher age index) when multiple updates are available at the same time.
By default, this ordering is also used to prevent automatic downgrades.
For custom environments where automatic downgrades have to be supported, the following configuration snippet can be used to enable them:
```toml
[updates]
allow_downgrade = true
```
Enabling such logic removes an additional safety check, and may allow rogue Cincinnati servers to induce downgrades to old releases with known security vulnerabilities.
It is generally not recommended to allow and perform automatic downgrades via Zincati.
## Disabling auto-updates
To disable auto-updates, a configuration snippet containing the following has to be installed on the system:
```toml
[updates]
enabled = false
```
Make sure that it has higher priority than previous settings, by using a path like `/etc/zincati/config.d/90-disable-auto-updates.toml`.
When auto-updates are disabled, Zincati does not perform any update action.
However, the service does not terminate and is kept alive idle for external status observers.
================================================
FILE: docs/usage/configuration.md
================================================
---
parent: Usage
---
# Configuration
Zincati supports runtime customization via configuration fragments (dropins), allowing users and distributions to tweak the agent behavior by writing plain-text files.
Each configuration fragment is a TOML snippet which is read by Zincati and assembled into the final runtime configuration. Only files with a `.toml` extension are considered.
Dropins are sourced from multiple directories, and merged by filename in lexicographic order.
The following configuration paths are scanned by Zincati, in order:
* `/usr/lib/zincati/config.d/`: distribution defaults, read-only path owned by the OS.
* `/etc/zincati/config.d/`: user customizations, writable path owned by the system administrator.
* `/run/zincati/config.d/`: runtime customizations, writable path that is not persisted across reboots.
Configuration directives from files that appear later in sorting order can override prior directives.
If multiple files with the same name exist, only the last-sorting one is read.
Additionally, symbolic links to `/dev/null` can be used to completely override a prior file with the same name.
Configuration dropins are organized in multiple TOML sections, which are described in details in their own documentation pages.
## Example
As an example, distribution defaults may generically enable a feature, but users may need to disable that in specific case.
To that extent, distributions can provide by default the following content at `/usr/lib/zincati/config.d/10-enable-feature.toml`:
```toml
[feature]
enabled = true
```
In order to override that setting, users can write the following to `/etc/zincati/config.d/90-disable-feature.toml`:
```toml
[feature]
enabled = false
```
After sorting all configuration directives by directory and filename priority, the user-provided dropin is considered with the highest priority. Thus, it will override any conflicting directives from other fragments.
================================================
FILE: docs/usage/logging.md
================================================
---
parent: Usage
---
# Logging
Zincati supports logging at multiple levels (trace, debug, info, warning, error). Usually only log messages at or above warning level are emitted.
Log verbosity can be increased by passing multiple `-v` flags as command-line arguments.
## Tweaking agent verbosity
By default, the Zincati agent is started with info level logging enabled (i.e. `-v`). However, logging verbosity can be freely tweaked via systemd drop-in files.
For example, debug logging (`-vv`) can be enabled by creating a drop-in file at `/etc/systemd/system/zincati.service.d/10-verbosity.conf` with the following contents:
```
[Service]
Environment=ZINCATI_VERBOSITY="-vv"
```
The maximum level (`-vvv`) equates to trace and can be very verbose. It is only meant for development/debugging and for short timespans.
It is recommended to not use the trace log level in production or for long periods of time as it reduces the signal-to-noise ratio and can easily saturate further log-persisting systems.
## Inspecting logs
By default Zincati runs as a systemd service, and its log messages are captured by systemd-journald.
Most recent logs can be inspected via `sudo journalctl -b 0 -e -u zincati.service`. The resulting output may look like this:
```
-- Logs begin at Sat 2020-09-12 16:12:13 UTC, end at Wed 2020-09-30 12:52:05 UTC. --
Sep 23 10:48:27 localhost systemd[1]: Started Zincati Update Agent.
Sep 23 10:48:27 localhost zincati[678]: [INFO ] starting update agent (zincati 0.0.12)
Sep 23 10:48:34 localhost zincati[678]: [INFO ] Cincinnati service: https://updates.coreos.fedoraproject.org
Sep 23 10:48:34 localhost zincati[678]: [INFO ] agent running on node '<ID>', in update group '<GROUP>'
Sep 23 10:48:34 localhost zincati[678]: [INFO ] initialization complete, auto-updates logic enabled
...
```
Optionally, `journalctl` allows to follow log messages emitted in real time by additionally passing a `-f` flag.
================================================
FILE: docs/usage/metrics.md
================================================
---
parent: Usage
---
# Metrics
Zincati tracks and exposes some of its internal metrics, in order to ease monitoring tasks across a large fleet of nodes.
Metrics are collected and exported according to [Prometheus][Prometheus] [textual format][prom-text], over a local endpoint.
[Prometheus]: https://prometheus.io/
[prom-text]: https://prometheus.io/docs/instrumenting/exposition_formats/
## Gathering metrics
To gather metrics from a locally running Zincati instance, it is sufficient to connect and read from the Unix-domain socket located at `/run/zincati/public/metrics.promsock`.
For example, manual inspection can be performed via `socat`:
```
$ sudo socat - UNIX-CONNECT:/run/zincati/public/metrics.promsock
# HELP zincati_update_agent_last_refresh_timestamp UTC timestamp of update-agent last refresh tick.
# TYPE zincati_update_agent_last_refresh_timestamp gauge
zincati_update_agent_last_refresh_timestamp 1563360122
# HELP zincati_update_agent_latest_state_change_timestamp UTC timestamp of update-agent last state change.
# TYPE zincati_update_agent_latest_state_change_timestamp gauge
zincati_update_agent_latest_state_change_timestamp 1563360122
# HELP zincati_update_agent_updates_enabled Whether auto-updates logic is enabled.
# TYPE zincati_update_agent_updates_enabled gauge
zincati_update_agent_updates_enabled 1
[...]
```
Additionally, the local Unix-domain socket can be proxied to HTTP and exposed to Prometheus.
For an example of such setup, check the [local\_exporter][local_exporter] repository.
[local_exporter]: https://github.com/lucab/local_exporter
================================================
FILE: docs/usage/updates-strategy.md
================================================
---
parent: Usage
---
# Updates strategy
To minimize service disruption, Zincati allows administrators to control when machines are allowed to reboot and finalize auto-updates.
Several updates strategies are supported, which can be configured at runtime via configuration snippets as shown below.
If not otherwise configured, the default updates strategy resolves to `immediate`.
# Immediate strategy
The simplest updates strategy consists of minimal logic to immediately finalize an update as soon as it is staged locally.
For configuration purposes, such strategy is labeled `immediate` and takes no additional configuration parameters.
This strategy can be enabled via a configuration snippet like the following:
```toml
[updates]
strategy = "immediate"
```
The `immediate` strategy is an aggressive finalization method which is biased towards finalizing updates as soon as possible, and it is only aware of node-local state.
Such an approach is only recommended for environments where temporary service interruption are not problematic, or there is no need for more complex reboot scheduling.
# Lock-based strategy
In case of a fleet of machines grouped into a cluster, it is often required to orchestrate reboots so that hosted services are not disrupted when single nodes are rebooting to finalize updates.
In this case it is helpful to have an external orchestrator managing reboots cluster-wide, and having each machine trying to lock (and unlock) a reboot slot with the centralized lock-manager.
Several distributed databases and lock-managers exist for such purpose, each one with a specific remote API for clients and a variety of transport mechanisms.
Zincati does not mandate any specific lock-manager or database, but instead it uses a simple HTTP-based protocol modeling a distributed counting semaphore with recursive locking, called [FleetLock][fleet_lock],
In short, it consists of two operations:
* lock: before rebooting, a reboot slot must be locked (and confirmed) by the lock-manager.
* unlock: after rebooting, any reboot slot owned by the node must be unlocked (and confirmed) by the lock-manager before proceeding further.
This protocol is not coupled to any specific backend, and can be implemented on top of any suitable infrastructure:
* [airlock] is a free-software project which implements such protocol on top of [etcd3].
* a Kubernetes-based reboot-manager is provided as part of [Typhoon](https://github.com/poseidon/fleetlock).
* <https://github.com/opencounter/terraform-fleet-lock-dynamodb> is a serverless implementation via AWS API Gateway and DynamoDB.
For configuration purposes, such strategy is labeled `fleet_lock` and takes the following configuration parameters:
* `base_url` (string, mandatory, non-empty): the base URL for the FleetLock service.
This strategy can be enabled via a configuration snippet like the following:
```toml
[updates]
strategy = "fleet_lock"
[updates.fleet_lock]
base_url = "http://example.com/fleet_lock/"
```
The `fleet_lock` strategy is a conservative method which is biased towards avoiding service disruptions, but it requires an external component which is aware of cluster-wide state.
Such an approach is only recommended where nodes are already grouped into an orchestrated cluster, which can thus provide better overall scheduling decisions.
[fleet_lock]: ../development/fleetlock/protocol.md
[airlock]: https://github.com/coreos/airlock
[etcd3]: https://etcd.io/
# Periodic strategy
The `periodic` strategy allows Zincati to only reboot for updates during certain timeframes, also known as "maintenance windows" or "reboot windows".
Outside of those maintenance windows, reboots are not automatically performed and auto-updates are staged and held until the next available window.
Reboot windows recur on a weekly basis, and can be defined in any arbitrary order and length. Their individual length must be greater than zero.
By default, all maintenance windows are defined in UTC dates and times. This is meant to avoid timezone-related skews in a fleet of machines, as well as possible side-effects of Daylight Savings Time (DST) policies.
Periodic reboot windows can be configured and enabled in the following way:
```toml
[updates]
strategy = "periodic"
[[updates.periodic.window]]
days = [ "Sat", "Sun" ]
start_time = "23:30"
length_minutes = 60
[[updates.periodic.window]]
days = [ "Wed" ]
start_time = "01:00"
length_minutes = 30
```
The above configuration would result in three maintenance windows during which Zincati is allowed to reboot the machine for updates:
* 60 minutes starting at 23:30 UTC on Saturday night, and ending at 00:30 UTC on Sunday morning
* 60 minutes starting at 23:30 UTC on Sunday night, and ending at 00:30 UTC on Monday morning
* 30 minutes starting at 01:00 UTC on Wednesday morning, and ending at 01:30 UTC on Wednesday morning
Reboot windows can be separately configured in multiple snippets, as long as each `updates.periodic.window` entry contains all the required properties:
* `days`: an array of weekdays (C locale), either in full or abbreviated (first three letters) form
* `start_time`: window starting time, in `hh:mm` ISO 8601 format
* `length_minutes`: non-zero window duration, in minutes
For convenience, multiple entries can be defined with overlapping times, and each window definition is allowed to cross day and week boundaries (wrapping to the next day).
## Time zone configuration
To configure a non-UTC time zone for all the reboot windows, specify the `time_zone` field in a `updates.periodic` entry. The specified time zone must be either `"localtime"` or a time zone name from the [IANA Time Zone Database][IANA_tz_db] (you can find an unofficial list of time zone names [here][wikipedia_tz_names]).
If using `"localtime"`, the system's [local time zone configuration file][localtime], `/etc/localtime`, is used. As such, `/etc/localtime` must either be a symlink to a valid `tzfile` entry in your system's local time zone database (under `/usr/share/zoneinfo/`), or not exist, in which case `UTC` is used.
Note that you can only specify a single time zone for _all_ reboot windows.
A time zone can be specified in the following way:
```toml
[updates]
strategy = "periodic"
[updates.periodic]
time_zone = "America/Panama"
[[updates.periodic.window]]
days = [ "Sat", "Sun" ]
start_time = "23:30"
length_minutes = 60
[[updates.periodic.window]]
days = [ "Mon" ]
start_time = "00:00"
length_minutes = 60
```
Since Panama does not have Daylight Savings Time and follows Eastern Standard Time (which has a fixed offset of UTC -5) all year, the above configuration would result in two maintenance windows during which Zincati is allowed to reboot the machine for updates:
* 60 minutes starting at 23:30 EST on Saturday night, and ending at 00:30 EST on Sunday morning
* 90 minutes starting at 23:30 EST on Sunday night, and ending at 01:00 EST on Monday morning
### Time zone caveats
⚠️ **Reboot window lengths may vary.** ⚠️
Because reboot window clock times are always obeyed, reboot windows may be lengthened or shortened due to shifts in clock time. For example, with the `US/Eastern` time zone which shifts between Eastern Standard Time and Eastern Daylight Time, on "fall back" day, a specified reboot window may be lengthened by up to one hour; on "spring forward" day, a specified reboot window may be shortened by up to one hour, or skipped entirely.
Example of varying length reboot windows using the `US/Eastern` time zone:
```toml
[updates]
strategy = "periodic"
[updates.periodic]
time_zone = "US/Eastern"
[[updates.periodic.window]]
days = [ "Sun" ]
start_time = "01:30"
length_minutes = 60
```
The above configuration will result in reboots being allowed at 1:30 AM to 2:30 AM on _every_ Sunday. This includes days when a Daylight Savings Shift occurs.
On the `US/Eastern` time zone's "fall back" day, where clocks are shifted back by one hour on a Sunday in Fall just before 3:00 AM, the thirty minutes between 2:00 AM and 2:30 AM will occur twice. As such, the reboot window will be lengthened by thirty minutes each year on "fall back" day.
On "spring forward" day, where clocks are shifted forward by one hour on a Sunday in Spring just before 2:00 AM, the thirty minutes between 2:00 AM and 2:30 AM will not occur. As such, the reboot window will be shortened by thirty minutes each year on "spring forward" day. Effectively, the reboot window on "spring forward" day will only be between 1:30 AM and 2:00 AM.
⚠️ **Incorrect reboot times due to stale time zone database.** ⚠️
Time zone data is read from the system's time zone database at `/usr/share/zoneinfo`. This directory and its contents are part of the `tzdata` RPM package; in the latest release of Fedora CoreOS, `tzdata` should be kept fairly up-to-date with the latest official release from the IANA.
However, if your system does not have the latest IANA time zone database, or there is a sudden policy change in the jurisdiction associated with your configured time zone, then reboots may happen at unexpected and incorrect times.
[IANA_tz_db]: https://www.iana.org/time-zones
[wikipedia_tz_names]: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
[localtime]: https://www.freedesktop.org/software/systemd/man/localtime.html
================================================
FILE: docs/usage.md
================================================
---
nav_order: 2
has_children: true
---
# Usage
================================================
FILE: src/cincinnati/client.rs
================================================
//! Asynchronous Cincinnati client.
//!
//! This client implements the [Cincinnati protocol] for update-hints.
//!
//! [Cincinnati protocol]: https://github.com/openshift/cincinnati/blob/master/docs/design/cincinnati.md#graph-api
// TODO(lucab): eventually move to its own "cincinnati client library" crate
use anyhow::{Context, Result};
use futures::prelude::*;
use reqwest::Method;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::time::Duration;
use thiserror::Error;
/// Default timeout for HTTP requests completion (30 minutes).
const DEFAULT_HTTP_COMPLETION_TIMEOUT: Duration = Duration::from_secs(30 * 60);
/// Cincinnati graph API path endpoint (v1).
static V1_GRAPH_PATH: &str = "v1/graph";
/// Cincinnati JSON protocol: node object.
#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
pub struct Node {
pub version: String,
pub payload: String,
pub metadata: HashMap<String, String>,
}
/// Cincinnati JSON protocol: graph object.
#[derive(Debug, Deserialize, PartialEq, Eq)]
pub struct Graph {
pub nodes: Vec<Node>,
pub edges: Vec<(u64, u64)>,
}
/// Cincinnati JSON protocol: service error.
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
pub struct GraphJsonError {
/// Machine-friendly brief error kind.
pub(crate) kind: String,
/// Human-friendly detailed error explanation.
pub(crate) value: String,
}
/// Error related to the Cincinnati service.
#[derive(Clone, Debug, Error, PartialEq, Eq)]
pub enum CincinnatiError {
/// Graph endpoint error.
Graph(reqwest::StatusCode, GraphJsonError),
/// Generic HTTP error.
Http(reqwest::StatusCode),
/// Client builder failed.
FailedClientBuilder(String),
/// Client failed JSON decoding.
FailedJsonDecoding(String),
/// Failed to lookup node in graph.
FailedNodeLookup(String),
/// Failed parsing node from graph.
FailedNodeParsing(String),
/// Client failed request.
FailedRequest(String),
}
impl CincinnatiError {
/// Return the machine-friendly brief error kind.
pub fn error_kind(&self) -> String {
match *self {
CincinnatiError::Graph(_, ref err) => err.kind.clone(),
CincinnatiError::Http(status) => format!("generic_http_{}", status.as_u16()),
CincinnatiError::FailedClientBuilder(_) => "client_failed_build".to_string(),
CincinnatiError::FailedJsonDecoding(_) => "client_failed_json_decoding".to_string(),
CincinnatiError::FailedNodeLookup(_) => "client_failed_node_lookup".to_string(),
CincinnatiError::FailedNodeParsing(_) => "client_failed_node_parsing".to_string(),
CincinnatiError::FailedRequest(_) => "client_failed_request".to_string(),
}
}
/// Return the human-friendly detailed error explanation.
pub fn error_value(&self) -> String {
match *self {
CincinnatiError::Graph(_, ref err) => err.value.clone(),
CincinnatiError::Http(_) => "(unknown/generic server error)".to_string(),
CincinnatiError::FailedClientBuilder(ref err)
| CincinnatiError::FailedJsonDecoding(ref err)
| CincinnatiError::FailedNodeLookup(ref err)
| CincinnatiError::FailedNodeParsing(ref err)
| CincinnatiError::FailedRequest(ref err) => err.clone(),
}
}
/// Return the server-side error status code, if any.
pub fn status_code(&self) -> Option<u16> {
match *self {
CincinnatiError::Graph(s, _) | CincinnatiError::Http(s) => Some(s.as_u16()),
_ => None,
}
}
}
impl std::fmt::Display for CincinnatiError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
// Account for both server-side and client-side failures.
let context = match self.status_code() {
Some(s) => format!("server-side error, code {}", s),
None => "client-side error".to_string(),
};
write!(f, "{}: {}", context, self.error_value())
}
}
/// Client to make outgoing API requests.
#[derive(Clone, Debug)]
pub struct Client {
/// Base URL for API endpoint.
api_base: reqwest::Url,
/// Asynchronous reqwest client.
hclient: reqwest::Client,
/// Client parameters (query portion).
query_params: HashMap<String, String>,
}
impl Client {
/// Fetch an update-graph from Cincinnati.
pub fn fetch_graph(&self) -> impl Future<Output = Result<Graph, CincinnatiError>> {
let req = self
.new_request(Method::GET, V1_GRAPH_PATH)
.map_err(|e| CincinnatiError::FailedRequest(e.to_string()));
futures::future::ready(req)
.and_then(|req| {
req.send()
.map_err(|e| CincinnatiError::FailedRequest(e.to_string()))
})
.and_then(Self::map_response)
}
/// Return a request builder with base URL and parameters set.
fn new_request<S: AsRef<str>>(
&self,
method: reqwest::Method,
url_suffix: S,
) -> Result<reqwest::RequestBuilder> {
let url = self.api_base.clone().join(url_suffix.as_ref())?;
let builder = self
.hclient
.request(method, url)
.header("accept", "application/json")
.query(&self.query_params);
Ok(builder)
}
/// Map an HTTP response to a service result.
async fn map_response(response: reqwest::Response) -> Result<Graph, CincinnatiError> {
let status = response.status();
// On success, try to decode graph.
if status.is_success() {
let graph = response.json::<Graph>().await.map_err(|e| {
CincinnatiError::FailedJsonDecoding(format!("failed to decode graph: {}", e))
})?;
return Ok(graph);
}
// On error, decode failure details (or synthesize a generic error).
match response.json::<GraphJsonError>().await {
Ok(rej) => Err(CincinnatiError::Graph(status, rej)),
_ => Err(CincinnatiError::Http(status)),
}
}
}
/// Client builder.
#[derive(Clone, Debug)]
pub struct ClientBuilder {
/// Base URL for API endpoint (mandatory).
api_base: String,
/// Asynchronous reqwest client (custom).
hclient: Option<reqwest::Client>,
/// Client parameters (custom).
query_params: Option<HashMap<String, String>>,
}
impl ClientBuilder {
/// Return a new builder for the given base API endpoint URL.
pub fn new<T>(api_base: T) -> Self
where
T: Into<String>,
{
Self {
api_base: api_base.into(),
hclient: None,
query_params: None,
}
}
/// Set (or reset) the query parameters to use.
pub fn query_params(self, params: Option<HashMap<String, String>>) -> Self {
let mut builder = self;
builder.query_params = params;
builder
}
/// Build a client with specified parameters.
pub fn build(self) -> Result<Client> {
let hclient = match self.hclient {
Some(client) => client,
None => reqwest::ClientBuilder::new()
.timeout(DEFAULT_HTTP_COMPLETION_TIMEOUT)
.build()?,
};
let query_params = self.query_params.unwrap_or_default();
let api_base = reqwest::Url::parse(&self.api_base)
.context(format!("failed to parse '{}'", &self.api_base))?;
let client = Client {
api_base,
hclient,
query_params,
};
Ok(client)
}
}
#[cfg(test)]
mod tests {
use super::*;
use http::response::Response;
use http::status::StatusCode;
use tokio::runtime as rt;
#[test]
fn test_graph_server_error_display() {
let err_body = r#"
{
"kind": "failure_foo",
"value": "failed to perform foo"
}
"#;
let runtime = rt::Runtime::new().unwrap();
let response = Response::builder().status(466).body(err_body).unwrap();
let fut_rejection = Client::map_response(response.into());
let rejection = runtime.block_on(fut_rejection).unwrap_err();
let expected_rejection = CincinnatiError::Graph(
StatusCode::from_u16(466).unwrap(),
GraphJsonError {
kind: "failure_foo".to_string(),
value: "failed to perform foo".to_string(),
},
);
assert_eq!(&rejection, &expected_rejection);
let msg = rejection.to_string();
let expected_msg = "server-side error, code 466: failed to perform foo";
assert_eq!(&msg, expected_msg);
}
#[test]
fn test_graph_http_error_display() {
let runtime = rt::Runtime::new().unwrap();
let response = Response::builder().status(433).body("").unwrap();
let fut_rejection = Client::map_response(response.into());
let rejection = runtime.block_on(fut_rejection).unwrap_err();
let expected_rejection = CincinnatiError::Http(StatusCode::from_u16(433).unwrap());
assert_eq!(&rejection, &expected_rejection);
let msg = rejection.to_string();
let expected_msg = "server-side error, code 433: (unknown/generic server error)";
assert_eq!(&msg, expected_msg);
}
#[test]
fn test_graph_client_error_display() {
let runtime = rt::Runtime::new().unwrap();
let response = Response::builder().status(200).body("{}").unwrap();
let fut_rejection = Client::map_response(response.into());
let rejection = runtime.block_on(fut_rejection).unwrap_err();
let expected_rejection = CincinnatiError::FailedJsonDecoding(
"failed to decode graph: error decoding response body".to_string(),
);
assert_eq!(&rejection, &expected_rejection);
let msg = rejection.to_string();
let expected_msg =
"client-side error: failed to decode graph: error decoding response body";
assert_eq!(&msg, expected_msg);
}
}
================================================
FILE: src/cincinnati/mock_tests.rs
================================================
use crate::cincinnati::*;
use crate::identity::Identity;
use mockito::{self, Matcher};
use std::collections::BTreeSet;
use tokio::runtime as rt;
#[test]
fn test_empty_graph() {
let mut server = mockito::Server::new();
let empty_graph = r#"{ "nodes": [], "edges": [] }"#;
let m_graph = server
.mock("GET", Matcher::Regex(r"^/v1/graph?.+$".to_string()))
.with_status(200)
.with_header("accept", "application/json")
.with_body(empty_graph)
.create();
let runtime = rt::Runtime::new().unwrap();
let id = Identity::mock_default();
let client = Cincinnati {
base_url: server.url(),
};
let update = runtime.block_on(client.next_update(&id, BTreeSet::new(), false));
m_graph.assert();
assert!(update.unwrap().is_none());
}
================================================
FILE: src/cincinnati/mod.rs
================================================
//! Asynchronous Cincinnati client.
// Cincinnati client.
mod client;
pub use client::{CincinnatiError, Node};
#[cfg(test)]
mod mock_tests;
use crate::config::inputs;
use crate::identity::Identity;
use crate::rpm_ostree::{Payload, Release};
use anyhow::{Context, Result};
use fn_error_context::context;
use futures::prelude::*;
use futures::TryFutureExt;
use prometheus::{IntCounter, IntCounterVec, IntGauge};
use serde::Serialize;
use std::collections::BTreeSet;
use std::pin::Pin;
use std::sync::atomic::{AtomicU8, Ordering};
/// Metadata key for payload scheme.
pub static AGE_INDEX_KEY: &str = "org.fedoraproject.coreos.releases.age_index";
/// Metadata key for payload scheme.
pub static SCHEME_KEY: &str = "org.fedoraproject.coreos.scheme";
/// Metadata key for dead-end sentinel.
pub static DEADEND_KEY: &str = "org.fedoraproject.coreos.updates.deadend";
/// Metadata key for dead-end reason.
pub static DEADEND_REASON_KEY: &str = "org.fedoraproject.coreos.updates.deadend_reason";
/// Metadata value for "oci" payload scheme.
pub const OCI_SCHEME: &str = "oci";
lazy_static::lazy_static! {
static ref GRAPH_NODES: IntGauge = register_int_gauge!(opts!(
"zincati_cincinnati_graph_nodes_count",
"Number of nodes in Cincinnati update graph."
)).unwrap();
static ref GRAPH_EDGES: IntGauge = register_int_gauge!(opts!(
"zincati_cincinnati_graph_edges_count",
"Number of edges in Cincinnati update graph."
)).unwrap();
static ref BOOTED_DEADEND: IntGauge = register_int_gauge!(
"zincati_cincinnati_booted_release_is_deadend",
"Whether currently booted OS release is a dead-end."
).unwrap();
static ref UPDATE_TARGETS_IGNORED: IntGauge = register_int_gauge!(
"zincati_cincinnati_ignored_update_targets",
"Number of ignored targets among update targets found."
).unwrap();
static ref UPDATE_CHECKS: IntCounter = register_int_counter!(opts!(
"zincati_cincinnati_update_checks_total",
"Total number of checks for updates to the upstream Cincinnati server."
)).unwrap();
static ref UPDATE_CHECKS_ERRORS: IntCounterVec = register_int_counter_vec!(
"zincati_cincinnati_update_checks_errors_total",
"Total number of errors while checking for updates.",
&["kind"]
).unwrap();
static ref DEADEND_STATE : DeadEndState = DeadEndState::default();
}
/// For tracking a dead-end release.
pub struct DeadEndState(AtomicU8);
impl Default for DeadEndState {
fn default() -> Self {
Self(AtomicU8::new(DeadEndState::UNKNOWN))
}
}
impl DeadEndState {
const FALSE: u8 = 0;
const TRUE: u8 = 1;
const UNKNOWN: u8 = 2;
/// Return whether this is in a known dead-end state.
pub fn is_deadend(&self) -> bool {
self.0.load(Ordering::SeqCst) == Self::TRUE
}
/// Return whether this is in a known NOT dead-end state.
pub fn is_no_deadend(&self) -> bool {
self.0.load(Ordering::SeqCst) == Self::FALSE
}
pub fn set_deadend(&self) {
self.0.store(Self::TRUE, Ordering::SeqCst);
}
pub fn set_no_deadend(&self) {
self.0.store(Self::FALSE, Ordering::SeqCst);
}
}
/// Cincinnati configuration.
#[derive(Debug, Serialize, Clone)]
pub struct Cincinnati {
/// Service base URL.
pub base_url: String,
}
impl Cincinnati {
/// Process Cincinnati configuration.
#[context("failed to validate cincinnati configuration")]
pub(crate) fn with_config(cfg: inputs::CincinnatiInput, id: &Identity) -> Result<Self> {
if cfg.base_url.is_empty() {
anyhow::bail!("empty Cincinnati base URL");
}
// Substitute templated key with agent runtime values.
let base_url = if envsubst::is_templated(&cfg.base_url) {
let context = id.url_variables();
envsubst::validate_vars(&context)?;
envsubst::substitute(cfg.base_url, &context)?
} else {
cfg.base_url
};
log::info!("Cincinnati service: {}", &base_url);
let c = Self { base_url };
Ok(c)
}
/// Fetch next update-hint from Cincinnati.
pub(crate) fn fetch_update_hint(
&self,
id: &Identity,
denylisted_depls: BTreeSet<Release>,
allow_downgrade: bool,
) -> Pin<Box<dyn Future<Output = Option<Release>>>> {
UPDATE_CHECKS.inc();
log::trace!("checking upstream Cincinnati server for updates");
let update = self
.next_update(id, denylisted_depls, allow_downgrade)
.unwrap_or_else(|e| {
UPDATE_CHECKS_ERRORS
.with_label_values(&[&e.error_kind()])
.inc();
log::error!("failed to check Cincinnati for updates: {}", e);
None
});
Box::pin(update)
}
/// Get the next update.
fn next_update(
&self,
id: &Identity,
denylisted_depls: BTreeSet<Release>,
allow_downgrade: bool,
) -> Pin<Box<dyn Future<Output = Result<Option<Release>, CincinnatiError>>>> {
let booted = id.current_os.clone();
let params = id.cincinnati_params();
let client = client::ClientBuilder::new(self.base_url.to_string())
.query_params(Some(params))
.build()
.map_err(|e| CincinnatiError::FailedClientBuilder(e.to_string()));
let next = futures::future::ready(client)
.and_then(|c| c.fetch_graph())
.and_then(move |graph| async move {
find_update(graph, booted, denylisted_depls, allow_downgrade)
});
Box::pin(next)
}
}
/// Evaluate and record whether booted OS is a dead-end release, and
/// log that information in a MOTD file.
fn refresh_deadend_status(node: &Node) -> Result<()> {
match evaluate_deadend(node) {
Some(reason) => {
BOOTED_DEADEND.set(1);
if !DEADEND_STATE.is_deadend() {
log::warn!("current release detected as dead-end, reason: {}", reason);
std::process::Command::new("pkexec")
.arg("/usr/libexec/zincati")
.arg("deadend-motd")
.arg("set")
.arg("--reason")
.arg(reason)
.output()
.context("failed to write dead-end release information")?;
DEADEND_STATE.set_deadend();
log::debug!("MOTD updated with dead-end state");
}
}
None => {
BOOTED_DEADEND.set(0);
if !DEADEND_STATE.is_no_deadend() {
log::info!("current release detected as not a dead-end");
std::process::Command::new("pkexec")
.arg("/usr/libexec/zincati")
.arg("deadend-motd")
.arg("unset")
.output()
.context("failed to remove dead-end release MOTD file")?;
DEADEND_STATE.set_no_deadend();
log::debug!("MOTD updated with no dead-end state");
}
}
};
Ok(())
}
/// Walk the graph, looking for an update reachable from the given digest.
fn find_update(
graph: client::Graph,
booted_depl: Release,
denylisted_depls: BTreeSet<Release>,
allow_downgrade: bool,
) -> Result<Option<Release>, CincinnatiError> {
GRAPH_NODES.set(graph.nodes.len() as i64);
GRAPH_EDGES.set(graph.edges.len() as i64);
log::trace!(
"got an update graph with {} nodes and {} edges",
graph.nodes.len(),
graph.edges.len()
);
// Find booted deployment in graph.
let (cur_position, cur_node) = match graph
.nodes
.iter()
.enumerate()
.find(|(_, node)| is_same_checksum(node, &booted_depl))
{
Some(current) => current,
None => {
log::warn!(
"booted deployment {} not found in the update graph",
&booted_depl.payload
);
return Ok(None);
}
};
drop(booted_depl);
let cur_release = Release::from_cincinnati(cur_node.clone())
.map_err(|e| CincinnatiError::FailedNodeParsing(e.to_string()))?;
if let Err(e) = refresh_deadend_status(cur_node) {
log::warn!("failed to refresh dead-end status: {}", e);
}
// Evaluate and record whether booted OS is a dead-end release.
// TODO(lucab): consider exposing this information in more places
// (e.g. logs, motd, env/json file in a well-known location).
let is_deadend: i64 = evaluate_deadend(cur_node).is_some().into();
BOOTED_DEADEND.set(is_deadend);
// Try to find all denylisted deployments in the graph too.
let denylisted_releases = find_denylisted_releases(&graph, denylisted_depls);
// Find all possible update targets from booted deployment.
let targets: Vec<_> = graph
.edges
.iter()
.filter_map(|(src, dst)| {
if *src == cur_position as u64 {
Some(*dst as usize)
} else {
None
}
})
.collect();
let mut updates = BTreeSet::new();
for pos in targets {
let node = match graph.nodes.get(pos) {
Some(n) => n.clone(),
None => {
let msg = format!("target node '{}' not present in graph", pos);
return Err(CincinnatiError::FailedNodeLookup(msg));
}
};
let release = Release::from_cincinnati(node)
.map_err(|e| CincinnatiError::FailedNodeParsing(e.to_string()))?;
updates.insert(release);
}
// Exclude targets in denylist.
let new_updates = updates.difference(&denylisted_releases);
// Log that we will avoid updating to denylisted releases.
let prev_deployed_excluded = updates.intersection(&denylisted_releases).count();
if prev_deployed_excluded > 0 {
log::debug!(
"Found {} possible update target{} present in denylist; ignoring",
prev_deployed_excluded,
if prev_deployed_excluded > 1 { "s" } else { "" }
);
}
UPDATE_TARGETS_IGNORED.set(prev_deployed_excluded as i64);
// Pick highest available updates target (based on age-index).
let next = match new_updates.last().cloned() {
Some(rel) => rel,
None => return Ok(None),
};
// Check for downgrades.
if next <= cur_release {
log::warn!("downgrade hint towards target release '{}'", next.version);
if !allow_downgrade {
log::warn!("update hint rejected, downgrades are not allowed by configuration");
return Ok(None);
}
}
Ok(Some(next))
}
/// Try to match a set of (denylisted) deployments to their graph entries.
fn find_denylisted_releases(graph: &client::Graph, depls: BTreeSet<Release>) -> BTreeSet<Release> {
use std::collections::HashSet;
let mut local_releases = BTreeSet::new();
let local_payloads: HashSet<Payload> = depls.into_iter().map(|rel| rel.payload).collect();
for entry in &graph.nodes {
if let Ok(release) = Release::from_cincinnati(entry.clone()) {
if local_payloads.contains(&release.payload) {
local_releases.insert(release);
}
}
}
local_releases
}
/// Check whether input node matches current checksum.
fn is_same_checksum(node: &Node, deploy: &Release) -> bool {
match node.metadata.get(SCHEME_KEY) {
Some(scheme) if scheme == OCI_SCHEME => deploy.payload.whole() == node.payload,
_ => false,
}
}
/// Check whether input node is a dead-end; if so, return the reason.
///
/// Note: this is usually only called on the node
/// corresponding to the booted deployment.
fn evaluate_deadend(node: &Node) -> Option<String> {
let node_is_deadend = node
.metadata
.get(DEADEND_KEY)
.map(|v| v == "true")
.unwrap_or(false);
if !node_is_deadend {
return None;
}
let mut deadend_reason = node
.metadata
.get(DEADEND_REASON_KEY)
.map(|v| v.to_string())
.unwrap_or_default();
if deadend_reason.is_empty() {
deadend_reason = "(unknown reason)".to_string();
}
Some(deadend_reason)
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
#[test]
fn source_node_comparison() {
let current = Release {
version: String::new(),
payload: Payload::try_from("quay.io/fedora/fedora-coreos:oci-mock").unwrap(),
age_index: None,
};
let mut metadata = HashMap::new();
metadata.insert(SCHEME_KEY.to_string(), OCI_SCHEME.to_string());
let matching = Node {
version: "v0".to_string(),
payload: "quay.io/fedora/fedora-coreos:oci-mock".to_string(),
metadata,
};
assert!(is_same_checksum(&matching, ¤t));
let mismatch = Node {
version: "v0".to_string(),
payload: "mismatch".to_string(),
metadata: HashMap::new(),
};
assert!(!is_same_checksum(&mismatch, ¤t));
}
#[test]
fn deadend_node() {
let deadend_json = r#"
{
"version": "30.20190716.1",
"metadata": {
"org.fedoraproject.coreos.releases.age_index": "0",
"org.fedoraproject.coreos.scheme": "checksum",
"org.fedoraproject.coreos.updates.deadend": "true",
"org.fedoraproject.coreos.updates.deadend_reason": "https://github.com/coreos/fedora-coreos-tracker/issues/215"
},
"payload": "ff4803b069b5a10e5bee2f6bb0027117637559d813c2016e27d57b309dd09d6f"
}
"#;
let deadend: Node = serde_json::from_str(deadend_json).unwrap();
let reason = "https://github.com/coreos/fedora-coreos-tracker/issues/215".to_string();
assert_eq!(evaluate_deadend(&deadend), Some(reason));
let common_json = r#"
{
"version": "30.20190725.0",
"metadata": {
"org.fedoraproject.coreos.releases.age_index": "1",
"org.fedoraproject.coreos.scheme": "checksum"
},
"payload": "8b79877efa7ac06becd8637d95f8ca83aa385f89f383288bf3c2c31ca53216c7"
}
"#;
let common: Node = serde_json::from_str(common_json).unwrap();
assert_eq!(evaluate_deadend(&common), None);
}
}
================================================
FILE: src/cli/agent.rs
================================================
//! Logic for the `agent` subcommand.
use super::ensure_user;
use crate::{config, dbus, metrics, rpm_ostree, update_agent, utils};
use actix::{Actor, Addr};
use anyhow::{Context, Result};
use clap::{crate_name, crate_version};
use log::{info, trace};
use prometheus::IntGauge;
use tokio::runtime::Runtime;
lazy_static::lazy_static! {
static ref PROCESS_START_TIME: IntGauge = register_int_gauge!(opts!(
"process_start_time_seconds",
"Start time of the process since unix epoch in seconds."
)).unwrap();
}
/// Agent subcommand entry-point.
pub(crate) fn run_agent() -> Result<()> {
ensure_user("zincati", "update agent not running as `zincati` user")?;
info!(
"starting update agent ({} {})",
crate_name!(),
crate_version!()
);
// Start a new dedicated signal handling thread in a new runtime.
let signal_handling_rt = Runtime::new().unwrap();
signal_handling_rt.spawn(async {
use tokio::signal::unix::{signal, SignalKind};
// Create stream of terminate signals.
let mut stream = signal(SignalKind::terminate()).expect("failed to set SIGTERM handler");
stream.recv().await;
// Reset status text to empty string (default).
utils::update_unit_status("");
utils::notify_stopping();
std::process::exit(0);
});
let settings = config::Settings::assemble()?;
settings.refresh_metrics();
info!(
"agent running on node '{}', in update group '{}'",
settings.identity.node_uuid.lower_hex(),
settings.identity.group
);
// Expose process start timestamp.
let start_time = chrono::Utc::now();
PROCESS_START_TIME.set(start_time.timestamp());
trace!("creating actor system");
let sys = actix::System::new();
// Lift the dbus_service_addr ref to a higher scope to prevent drop() from
// being called on it, else we'll lose the listener as soon as we create it.
// This previously worked without doing so because of a connection leak in zbus:
// https://github.com/dbus2/zbus/commit/ba2a40752dcb45a034eeda6902b59e1ac437cdcb
let _dbus_service_addr = sys.block_on(async {
trace!("creating metrics service");
let _metrics_addr = metrics::MetricsService::bind_socket()?.start();
trace!("creating rpm-ostree client");
let rpm_ostree_addr = rpm_ostree::RpmOstreeClient::start(1);
trace!("creating update agent");
let agent = update_agent::UpdateAgent::with_config(settings, rpm_ostree_addr);
let agent_addr = agent.start();
trace!("creating D-Bus service");
let dbus_service_addr = dbus::DBusService::start(1, agent_addr);
Ok::<Addr<dbus::DBusService>, anyhow::Error>(dbus_service_addr)
})?;
trace!("starting actor system");
sys.run().context("agent failed")?;
Ok(())
}
================================================
FILE: src/cli/deadend.rs
================================================
//! Logic for the `deadend` subcommand.
use super::ensure_user;
use anyhow::{Context, Result};
use clap::Subcommand;
use fn_error_context::context;
use std::fs::Permissions;
use std::io::Write;
use std::os::unix::fs::PermissionsExt;
/// Absolute path to the MOTD fragments directory.
static MOTD_FRAGMENTS_DIR: &str = "/run/motd.d/";
/// Absolute path to the MOTD fragment with deadend state.
static DEADEND_MOTD_PATH: &str = "/run/motd.d/85-zincati-deadend.motd";
/// Subcommand `deadend-motd`.
#[derive(Debug, Subcommand)]
pub enum Cmd {
/// Set deadend state, with given reason.
#[command(name = "set")]
Set {
#[arg(long = "reason")]
reason: String,
},
/// Unset deadend state.
#[command(name = "unset")]
Unset,
}
impl Cmd {
/// `deadend-motd` subcommand entry point.
#[context("failed to run `deadend-motd` subcommand")]
pub(crate) fn run(self) -> Result<()> {
ensure_user(
"root",
"deadend-motd subcommand must be run as `root` user, \
and should be called by the Zincati agent process",
)?;
match self {
Cmd::Set { reason } => refresh_motd_fragment(reason),
Cmd::Unset => remove_motd_fragment(),
}
}
}
/// Refresh MOTD fragment with deadend reason.
fn refresh_motd_fragment(reason: String) -> Result<()> {
// Avoid showing partially-written messages using tempfile and
// persist (rename).
let mut f = tempfile::Builder::new()
.prefix(".deadend.")
.suffix(".motd.partial")
// Create the tempfile in the same directory as the final MOTD,
// to ensure proper SELinux labels are applied to the tempfile
// before renaming.
.tempfile_in(MOTD_FRAGMENTS_DIR)
.with_context(|| {
format!(
"failed to create temporary MOTD file under '{}'",
MOTD_FRAGMENTS_DIR
)
})?;
// Set correct permissions of the temporary file, before moving to
// the destination (`tempfile` creates files with mode 0600).
std::fs::set_permissions(f.path(), Permissions::from_mode(0o644)).with_context(|| {
format!(
"failed to set permissions of temporary MOTD file at '{}'",
f.path().display()
)
})?;
writeln!(
f,
"This release is a dead-end and will not further auto-update: {}",
reason
)
.and_then(|_| f.flush())
.with_context(|| format!("failed to write MOTD content to '{}'", f.path().display()))?;
f.persist(DEADEND_MOTD_PATH)
.with_context(|| format!("failed to persist MOTD fragment to '{}'", DEADEND_MOTD_PATH))?;
Ok(())
}
/// Remove motd fragment file, if any.
fn remove_motd_fragment() -> Result<()> {
if let Err(e) = std::fs::remove_file(DEADEND_MOTD_PATH) {
if e.kind() != std::io::ErrorKind::NotFound {
anyhow::bail!(
"failed to remove MOTD fragment at '{}': {}",
DEADEND_MOTD_PATH,
e
);
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cli::{CliCommand, CliOptions};
use clap::Parser;
#[test]
fn test_deadend_motd_set() {
{
let missing_flag = vec!["zincati", "deadend-motd", "set"];
let cli = CliOptions::try_parse_from(missing_flag);
assert!(cli.is_err());
}
{
let missing_reason = vec!["zincati", "deadend-motd", "set", "--reason"];
let cli = CliOptions::try_parse_from(missing_reason);
assert!(cli.is_err());
}
{
let empty_reason = vec!["zincati", "deadend-motd", "set", "--reason", ""];
let cli = CliOptions::try_parse_from(empty_reason).unwrap();
if let CliCommand::DeadendMotd(Cmd::Set { reason }) = &cli.cmd {
assert_eq!(reason, "");
} else {
panic!("unexpected result: {:?}", cli);
}
}
{
let reason_message = vec!["zincati", "deadend-motd", "set", "--reason", "foo"];
let cli = CliOptions::try_parse_from(reason_message).unwrap();
if let CliCommand::DeadendMotd(Cmd::Set { reason }) = &cli.cmd {
assert_eq!(reason, "foo");
} else {
panic!("unexpected result: {:?}", cli);
}
}
}
#[test]
fn test_deadend_motd_unset() {
{
let extra_flags = vec!["zincati", "deadend-motd", "unset", "--reason", "foo"];
let cli = CliOptions::try_parse_from(extra_flags);
assert!(cli.is_err());
}
{
let unset = vec!["zincati", "deadend-motd", "unset"];
let cli = CliOptions::try_parse_from(unset).unwrap();
if !matches!(&cli.cmd, CliCommand::DeadendMotd(Cmd::Unset)) {
panic!("unexpected result: {:?}", cli);
}
}
}
}
================================================
FILE: src/cli/ex.rs
================================================
//! Logic for the ex subcommand.
use super::ensure_user;
use anyhow::Result;
use clap::Subcommand;
use fn_error_context::context;
use zbus::proxy;
#[derive(Debug, Subcommand)]
pub enum Cmd {
/// Replies different cow-speak depending on whether the
/// talkative flag is set.
#[command(name = "moo")]
Moo {
#[arg(long)]
talkative: bool,
},
/// Get last refresh time of update agent actor's state.
#[command(name = "last-refresh-time")]
LastRefreshTime,
}
impl Cmd {
/// `ex` subcommand entry point.
#[context("failed to run `ex` subcommand")]
pub(crate) fn run(self) -> Result<()> {
ensure_user(
"root",
"ex subcommand must be run as `root` user, \
and should only be used for testing purposes",
)?;
let connection = zbus::blocking::Connection::system()?;
let proxy = ExperimentalProxyBlocking::new(&connection)?;
match self {
Cmd::Moo { talkative } => {
println!("{}", proxy.moo(talkative)?);
Ok(())
}
Cmd::LastRefreshTime => {
println!("{}", proxy.last_refresh_time()?);
Ok(())
}
}
}
}
#[proxy(
interface = "org.coreos.zincati.Experimental",
default_service = "org.coreos.zincati",
default_path = "/org/coreos/zincati"
)]
trait Experimental {
/// LastRefreshTime method
fn last_refresh_time(&self) -> zbus::Result<i64>;
/// Moo method
fn moo(&self, talkative: bool) -> zbus::Result<String>;
}
================================================
FILE: src/cli/mod.rs
================================================
//! Command-Line Interface (CLI) logic.
mod agent;
mod deadend;
mod ex;
use anyhow::Result;
use clap::{ArgAction, Parser};
use log::LevelFilter;
use users::get_current_username;
/// CLI configuration options.
#[derive(Debug, Parser)]
pub(crate) struct CliOptions {
/// Verbosity level (higher is more verbose).
#[arg(action = ArgAction::Count, short = 'v', global = true)]
verbosity: u8,
/// CLI sub-command.
#[clap(subcommand)]
pub(crate) cmd: CliCommand,
}
impl CliOptions {
/// Returns the log-level set via command-line flags.
pub(crate) fn loglevel(&self) -> LevelFilter {
match self.verbosity {
0 => LevelFilter::Warn,
1 => LevelFilter::Info,
2 => LevelFilter::Debug,
_ => LevelFilter::Trace,
}
}
/// Dispatch CLI subcommand.
pub(crate) fn run(self) -> Result<()> {
match self.cmd {
CliCommand::Agent => agent::run_agent(),
CliCommand::DeadendMotd(cmd) => cmd.run(),
CliCommand::Ex(cmd) => cmd.run(),
}
}
}
/// CLI sub-commands.
#[derive(Debug, Parser)]
#[command(rename_all = "kebab-case")]
pub(crate) enum CliCommand {
/// Long-running agent for auto-updates.
Agent,
/// Set or unset deadend MOTD state.
#[command(hide = true, subcommand)]
DeadendMotd(deadend::Cmd),
/// Print update agent state's last refresh time.
#[command(hide = true, subcommand)]
Ex(ex::Cmd),
}
/// Return Error with msg if not run by user.
fn ensure_user(user: &str, msg: &str) -> Result<()> {
if let Some(uname) = get_current_username() {
if uname == user {
return Ok(());
}
}
anyhow::bail!("{}", msg)
}
================================================
FILE: src/config/fragments.rs
================================================
//! TOML configuration fragments.
use ordered_float::NotNan;
use serde::Deserialize;
use std::collections::BTreeSet;
use std::num::NonZeroU64;
/// Top-level configuration stanza.
#[derive(Debug, Deserialize, PartialEq, Eq)]
pub(crate) struct ConfigFragment {
/// Agent configuration.
pub(crate) agent: Option<AgentFragment>,
/// Cincinnati client configuration.
pub(crate) cincinnati: Option<CincinnatiFragment>,
/// Agent identity.
pub(crate) identity: Option<IdentityFragment>,
/// Update strategy configuration.
pub(crate) updates: Option<UpdateFragment>,
}
/// Config fragment for agent settings.
#[derive(Debug, Deserialize, PartialEq, Eq)]
pub(crate) struct AgentFragment {
/// Timing settings for the agent.
pub(crate) timing: Option<AgentTiming>,
}
/// Config fragment for agent timing.
#[derive(Debug, Deserialize, PartialEq, Eq)]
pub(crate) struct AgentTiming {
/// Pausing interval between updates checks in steady mode, in seconds (default: 300).
pub(crate) steady_interval_secs: Option<NonZeroU64>,
}
// Config fragment for agent identity.
#[derive(Debug, Deserialize, PartialEq, Eq)]
pub(crate) struct IdentityFragment {
/// Update group for this agent (default: 'default')
pub(crate) group: Option<String>,
/// Update group for this agent (default: derived from machine-id)
pub(crate) node_uuid: Option<String>,
/// Update group for this agent (default: derived server-side)
pub(crate) rollout_wariness: Option<NotNan<f64>>,
}
/// Config fragment for Cincinnati client.
#[derive(Debug, Deserialize, PartialEq, Eq)]
pub(crate) struct CincinnatiFragment {
/// Base URL to upstream cincinnati server.
pub(crate) base_url: Option<String>,
}
/// Config fragment for update logic.
#[derive(Debug, Deserialize, PartialEq, Eq)]
pub(crate) struct UpdateFragment {
/// Whether to enable automatic downgrades.
pub(crate) allow_downgrade: Option<bool>,
/// Whether to enable auto-updates logic.
pub(crate) enabled: Option<bool>,
/// Update strategy (default: immediate).
pub(crate) strategy: Option<String>,
/// `fleet_lock` strategy config.
pub(crate) fleet_lock: Option<UpdateFleetLock>,
/// `periodic` strategy config.
pub(crate) periodic: Option<UpdatePeriodic>,
}
/// Config fragment for `fleet_lock` update strategy.
#[derive(Debug, Deserialize, PartialEq, Eq)]
pub(crate) struct UpdateFleetLock {
/// Base URL for the remote semaphore manager.
pub(crate) base_url: Option<String>,
}
/// Config fragment for `periodic` update strategy.
#[derive(Debug, Deserialize, PartialEq, Eq)]
pub(crate) struct UpdatePeriodic {
/// A weekly window.
pub(crate) window: Option<Vec<UpdatePeriodicWindow>>,
/// A time zone in the IANA Time Zone Database (https://www.iana.org/time-zones)
/// or "localtime". If unset, UTC is used.
///
/// Examples: `America/Toronto`, `Europe/Rome`
pub(crate) time_zone: Option<String>,
}
/// Config fragment for a `periodic.window` entry.
#[derive(Debug, Deserialize, PartialEq, Eq)]
pub(crate) struct UpdatePeriodicWindow {
/// Weekdays (English names).
pub(crate) days: BTreeSet<String>,
/// Start time (`hh:mm` 24h format).
pub(crate) start_time: String,
/// Window length in minutes.
pub(crate) length_minutes: u32,
}
#[cfg(test)]
mod tests {
use super::*;
use maplit::btreeset;
#[test]
fn basic_dist_config_sample() {
let content = std::fs::read_to_string("tests/fixtures/00-config-sample.toml").unwrap();
let cfg: ConfigFragment = toml::from_str(&content).unwrap();
let expected = ConfigFragment {
agent: Some(AgentFragment {
timing: Some(AgentTiming {
steady_interval_secs: Some(NonZeroU64::new(35).unwrap()),
}),
}),
cincinnati: Some(CincinnatiFragment {
base_url: Some("http://cincinnati.example.com:80/".to_string()),
}),
identity: Some(IdentityFragment {
group: Some("workers".to_string()),
node_uuid: Some("27e3ac02af3946af995c9940e18b0cce".to_string()),
rollout_wariness: Some(NotNan::new(0.5).unwrap()),
}),
updates: Some(UpdateFragment {
allow_downgrade: Some(true),
enabled: Some(false),
strategy: Some("fleet_lock".to_string()),
fleet_lock: Some(UpdateFleetLock {
base_url: Some("http://fleet-lock.example.com:8080/".to_string()),
}),
periodic: Some(UpdatePeriodic {
window: Some(vec![
UpdatePeriodicWindow {
days: btreeset!("Sat".to_string(), "Sun".to_string()),
start_time: "23:00".to_string(),
length_minutes: 120,
},
UpdatePeriodicWindow {
days: btreeset!("Wed".to_string()),
start_time: "23:30".to_string(),
length_minutes: 25,
},
]),
time_zone: Some("localtime".to_string()),
}),
}),
};
assert_eq!(cfg, expected);
}
}
================================================
FILE: src/config/inputs.rs
================================================
use crate::config::fragments;
use crate::update_agent::DEFAULT_STEADY_INTERVAL_SECS;
use anyhow::{Context, Result};
use fn_error_context::context;
use log::trace;
use ordered_float::NotNan;
use serde::Serialize;
use std::num::NonZeroU64;
/// Runtime configuration holding environmental inputs.
#[derive(Debug, Serialize)]
pub(crate) struct ConfigInput {
pub(crate) agent: AgentInput,
pub(crate) cincinnati: CincinnatiInput,
pub(crate) updates: UpdateInput,
pub(crate) identity: IdentityInput,
}
impl ConfigInput {
/// Read config fragments and merge them into a single config.
#[context("failed to read and merge config fragments")]
pub(crate) fn read_configs(
dirs: Vec<String>,
common_path: &str,
extensions: Vec<String>,
) -> Result<Self> {
let mut fragments = Vec::new();
for (_, fpath) in liboverdrop::scan(dirs, common_path, extensions.as_slice(), true) {
trace!("reading config fragment '{}'", fpath.display());
let content = std::fs::read_to_string(&fpath)
.with_context(|| format!("failed to read file '{}'", fpath.display()))?;
let frag: fragments::ConfigFragment =
toml::from_str(&content).context("failed to parse TOML")?;
fragments.push(frag);
}
let cfg = Self::merge_fragments(fragments);
Ok(cfg)
}
/// Merge multiple fragments into a single configuration.
pub(crate) fn merge_fragments(fragments: Vec<fragments::ConfigFragment>) -> Self {
let mut agents = vec![];
let mut cincinnatis = vec![];
let mut updates = vec![];
let mut identities = vec![];
for snip in fragments {
if let Some(a) = snip.agent {
agents.push(a);
}
if let Some(c) = snip.cincinnati {
cincinnatis.push(c);
}
if let Some(f) = snip.updates {
updates.push(f);
}
if let Some(i) = snip.identity {
identities.push(i);
}
}
Self {
agent: AgentInput::from_fragments(agents),
cincinnati: CincinnatiInput::from_fragments(cincinnatis),
updates: UpdateInput::from_fragments(updates),
identity: IdentityInput::from_fragments(identities),
}
}
}
/// Config for the agent.
#[derive(Debug, Serialize)]
pub(crate) struct AgentInput {
pub(crate) steady_interval_secs: NonZeroU64,
}
impl AgentInput {
fn from_fragments(fragments: Vec<fragments::AgentFragment>) -> Self {
let mut cfg = Self {
steady_interval_secs: NonZeroU64::new(DEFAULT_STEADY_INTERVAL_SECS)
.expect("non-zero interval"),
};
for snip in fragments {
if let Some(timing) = snip.timing {
if let Some(s) = timing.steady_interval_secs {
cfg.steady_interval_secs = s;
}
}
}
cfg
}
}
#[derive(Clone, Debug, Serialize)]
pub(crate) struct CincinnatiInput {
/// Base URL (template) for the Cincinnati service.
pub(crate) base_url: String,
}
impl CincinnatiInput {
fn from_fragments(fragments: Vec<fragments::CincinnatiFragment>) -> Self {
let mut cfg = Self {
base_url: String::new(),
};
for snip in fragments {
if let Some(u) = snip.base_url {
cfg.base_url = u;
}
}
cfg
}
}
#[derive(Debug, Serialize)]
pub(crate) struct IdentityInput {
pub(crate) group: String,
pub(crate) node_uuid: String,
pub(crate) rollout_wariness: Option<NotNan<f64>>,
}
impl IdentityInput {
fn from_fragments(fragments: Vec<fragments::IdentityFragment>) -> Self {
let mut cfg = Self {
group: String::new(),
node_uuid: String::new(),
rollout_wariness: None,
};
for snip in fragments {
if let Some(g) = snip.group {
cfg.group = g;
}
if let Some(nu) = snip.node_uuid {
cfg.node_uuid = nu;
}
if let Some(rw) = snip.rollout_wariness {
cfg.rollout_wariness = Some(rw);
}
}
cfg
}
}
/// Config for update logic.
#[derive(Debug, Serialize)]
pub(crate) struct UpdateInput {
/// Whether to enable automatic downgrades.
pub(crate) allow_downgrade: bool,
/// Whether to enable auto-updates logic.
pub(crate) enabled: bool,
/// Update strategy.
pub(crate) strategy: String,
/// `fleet_lock` strategy config.
pub(crate) fleet_lock: FleetLockInput,
/// `periodic` strategy config.
pub(crate) periodic: PeriodicInput,
}
/// Config for "fleet_lock" strategy.
#[derive(Clone, Debug, Serialize)]
pub(crate) struct FleetLockInput {
/// Base URL (template) for the FleetLock service.
pub(crate) base_url: String,
}
/// Config for "periodic" strategy.
#[derive(Clone, Debug, Serialize)]
pub(crate) struct PeriodicInput {
/// Set of updates windows.
pub(crate) intervals: Vec<PeriodicIntervalInput>,
/// A time zone in the IANA Time Zone Database or "localtime".
/// Defaults to "UTC".
pub(crate) time_zone: String,
}
/// Update window for a "periodic" interval.
#[derive(Clone, Debug, Serialize)]
pub(crate) struct PeriodicIntervalInput {
pub(crate) start_day: String,
pub(crate) start_time: String,
pub(crate) length_minutes: u32,
}
impl UpdateInput {
fn from_fragments(fragments: Vec<fragments::UpdateFragment>) -> Self {
let mut allow_downgrade = false;
let mut enabled = true;
let mut strategy = String::new();
let mut fleet_lock = FleetLockInput {
base_url: String::new(),
};
let mut periodic = PeriodicInput {
intervals: vec![],
time_zone: "UTC".to_string(),
};
for snip in fragments {
if let Some(a) = snip.allow_downgrade {
allow_downgrade = a;
}
if let Some(e) = snip.enabled {
enabled = e;
}
if let Some(s) = snip.strategy {
strategy = s;
}
if let Some(fl) = snip.fleet_lock {
if let Some(b) = fl.base_url {
fleet_lock.base_url = b;
}
}
if let Some(w) = snip.periodic {
if let Some(tz) = w.time_zone {
periodic.time_zone = tz;
}
if let Some(win) = w.window {
for entry in win {
for day in entry.days {
let interval = PeriodicIntervalInput {
start_day: day,
start_time: entry.start_time.clone(),
length_minutes: entry.length_minutes,
};
periodic.intervals.push(interval);
}
}
}
}
}
Self {
allow_downgrade,
enabled,
strategy,
fleet_lock,
periodic,
}
}
}
================================================
FILE: src/config/mod.rs
================================================
//! Configuration parsing and validation.
//!
//! This module contains the following logical entities:
//! * Fragments: TOML configuration entries.
//! * Inputs: configuration fragments merged, but not yet validated.
//! * Settings: validated settings for the agent.
/// TOML structures.
pub(crate) mod fragments;
/// Configuration fragments.
pub(crate) mod inputs;
use crate::cincinnati::Cincinnati;
use crate::identity::Identity;
use crate::strategy::UpdateStrategy;
use crate::update_agent;
use anyhow::Result;
use clap::crate_name;
use fn_error_context::context;
use serde::Serialize;
use std::num::NonZeroU64;
/// Runtime configuration for the agent.
///
/// It holds validated agent configuration.
#[derive(Debug, Serialize)]
pub(crate) struct Settings {
/// Whether to enable automatic downgrades.
pub(crate) allow_downgrade: bool,
/// Whether to enable auto-updates logic.
pub(crate) enabled: bool,
/// Agent timing, steady state refresh period.
pub(crate) steady_interval_secs: NonZeroU64,
/// Cincinnati configuration.
pub(crate) cincinnati: Cincinnati,
/// Agent configuration.
pub(crate) identity: Identity,
/// Agent update strategy.
pub(crate) strategy: UpdateStrategy,
}
impl Settings {
/// Assemble runtime settings.
#[context("failed to assemble configuration settings")]
pub(crate) fn assemble() -> Result<Self> {
let prefixes = vec![
"/usr/lib/".to_string(),
"/run/".to_string(),
"/etc/".to_string(),
];
let common_path = format!("{}/config.d/", crate_name!());
let extensions = vec!["toml".to_string()];
let cfg = inputs::ConfigInput::read_configs(prefixes, &common_path, extensions)?;
Self::validate(cfg)
}
/// Refresh settings-related metrics values.
pub(crate) fn refresh_metrics(&self) {
// TODO(lucab): consider adding more metrics here (e.g. steady interval).
update_agent::UPDATES_ENABLED.set(i64::from(self.enabled));
update_agent::ALLOW_DOWNGRADE.set(i64::from(self.allow_downgrade));
self.strategy.refresh_metrics();
}
/// Validate config and return a valid agent settings.
fn validate(cfg: inputs::ConfigInput) -> Result<Self> {
let allow_downgrade = cfg.updates.allow_downgrade;
let enabled = cfg.updates.enabled;
let steady_interval_secs = cfg.agent.steady_interval_secs;
let identity = Identity::with_config(cfg.identity)?;
let strategy = UpdateStrategy::with_config(cfg.updates, &identity)?;
let cincinnati = Cincinnati::with_config(cfg.cincinnati, &identity)?;
Ok(Self {
allow_downgrade,
enabled,
steady_interval_secs,
cincinnati,
identity,
strategy,
})
}
}
================================================
FILE: src/dbus/experimental.rs
================================================
//! Experimental interface.
use crate::update_agent::{LastRefresh, UpdateAgent};
use actix::Addr;
use futures::prelude::*;
use tokio::runtime::Runtime;
use zbus::{fdo, interface};
/// Experimental interface for testing.
pub(crate) struct Experimental {
pub(crate) agent_addr: Addr<UpdateAgent>,
}
#[interface(name = "org.coreos.zincati.Experimental")]
impl Experimental {
/// Just a test method.
fn moo(&self, talkative: bool) -> String {
if talkative {
String::from("Moooo mooo moooo!")
} else {
String::from("moo.")
}
}
/// Get update_agent actor's last refresh time.
fn last_refresh_time(&self) -> fdo::Result<i64> {
let msg = LastRefresh {};
let refresh_time_fut = self.agent_addr.send(msg).map_err(|e| {
let err_msg = format!("failed to get last refresh time from agent actor: {}", e);
log::error!("LastRefreshTime D-Bus method call: {}", err_msg);
fdo::Error::Failed(err_msg)
});
Runtime::new()
.map_err(|e| {
let err_msg = format!("failed to create runtime to execute future: {}", e);
log::error!("{}", err_msg);
fdo::Error::Failed(err_msg)
})
.and_then(|runtime| runtime.block_on(refresh_time_fut))
}
}
================================================
FILE: src/dbus/mod.rs
================================================
//! D-Bus service actor.
mod experimental;
use experimental::Experimental;
use crate::update_agent::UpdateAgent;
use actix::prelude::*;
use actix::Addr;
use anyhow::Result;
use fn_error_context::context;
use log::trace;
use zbus::blocking::{connection, Connection};
pub struct DBusService {
agent_addr: Addr<UpdateAgent>,
connection: Option<Connection>,
}
impl DBusService {
/// Create new DBusService
fn new(agent_addr: Addr<UpdateAgent>) -> DBusService {
DBusService {
agent_addr,
connection: None,
}
}
/// Start the threadpool for DBusService actor.
pub(crate) fn start(threads: usize, agent_addr: Addr<UpdateAgent>) -> Addr<Self> {
SyncArbiter::start(threads, move || DBusService::new(agent_addr.clone()))
}
#[context("failed to start object server")]
fn start_object_server(&mut self) -> Result<Connection> {
let connection = connection::Builder::system()?
.allow_name_replacements(true)
.replace_existing_names(true)
.name("org.coreos.zincati")?
.serve_at(
"/org/coreos/zincati",
Experimental {
agent_addr: self.agent_addr.clone(),
},
)?
.build()?;
Ok(connection)
}
}
impl Actor for DBusService {
type Context = SyncContext<Self>;
fn started(&mut self, _ctx: &mut Self::Context) {
trace!("D-Bus service actor started");
if let Some(conn) = self.connection.take() {
drop(conn);
}
match self.start_object_server() {
Err(err) => log::error!("failed to start D-Bus service actor: {}", err),
Ok(conn) => self.connection = Some(conn),
};
}
}
================================================
FILE: src/fleet_lock/mock_tests.rs
================================================
use crate::fleet_lock::*;
use crate::identity::Identity;
use mockito::Matcher;
use tokio::runtime as rt;
#[test]
fn test_pre_reboot_lock() {
let mut server = mockito::Server::new();
let body = r#"
{
"client_params": {
"id": "e0f3745b108f471cbd4883c6fbed8cdd",
"group": "mock-workers"
}
}
"#;
let m_pre_reboot = server
.mock("POST", Matcher::Exact(format!("/{}", V1_PRE_REBOOT)))
.match_header("fleet-lock-protocol", "true")
.match_body(Matcher::PartialJsonString(body.to_string()))
.with_status(200)
.create();
let runtime = rt::Runtime::new().unwrap();
let id = Identity::mock_default();
let client = ClientBuilder::new(server.url(), &id).build().unwrap();
let res = runtime.block_on(client.pre_reboot());
m_pre_reboot.assert();
let lock = res.unwrap();
assert!(lock);
}
#[test]
fn test_pre_reboot_error() {
let mut server = mockito::Server::new();
let body = r#"
{
"kind": "f1",
"value": "pre-reboot failure"
}
"#;
let m_pre_reboot = server
.mock("POST", Matcher::Exact(format!("/{}", V1_PRE_REBOOT)))
.match_header("fleet-lock-protocol", "true")
.with_status(404)
.with_body(body)
.create();
let runtime = rt::Runtime::new().unwrap();
let id = Identity::mock_default();
let client = ClientBuilder::new(server.url(), &id).build().unwrap();
let res = runtime.block_on(client.pre_reboot());
m_pre_reboot.assert();
let _rejection = res.unwrap_err();
}
#[test]
fn test_steady_state_lock() {
let mut server = mockito::Server::new();
let body = r#"
{
"client_params": {
"id": "e0f3745b108f471cbd4883c6fbed8cdd",
"group": "mock-workers"
}
}
"#;
let m_steady_state = server
.mock("POST", Matcher::Exact(format!("/{}", V1_STEADY_STATE)))
.match_header("fleet-lock-protocol", "true")
.match_body(Matcher::PartialJsonString(body.to_string()))
.with_status(200)
.create();
let runtime = rt::Runtime::new().unwrap();
let id = Identity::mock_default();
let client = ClientBuilder::new(server.url(), &id).build().unwrap();
let res = runtime.block_on(client.steady_state());
m_steady_state.assert();
let unlock = res.unwrap();
assert!(unlock);
}
#[test]
fn test_steady_state_error() {
let mut server = mockito::Server::new();
let body = r#"
{
"kind": "f1",
"value": "pre-reboot failure"
}
"#;
let m_steady_state = server
.mock("POST", Matcher::Exact(format!("/{}", V1_STEADY_STATE)))
.match_header("fleet-lock-protocol", "true")
.with_status(404)
.with_body(body)
.create();
let runtime = rt::Runtime::new().unwrap();
let id = Identity::mock_default();
let client = ClientBuilder::new(server.url(), &id).build().unwrap();
let res = runtime.block_on(client.steady_state());
m_steady_state.assert();
let _rejection = res.unwrap_err();
}
================================================
FILE: src/fleet_lock/mod.rs
================================================
//! Asynchronous FleetLock client, remote lock management.
//!
//! This module implements a client for FleetLock, a bare HTTP
//! protocol for managing cluster-wide reboot via a remote
//! lock manager. Protocol specification is available at
//! https://coreos.github.io/zincati/development/fleetlock/protocol/ .
use crate::identity::Identity;
use anyhow::{Context, Result};
use futures::prelude::*;
use reqwest::Method;
use serde::{Deserialize, Serialize};
use std::time::Duration;
use thiserror::Error;
#[cfg(test)]
mod mock_tests;
/// Default timeout for HTTP requests completion (30 minutes).
const DEFAULT_HTTP_COMPLETION_TIMEOUT: Duration = Duration::from_secs(30 * 60);
/// FleetLock pre-reboot API path endpoint (v1).
static V1_PRE_REBOOT: &str = "v1/pre-reboot";
/// FleetLock steady-state API path endpoint (v1).
static V1_STEADY_STATE: &str = "v1/steady-state";
/// FleetLock JSON protocol: service error.
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
pub struct RemoteJsonError {
/// Machine-friendly brief error kind.
kind: String,
/// Human-friendly detailed error explanation.
value: String,
}
/// Error related to the FleetLock service.
#[derive(Clone, Debug, Error, PartialEq, Eq)]
pub enum FleetLockError {
/// Remote endpoint error.
Remote(reqwest::StatusCode, RemoteJsonError),
/// Generic HTTP error.
Http(reqwest::StatusCode),
/// Client builder failed.
FailedClientBuilder(String),
/// Client failed request.
FailedRequest(String),
}
impl FleetLockError {
/// Return the machine-friendly brief error kind.
pub fn error_kind(&self) -> String {
match *self {
FleetLockError::Remote(_, ref err) => err.kind.clone(),
FleetLockError::Http(status) => format!("generic_http_{}", status.as_u16()),
FleetLockError::FailedClientBuilder(_) => "client_failed_build".to_string(),
FleetLockError::FailedRequest(_) => "client_failed_request".to_string(),
}
}
/// Return the human-friendly detailed error explanation.
pub fn error_value(&self) -> String {
match *self {
FleetLockError::Remote(_, ref err) => err.value.clone(),
FleetLockError::Http(_) => "(unknown/generic server error)".to_string(),
FleetLockError::FailedClientBuilder(ref err)
| FleetLockError::FailedRequest(ref err) => err.clone(),
}
}
/// Return the server-side error status code, if any.
pub fn status_code(&self) -> Option<u16> {
match *self {
FleetLockError::Remote(s, _) | FleetLockError::Http(s) => Some(s.as_u16()),
_ => None,
}
}
}
impl std::fmt::Display for FleetLockError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
// Account for both server-side and client-side failures.
let context = match self.status_code() {
Some(s) => format!("server-side error, code {}", s),
None => "client-side error".to_string(),
};
write!(f, "{}: {}", context, self.error_value())
}
}
/// Client to make outgoing API requests.
#[derive(Clone, Debug, Serialize)]
pub struct Client {
/// Base URL for API endpoint.
#[serde(skip)]
api_base: reqwest::Url,
/// Asynchronous reqwest client.
#[serde(skip)]
hclient: reqwest::Client,
/// Request body.
body: String,
}
impl Client {
/// Try to lock a semaphore slot on the remote manager.
///
/// It returns `true` if the operation succeeds, or a `FleetLockError`
/// with the relevant error explanation.
pub fn pre_reboot(&self) -> impl Future<Output = Result<bool, FleetLockError>> {
let req = self
.new_request(Method::POST, V1_PRE_REBOOT)
.map_err(|e| FleetLockError::FailedClientBuilder(e.to_string()));
futures::future::ready(req)
.and_then(|req| {
req.send()
.map_err(|e| FleetLockError::FailedRequest(e.to_string()))
})
.and_then(Self::map_response)
}
/// Try to unlock a semaphore slot on the remote manager.
///
/// It returns `true` if the operation succeeds, or a `FleetLockError`
/// with the relevant error explanation.
pub fn steady_state(&self) -> impl Future<Output = Result<bool, FleetLockError>> {
let req = self
.new_request(Method::POST, V1_STEADY_STATE)
.map_err(|e| FleetLockError::FailedClientBuilder(e.to_string()));
futures::future::ready(req)
.and_then(|req| {
req.send()
.map_err(|e| FleetLockError::FailedRequest(e.to_string()))
})
.and_then(Self::map_response)
}
/// Return a request builder for the target URL, with proper parameters set.
fn new_request<S: AsRef<str>>(
&self,
method: reqwest::Method,
url_suffix: S,
) -> Result<reqwest::RequestBuilder> {
let url = self.api_base.clone().join(url_suffix.as_ref())?;
let builder = self
.hclient
.request(method, url)
.body(self.body.clone())
.header("fleet-lock-protocol", "true");
Ok(builder)
}
/// Map an HTTP response to a service result.
async fn map_response(response: reqwest::Response) -> Result<bool, FleetLockError> {
// On success, short-circuit to `true`.
let status = response.status();
if status.is_success() {
return Ok(true);
}
// On error, decode failure details (or synthesize a generic error).
match response.json::<RemoteJsonError>().await {
Ok(rej) => Err(FleetLockError::Remote(status, rej)),
_ => Err(FleetLockError::Http(status)),
}
}
}
/// Client builder.
#[derive(Clone, Debug)]
pub struct ClientBuilder {
/// Base URL for API endpoint (mandatory).
api_base: String,
/// Asynchronous reqwest client (custom).
hclient: Option<reqwest::Client>,
/// Client identity.
client_identity: ClientIdentity,
}
/// Client identity, for requests body.
#[derive(Clone, Debug, Serialize)]
pub struct ClientIdentity {
client_params: ClientParameters,
}
/// Client parameters.
#[derive(Clone, Debug, Serialize)]
pub struct ClientParameters {
/// Node identifier, for lock ownership.
id: String,
/// Reboot group, for role-specific remote configuration.
group: String,
}
impl ClientBuilder {
/// Return a new client builder for the given base API endpoint URL.
pub(crate) fn new<T>(api_base: T, identity: &Identity) -> Self
where
T: Into<String>,
{
Self {
api_base: api_base.into(),
hclient: None,
client_identity: ClientIdentity {
client_params: ClientParameters {
id: identity.node_uuid.lower_hex(),
group: identity.group.clone(),
},
},
}
}
/// Set (or reset) the HTTP client to use.
#[allow(dead_code)]
pub fn http_client(self, hclient: Option<reqwest::Client>) -> Self {
let mut builder = self;
builder.hclient = hclient;
builder
}
/// Build a client with specified parameters.
pub fn build(self) -> Result<Client> {
let hclient = match self.hclient {
Some(client) => client,
None => reqwest::ClientBuilder::new()
.timeout(DEFAULT_HTTP_COMPLETION_TIMEOUT)
.build()?,
};
let api_base = reqwest::Url::parse(&self.api_base)
.context(format!("failed to parse '{}'", &self.api_base))?;
if self.client_identity.client_params.group.is_empty() {
anyhow::bail!("missing group value");
}
let body = serde_json::to_string_pretty(&self.client_identity)?;
let client = Client {
api_base,
hclient,
body,
};
Ok(client)
}
}
#[cfg(test)]
mod tests {
use super::*;
use http::response::Response;
use http::status::StatusCode;
use tokio::runtime as rt;
#[test]
fn test_service_rejection_display() {
let err_body = r#"
{
"kind": "failure_foo",
"value": "failed to perform foo"
}
"#;
let runtime = rt::Runtime::new().unwrap();
let response = Response::builder().status(466).body(err_body).unwrap();
let fut_rejection = Client::map_response(response.into());
let rejection = runtime.block_on(fut_rejection).unwrap_err();
let expected_rejection = FleetLockError::Remote(
StatusCode::from_u16(466).unwrap(),
RemoteJsonError {
kind: "failure_foo".to_string(),
value: "failed to perform foo".to_string(),
},
);
assert_eq!(&rejection, &expected_rejection);
let msg = rejection.to_string();
let expected_msg = "server-side error, code 466: failed to perform foo";
assert_eq!(&msg, expected_msg);
}
#[test]
fn test_http_error_display() {
let runtime = rt::Runtime::new().unwrap();
let response = Response::builder().status(433).body("").unwrap();
let fut_rejection = Client::map_response(response.into());
let rejection = runtime.block_on(fut_rejection).unwrap_err();
let expected_rejection = FleetLockError::Http(StatusCode::from_u16(433).unwrap());
assert_eq!(&rejection, &expected_rejection);
let msg = rejection.to_string();
let expected_msg = "server-side error, code 433: (unknown/generic server error)";
assert_eq!(&msg, expected_msg);
}
}
================================================
FILE: src/identity/mod.rs
================================================
mod platform;
use crate::config::inputs;
use crate::rpm_ostree;
use anyhow::{anyhow, ensure, Context, Result};
use fn_error_context::context;
use lazy_static::lazy_static;
use libsystemd::id128;
use ordered_float::NotNan;
use prometheus::{Gauge, IntGaugeVec};
use regex::Regex;
use serde::Serialize;
use std::collections::HashMap;
/// Default group for reboot management.
static DEFAULT_GROUP: &str = "default";
/// Application ID (`de35106b6ec24688b63afddaa156679b`)
static APP_ID: &[u8] = &[
0xde, 0x35, 0x10, 0x6b, 0x6e, 0xc2, 0x46, 0x88, 0xb6, 0x3a, 0xfd, 0xda, 0xa1, 0x56, 0x67, 0x9b,
];
lazy_static::lazy_static! {
static ref ROLLOUT_WARINESS: Gauge = Gauge::new(
"zincati_identity_rollout_wariness",
"Client wariness for updates rollout"
).unwrap();
static ref OS_INFO: IntGaugeVec = register_int_gauge_vec!(
"zincati_identity_os_info",
"Information about the underlying booted OS",
&["os_version", "basearch", "stream", "platform"]
).unwrap();
}
/// Agent identity.
#[derive(Debug, Serialize, Clone)]
pub(crate) struct Identity {
/// OS base architecture.
pub(crate) basearch: String,
/// Current OS (version and deployment base-checksum).
pub(crate) current_os: rpm_ostree::Release,
/// Update group.
pub(crate) group: String,
/// Unique node identifier.
pub(crate) node_uuid: id128::Id128,
/// OS platform.
pub(crate) platform: String,
/// Client wariness for rollout throttling.
pub(crate) rollout_wariness: Option<NotNan<f64>>,
/// Stream label.
pub(crate) stream: String,
}
impl Identity {
/// Create from configuration.
#[context("failed to validate agent identity configuration")]
pub(crate) fn with_config(cfg: inputs::IdentityInput) -> Result<Self> {
let mut id = Self::try_default().context("failed to build default identity")?;
if !cfg.group.is_empty() {
id.group = cfg.group;
};
id.validate_group_label()?;
if !cfg.node_uuid.is_empty() {
id.node_uuid = id128::Id128::parse_str(&cfg.node_uuid)
.map_err(|e| anyhow!("failed to parse node UUID: {}", e))?;
}
if let Some(rw) = cfg.rollout_wariness {
ensure!(*rw >= 0.0, "unexpected negative rollout wariness: {}", rw);
ensure!(*rw <= 1.0, "unexpected overlarge rollout wariness: {}", rw);
prometheus::register(Box::new(ROLLOUT_WARINESS.clone()))?;
ROLLOUT_WARINESS.set(*rw);
id.rollout_wariness = Some(rw);
}
// Export info-metrics with details about booted deployment.
OS_INFO
.with_label_values(&[
&id.current_os.version,
&id.basearch,
&id.stream,
&id.platform,
])
.set(1);
Ok(id)
}
/// Try to build default agent identity.
pub fn try_default() -> Result<Self> {
// Invoke rpm-ostree to get the status of the currently booted deployment.
let status = rpm_ostree::invoke_cli_status(true)?;
let basearch = coreos_stream_metadata::this_architecture().to_string();
let current_os =
rpm_ostree::parse_booted(&status).context("failed to introspect booted OS image")?;
let node_uuid = {
let app_id = id128::Id128::try_from_slice(APP_ID)
.map_err(|e| anyhow!("failed to parse application ID: {}", e))?;
compute_node_uuid(&app_id)?
};
let platform = platform::read_id("/proc/cmdline")?;
let stream = rpm_ostree::parse_booted_updates_stream(&status)
.context("failed to introspect OS updates stream")?;
let id = Self {
basearch,
stream,
platform,
current_os,
group: DEFAULT_GROUP.to_string(),
node_uuid,
rollout_wariness: None,
};
Ok(id)
}
/// Return context variables for URL templates.
pub fn url_variables(&self) -> HashMap<String, String> {
// This explicitly does not include "current_version"
// and "node_uuid".
let mut vars = HashMap::new();
vars.insert("basearch".to_string(), self.basearch.clone());
vars.insert("group".to_string(), self.group.clone());
vars.insert("platform".to_string(), self.platform.clone());
vars.insert("stream".to_string(), self.stream.clone());
vars
}
/// Return Cincinnati client parameters.
pub fn cincinnati_params(&self) -> HashMap<String, String> {
let mut vars = HashMap::new();
vars.insert("basearch".to_string(), self.basearch.clone());
vars.insert("os_version".to_string(), self.current_os.version.clone());
vars.insert("group".to_string(), self.group.clone());
vars.insert("node_uuid".to_string(), self.node_uuid.lower_hex());
vars.insert("platform".to_string(), self.platform.clone());
vars.insert("stream".to_string(), self.stream.clone());
vars.insert("os_checksum".to_string(), self.current_os.payload.whole());
vars.insert("oci".to_string(), "true".to_string());
if let Some(rw) = self.rollout_wariness {
vars.insert("rollout_wariness".to_string(), format!("{:.06}", rw));
}
vars
}
#[cfg(test)]
pub(crate) fn mock_default() -> Self {
use rpm_ostree::Payload;
Self {
basearch: "mock-amd64".to_string(),
current_os: rpm_ostree::Release {
version: "0.0.0-mock".to_string(),
payload: Payload::try_from("quay.io/fedora/fedora-coreos:oci-mock").unwrap(),
age_index: None,
},
group: "mock-workers".to_string(),
node_uuid: id128::Id128::parse_str("e0f3745b108f471cbd4883c6fbed8cdd").unwrap(),
platform: "mock-azure".to_string(),
rollout_wariness: Some(NotNan::new(0.5).unwrap()),
stream: "mock-stable".to_string(),
}
}
/// Validate the group label value in current identity.
///
/// Group setting can be transmitted to external backends (Cincinnati and FleetLock).
/// This ensures that label value is compliant to specs regex:
/// - https://coreos.github.io/zincati/development/fleetlock/protocol/#body
fn validate_group_label(&self) -> Result<()> {
static VALID_GROUP: &str = "^[a-zA-Z0-9.-]+$";
lazy_static! {
static ref VALID_GROUP_REGEX: Regex = Regex::new(VALID_GROUP).unwrap();
}
if !VALID_GROUP_REGEX.is_match(&self.group) {
anyhow::bail!(
"invalid group label '{}': not conforming to expression '{}'",
self.group,
VALID_GROUP
);
}
Ok(())
}
}
fn compute_node_uuid(app_id: &id128::Id128) -> Result<id128::Id128> {
let id = id128::get_machine_app_specific(app_id)
.map_err(|e| anyhow!("failed to get node ID: {}", e))?;
Ok(id)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn identity_url_variables() {
let id = Identity::mock_default();
let vars = id.url_variables();
assert!(vars.contains_key("basearch"));
assert!(vars.contains_key("group"));
assert!(vars.contains_key("platform"));
assert!(vars.contains_key("stream"));
assert!(!vars.contains_key("node_uuid"));
assert!(!vars.contains_key("os_checksum"));
assert!(!vars.contains_key("os_version"));
}
#[test]
fn identity_cincinnati_params() {
let mut id = Identity::mock_default();
id.validate_group_label().unwrap();
{
let valid = vec![
"default",
"worker",
"01",
"group-A",
"infra.01",
"example.com",
];
for entry in valid {
id.group = entry.to_string();
id.validate_group_label().unwrap();
}
}
{
let invalid = vec!["", "intránët"];
for entry in invalid {
id.group = entry.to_string();
id.validate_group_label().unwrap_err();
}
}
}
#[test]
fn identity_validate_group() {
let id = Identity::mock_default();
let vars = id.cincinnati_params();
assert!(vars.contains_key("basearch"));
assert!(vars.contains_key("group"));
assert!(vars.contains_key("platform"));
assert!(vars.contains_key("stream"));
assert!(vars.contains_key("node_uuid"));
assert!(vars.contains_key("os_checksum"));
assert!(vars.contains_key("os_version"));
}
}
================================================
FILE: src/identity/platform.rs
================================================
//! Kernel cmdline parsing - utility functions
//!
//! NOTE(lucab): this is not a complete/correct cmdline parser, as it implements
//! just enough logic to extract the platform ID value. In particular, it does not
//! handle separator quoting/escaping, list of values, and merging of repeated
//! flags. Logic is taken from Afterburn, please backport any bugfix there too:
//! https://github.com/coreos/afterburn/blob/v4.1.0/src/util/cmdline.rs
use anyhow::{Context, Result};
/// Platform key.
static CMDLINE_PLATFORM_FLAG: &str = "ignition.platform.id";
/// Read platform value from cmdline file.
pub(crate) fn read_id<T>(cmdline_path: T) -> Result<String>
where
T: AsRef<str>,
{
let fpath = cmdline_path.as_ref();
let contents = std::fs::read_to_string(fpath)
.with_context(|| format!("failed to read cmdline file {}", fpath))?;
// lookup flag by key name
match find_flag_value(CMDLINE_PLATFORM_FLAG, &contents) {
Some(platform) => {
log::trace!("found platform id: {}", platform);
Ok(platform)
}
None => anyhow::bail!(
"could not find flag '{}' in {}",
CMDLINE_PLATFORM_FLAG,
fpath
),
}
}
/// Find OEM ID flag value in cmdline string.
fn find_flag_value(flagname: &str, cmdline: &str) -> Option<String> {
// split content into elements and keep key-value tuples only.
let params: Vec<(&str, &str)> = cmdline
.split(' ')
.filter_map(|s| {
let kv: Vec<&str> = s.splitn(2, '=').collect();
match kv.len() {
2 => Some((kv[0], kv[1])),
_ => None,
}
})
.collect();
// find the oem flag
for (key, val) in params {
if key != flagname {
continue;
}
let bare_val = val.trim();
if !bare_val.is_empty() {
return Some(bare_val.to_string());
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_find_flag() {
let flagname = "ignition.platform.id";
let tests = vec![
("", None),
("foo=bar", None),
("ignition.platform.id", None),
("ignition.platform.id=", None),
("ignition.platform.id=\t", None),
("ignition.platform.id=ec2", Some("ec2".to_string())),
("ignition.platform.id=\tec2", Some("ec2".to_string())),
("ignition.platform.id=ec2\n", Some("ec2".to_string())),
("foo=bar ignition.platform.id=ec2", Some("ec2".to_string())),
("ignition.platform.id=ec2 foo=bar", Some("ec2".to_string())),
];
for (tcase, tres) in tests {
let res = find_flag_value(flagname, tcase);
assert_eq!(res, tres, "failed testcase: '{}'", tcase);
}
}
}
================================================
FILE: src/main.rs
================================================
//! Agent for Fedora CoreOS auto-updates.
#![deny(missing_debug_implementations)]
#![deny(missing_docs)]
#[macro_use(fail_point)]
extern crate fail;
#[macro_use]
extern crate prometheus;
// Cincinnati client.
mod cincinnati;
mod cli;
/// File-based configuration.
mod config;
/// D-Bus service.
mod dbus;
/// FleetLock client.
mod fleet_lock;
/// Agent identity.
mod identity;
/// Metrics service.
mod metrics;
/// rpm-ostree client.
mod rpm_ostree;
/// Update strategies.
mod strategy;
/// Update agent.
mod update_agent;
/// Utility functions.
mod utils;
/// Logic for weekly maintenance windows.
mod weekly;
use clap::{crate_name, Parser};
/// Binary entrypoint, for all CLI subcommands.
fn main() {
let exit_code = run();
std::process::exit(exit_code);
}
/// Run till completion or failure, pretty-printing termination errors if any.
fn run() -> i32 {
// Parse command-line options.
let cli_opts = cli::CliOptions::parse();
// Setup logging.
env_logger::Builder::from_default_env()
.format_timestamp(None)
.format_module_path(false)
.filter(Some(crate_name!()), cli_opts.loglevel())
.init();
// Dispatch CLI subcommand.
match cli_opts.run() {
Ok(_) => libc::EXIT_SUCCESS,
Err(e) => {
log_error_chain(&e);
if e.root_cause()
.downcast_ref::<crate::rpm_ostree::SystemInoperable>()
.is_some()
{
0
} else {
libc::EXIT_FAILURE
}
}
}
}
/// Pretty-print a chain of errors, as a series of error-priority log messages.
fn log_error_chain(err_chain: &anyhow::Error) {
let mut chain_iter = err_chain.chain();
let top_err = match chain_iter.next() {
Some(e) => e.to_string(),
None => "(unspecified failure)".to_string(),
};
log::error!("error: {}", top_err);
for err in chain_iter {
log::error!(" -> {}", err);
}
}
================================================
FILE: src/metrics/mod.rs
================================================
//! Metrics endpoint over a Unix-domain socket.
use actix::prelude::*;
use anyhow::{bail, Context, Result};
use std::os::unix::net as std_net;
use std::path::Path;
use tokio::net as tokio_net;
/// Unix socket path.
static SOCKET_PATH: &str = "/run/zincati/public/metrics.promsock";
/// Metrics exposition service.
#[derive(Debug)]
pub struct MetricsService {
listener: std_net::UnixListener,
}
impl MetricsService {
/// Create metrics service and bind to the Unix-domain socket.
pub fn bind_socket() -> Result<Self> {
Self::bind_socket_at(SOCKET_PATH)
.with_context(|| format!("failed to setup metrics service on '{}'", SOCKET_PATH))
}
pub(crate) fn bind_socket_at(path: impl AsRef<Path>) -> Result<Self> {
if let Err(e) = std::fs::remove_file(path.as_ref()) {
if e.kind() != std::io::ErrorKind::NotFound {
bail!("failed to remove socket file: {}", e);
}
};
let listener = std_net::UnixListener::bind(path.as_ref())
.context("failed to bind metrics service to Unix socket'")?;
Ok(Self { listener })
}
/// Gather metrics from the default registry and encode them in textual format.
fn prometheus_text_encode() -> Result<Vec<u8>> {
use prometheus::Encoder;
let metric_families = prometheus::gather();
let encoder = prometheus::TextEncoder::new();
let mut buffer = Vec::new();
encoder.encode(&metric_families, &mut buffer)?;
Ok(buffer)
}
}
/// Incoming Unix-domain socket connection.
struct Connection {
stream: tokio_net::UnixStream,
}
impl Message for Connection {
type Result = ();
}
impl Actor for MetricsService {
type Context = actix::Context<Self>;
fn started(&mut self, ctx: &mut actix::Context<Self>) {
let listener = self
.listener
.try_clone()
.expect("failed to clone metrics listener");
listener
.set_nonblocking(true)
.expect("failed to move metrics listener into nonblocking mode");
let async_listener = tokio_net::UnixListener::from_std(listener)
.expect("failed to create async metrics listener");
// This uses manual stream unfolding in order to keep the async listener
// alive for the whole duration of the stream.
let connections = futures::stream::unfold(async_listener, |l| async move {
loop {
let next = l.accept().await;
if let Ok((stream, _addr)) = next {
let conn = Connection { stream };
break Some((conn, l));
}
}
});
ctx.add_stream(connections);
log::debug!(
"started metrics service on Unix-domain socket '{}'",
SOCKET_PATH
);
}
}
impl actix::io::WriteHandler<std::io::Error> for MetricsService {
fn error(&mut self, _err: std::io::Error, _ctx: &mut Self::Context) -> Running {
actix::Running::Continue
}
fn finished(&mut self, _ctx: &mut Self::Context) {}
}
impl StreamHandler<Connection> for MetricsService {
fn handle(&mut self, item: Connection, ctx: &mut actix::Context<MetricsService>) {
let mut wr = actix::io::Writer::new(item.stream, ctx);
if let Ok(metrics) = MetricsService::prometheus_text_encode() {
wr.write(&metrics);
}
wr.close();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bind_socket_at() {
// Error path (EPERM or EISDIR).
MetricsService::bind_socket_at("/proc").unwrap_err();
let tmpdir = tempfile::tempdir().unwrap();
let tmp_socket_path = tmpdir.path().join("test-socket");
// Create a socket file and leave it behind on disk.
let service = MetricsService::bind_socket_at(&tmp_socket_path).unwrap();
drop(service);
// Make sure that the next run can remove it and start normally.
let service = MetricsService::bind_socket_at(&tmp_socket_path).unwrap();
drop(service);
}
}
================================================
FILE: src/rpm_ostree/actor.rs
================================================
//! rpm-ostree client actor.
use super::cli_status::Status;
use super::Release;
use actix::prelude::*;
use anyhow::{Context, Result};
use filetime::FileTime;
use log::trace;
use ostree_ext::container::OstreeImageReference;
use ostree_ext::oci_spec::distribution::Reference;
use std::collections::BTreeSet;
use std::rc::Rc;
/// Cache of local deployments.
#[derive(Clone, Debug)]
pub struct StatusCache {
pub status: Rc<Status>,
pub mtime: FileTime,
}
/// Client actor for rpm-ostree.
#[derive(Debug, Default, Clone)]
pub struct RpmOstreeClient {
// NB: This is OK for now because `rpm-ostree` actor is curently spawned on a single thread,
// but if we move to a larger threadpool, each actor thread will have its own cache.
pub status_cache: Option<StatusCache>,
}
impl Actor for RpmOstreeClient {
type Context = SyncContext<Self>;
}
impl RpmOstreeClient {
/// Start the threadpool for rpm-ostree blocking clients.
pub fn start(threads: usize) -> Addr<Self> {
SyncArbiter::start(threads, RpmOstreeClient::default)
}
}
/// Request: stage a deployment (in finalization-locked mode).
#[derive(Debug, Clone)]
pub struct StageDeployment {
/// Whether to allow downgrades.
pub allow_downgrade: bool,
/// Release to be staged.
pub release: Release,
}
impl Message for StageDeployment {
type Result = Result<Release>;
}
impl Handler<StageDeployment> for RpmOstreeClient {
type Result = Result<Release>;
fn handle(&mut self, msg: StageDeployment, _ctx: &mut Self::Context) -> Self::Result {
let rebase_target = {
// If there is staged deployment we use that to determine if we should rebase or deploy
// Otherwise, fallback to booted.
// This is because if we already staged a rebase, rebasing again won't work
// as "Old and new refs are equal"
// see https://github.com/coreos/szincati/pull/1273#issuecomment-2721531804
let status = super::cli_status::invoke_cli_status(false)?;
let local_deploy = match super::cli_status::get_staged_deployment(&status) {
Some(staged_deploy) => staged_deploy,
None => super::cli_status::booted_status(&status)?,
};
if let Some(booted_imgref) = local_deploy.get_container_image_reference() {
let booted_oci_ref: Reference = booted_imgref.imgref.name.parse()?;
let stream = local_deploy.get_fcos_update_stream()?;
// The cinncinati payload contains the container image pullspec, pinned to a digest.
// There are two cases where we want to rebase to a OSTree OCI refspec.
// 1 - The image we are booted on does not match a stream tag, e.g. the node was manually
// rebased to a version tag or a pinned digest. Here deploy would work but would lead
// to a weird UX:
// rpm-ostree status would show the version tag in the origin after we moved on to
// another version.
// 2 - The image name we are following has changed (new registry, new name)
// In that case `deploy` won't work and we need to rebase to the new refspec.
// The oci reference we want to end up with
let tagged_rebase_ref = Reference::with_tag(
msg.release.payload.registry().to_string(),
msg.release.payload.repository().to_string(),
stream,
);
// if those don't match we need to rebase
if booted_oci_ref != tagged_rebase_ref {
// craft a new ostree imgref object with the tagged oci reference we'll use for
// the rebase command so rpm-ostree will verify the signature of the OSTree commit
// wrapped inside the container:
let rebase_target = OstreeImageReference {
sigverify: booted_imgref.sigverify,
imgref: ostree_ext::container::ImageReference {
transport: booted_imgref.imgref.transport,
name: tagged_rebase_ref.whole(),
},
};
Some(rebase_target)
} else {
None
}
} else {
// This should never happen as requesting the OCI graph only happens after we detected the local deployment is OCI.
// But let's fail gracefuly just in case.
anyhow::bail!("Zincati does not support OCI updates if the current deployment is not already an OCI image reference.")
}
};
trace!("request to stage release: {:?}", &msg.release);
let release =
super::cli_deploy::deploy_locked(msg.release, msg.allow_downgrade, rebase_target);
trace!("rpm-ostree CLI returned: {:?}", release);
release
}
}
/// Request: finalize a staged deployment (by unlocking it and rebooting).
#[derive(Debug, Clone)]
pub struct FinalizeDeployment {
/// Finalized release to finalize.
pub release: Release,
}
impl Message for FinalizeDeployment {
type Result = Result<Release>;
}
impl Handler<FinalizeDeployment> for RpmOstreeClient {
type Result = Result<Release>;
fn handle(&mut self, msg: FinalizeDeployment, _ctx: &mut Self::Context) -> Self::Result {
trace!("request to finalize release: {:?}", msg.release);
let release = super::cli_finalize::finalize_deployment(msg.release);
trace!("rpm-ostree CLI returned: {:?}", release);
release
}
}
/// Request: query local deployments.
#[derive(Debug, Clone)]
pub struct QueryLocalDeployments {
/// Whether to include staged (i.e. not finalized) deployments in query result.
pub(crate) omit_staged: bool,
}
impl Message for QueryLocalDeployments {
type Result = Result<BTreeSet<Release>>;
}
impl Handler<QueryLocalDeployments> for RpmOstreeClient {
type Result = Result<BTreeSet<Release>>;
fn handle(
&mut self,
query_msg: QueryLocalDeployments,
_ctx: &mut Self::Context,
) -> Self::Result {
trace!("request to list local deployments");
let releases = super::cli_status::local_deployments(self, query_msg.omit_staged);
trace!("rpm-ostree CLI returned: {:?}", releases);
releases
}
}
/// Request: query pending deployment and stream.
#[derive(Debug, Clone)]
pub struct QueryPendingDeploymentStream {}
impl Message for QueryPendingDeploymentStream {
type Result = Result<Option<(Release, String)>>;
}
impl Handler<QueryPendingDeploymentStream> for RpmOstreeClient {
type Result = Result<Option<(Release, String)>>;
fn handle(
&mut self,
_msg: QueryPendingDeploymentStream,
_ctx: &mut Self::Context,
) -> Self::Result {
trace!("fetching details for staged deployment");
let status = super::cli_status::invoke_cli_status(false)?;
super::cli_status::parse_pending_deployment(&status)
.context("failed to introspect pending deployment")
}
}
/// Request: cleanup pending deployment.
#[derive(Debug, Clone)]
pub struct CleanupPendingDeployment {}
impl Message for CleanupPendingDeployment {
type Result = Result<()>;
}
impl Handler<CleanupPendingDeployment> for RpmOstreeClient {
type Result = Result<()>;
fn handle(&mut self, _msg: CleanupPendingDeployment, _ctx: &mut Self::Context) -> Self::Result {
trace!("request to cleanup pending deployment");
super::cli_deploy::invoke_cli_cleanup()?;
Ok(())
}
}
/// Request: Register as the update driver for rpm-ostree.
#[derive(Debug, Clone)]
pub struct RegisterAsDriver {}
impl Message for RegisterAsDriver {
type Result = ();
}
impl Handler<RegisterAsDriver> for RpmOstreeClient {
type Result = ();
fn handle(&mut self, _msg: RegisterAsDriver, _ctx: &mut Self::Context) -> Self::Result {
trace!("request to register as rpm-ostree update
gitextract_ibeb0zdk/
├── .cci.jenkinsfile
├── .gemini/
│ └── config.yaml
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug.md
│ │ ├── feature.md
│ │ └── release-checklist.md
│ ├── dependabot.yml
│ └── workflows/
│ ├── containers.yml
│ └── rust.yml
├── .gitignore
├── .packit.yaml
├── COPYRIGHT
├── Cargo.toml
├── DCO
├── LICENSE
├── Makefile
├── README.md
├── contrib/
│ └── monitoring-mixins/
│ ├── README.md
│ ├── dashboards/
│ │ └── dashboards.libsonnet
│ ├── jsonnetfile.json
│ └── mixin.libsonnet
├── dist/
│ ├── bin/
│ │ └── zincati-update-now
│ ├── config.d/
│ │ ├── 10-agent.toml
│ │ ├── 10-auto-updates.toml
│ │ ├── 10-identity.toml
│ │ ├── 30-updates-strategy.toml
│ │ └── 50-fedora-coreos-cincinnati.toml
│ ├── dbus-1/
│ │ └── system.d/
│ │ └── org.coreos.zincati.conf
│ ├── polkit-1/
│ │ ├── actions/
│ │ │ └── org.coreos.zincati.deadend.policy
│ │ └── rules.d/
│ │ └── zincati.rules
│ ├── systemd/
│ │ └── system/
│ │ └── zincati.service
│ ├── sysusers.d/
│ │ └── 50-zincati.conf
│ └── tmpfiles.d/
│ └── zincati.conf
├── docs/
│ ├── _config.yml
│ ├── _sass/
│ │ └── color_schemes/
│ │ └── coreos.scss
│ ├── contributing.md
│ ├── development/
│ │ ├── agent-actor-system.md
│ │ ├── cincinnati/
│ │ │ ├── protocol.md
│ │ │ └── response.json
│ │ ├── fleetlock/
│ │ │ └── protocol.md
│ │ ├── os-metadata.md
│ │ ├── quickstart.md
│ │ ├── testing.md
│ │ └── update-strategy-periodic.md
│ ├── development.md
│ ├── images/
│ │ ├── zincati-actors.dot
│ │ ├── zincati-fleetlock.msc
│ │ └── zincati-fsm.dot
│ ├── index.md
│ ├── usage/
│ │ ├── agent-identity.md
│ │ ├── auto-updates.md
│ │ ├── configuration.md
│ │ ├── logging.md
│ │ ├── metrics.md
│ │ └── updates-strategy.md
│ └── usage.md
├── src/
│ ├── cincinnati/
│ │ ├── client.rs
│ │ ├── mock_tests.rs
│ │ └── mod.rs
│ ├── cli/
│ │ ├── agent.rs
│ │ ├── deadend.rs
│ │ ├── ex.rs
│ │ └── mod.rs
│ ├── config/
│ │ ├── fragments.rs
│ │ ├── inputs.rs
│ │ └── mod.rs
│ ├── dbus/
│ │ ├── experimental.rs
│ │ └── mod.rs
│ ├── fleet_lock/
│ │ ├── mock_tests.rs
│ │ └── mod.rs
│ ├── identity/
│ │ ├── mod.rs
│ │ └── platform.rs
│ ├── main.rs
│ ├── metrics/
│ │ └── mod.rs
│ ├── rpm_ostree/
│ │ ├── actor.rs
│ │ ├── cli_deploy.rs
│ │ ├── cli_finalize.rs
│ │ ├── cli_status.rs
│ │ ├── mock_tests.rs
│ │ └── mod.rs
│ ├── strategy/
│ │ ├── fleet_lock.rs
│ │ ├── immediate.rs
│ │ ├── mod.rs
│ │ └── periodic.rs
│ ├── update_agent/
│ │ ├── actor.rs
│ │ └── mod.rs
│ ├── utils.rs
│ └── weekly/
│ ├── mod.rs
│ └── utils.rs
└── tests/
├── fixtures/
│ ├── 00-config-sample.toml
│ ├── 20-periodic-sample.toml
│ ├── 30-periodic-sample-non-utc.toml
│ ├── 31-periodic-sample-non-utc.toml
│ ├── rpm-ostree-staged.json
│ ├── rpm-ostree-status-annotation.json
│ └── rpm-ostree-status.json
└── kola/
├── common/
│ └── libtest.sh
├── dbus/
│ └── test-experimental.sh
├── misc/
│ └── test-status.sh
└── server/
├── config.fcc
├── test-deadend-release.sh
└── test-stream.sh
SYMBOL INDEX (390 symbols across 33 files)
FILE: src/cincinnati/client.rs
constant DEFAULT_HTTP_COMPLETION_TIMEOUT (line 18) | const DEFAULT_HTTP_COMPLETION_TIMEOUT: Duration = Duration::from_secs(30...
type Node (line 25) | pub struct Node {
type Graph (line 33) | pub struct Graph {
type GraphJsonError (line 40) | pub struct GraphJsonError {
type CincinnatiError (line 49) | pub enum CincinnatiError {
method error_kind (line 68) | pub fn error_kind(&self) -> String {
method error_value (line 81) | pub fn error_value(&self) -> String {
method status_code (line 94) | pub fn status_code(&self) -> Option<u16> {
method fmt (line 103) | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
type Client (line 115) | pub struct Client {
method fetch_graph (line 126) | pub fn fetch_graph(&self) -> impl Future<Output = Result<Graph, Cincin...
method new_request (line 140) | fn new_request<S: AsRef<str>>(
method map_response (line 155) | async fn map_response(response: reqwest::Response) -> Result<Graph, Ci...
type ClientBuilder (line 176) | pub struct ClientBuilder {
method new (line 187) | pub fn new<T>(api_base: T) -> Self
method query_params (line 199) | pub fn query_params(self, params: Option<HashMap<String, String>>) -> ...
method build (line 206) | pub fn build(self) -> Result<Client> {
function test_graph_server_error_display (line 234) | fn test_graph_server_error_display() {
function test_graph_http_error_display (line 260) | fn test_graph_http_error_display() {
function test_graph_client_error_display (line 274) | fn test_graph_client_error_display() {
FILE: src/cincinnati/mock_tests.rs
function test_empty_graph (line 8) | fn test_empty_graph() {
FILE: src/cincinnati/mod.rs
constant OCI_SCHEME (line 36) | pub const OCI_SCHEME: &str = "oci";
type DeadEndState (line 68) | pub struct DeadEndState(AtomicU8);
constant FALSE (line 77) | const FALSE: u8 = 0;
constant TRUE (line 78) | const TRUE: u8 = 1;
constant UNKNOWN (line 79) | const UNKNOWN: u8 = 2;
method is_deadend (line 82) | pub fn is_deadend(&self) -> bool {
method is_no_deadend (line 87) | pub fn is_no_deadend(&self) -> bool {
method set_deadend (line 91) | pub fn set_deadend(&self) {
method set_no_deadend (line 95) | pub fn set_no_deadend(&self) {
method default (line 71) | fn default() -> Self {
type Cincinnati (line 102) | pub struct Cincinnati {
method with_config (line 110) | pub(crate) fn with_config(cfg: inputs::CincinnatiInput, id: &Identity)...
method fetch_update_hint (line 130) | pub(crate) fn fetch_update_hint(
method next_update (line 152) | fn next_update(
function refresh_deadend_status (line 176) | fn refresh_deadend_status(node: &Node) -> Result<()> {
function find_update (line 213) | fn find_update(
function find_denylisted_releases (line 318) | fn find_denylisted_releases(graph: &client::Graph, depls: BTreeSet<Relea...
function is_same_checksum (line 336) | fn is_same_checksum(node: &Node, deploy: &Release) -> bool {
function evaluate_deadend (line 347) | fn evaluate_deadend(node: &Node) -> Option<String> {
function source_node_comparison (line 376) | fn source_node_comparison() {
function deadend_node (line 401) | fn deadend_node() {
FILE: src/cli/agent.rs
function run_agent (line 20) | pub(crate) fn run_agent() -> Result<()> {
FILE: src/cli/deadend.rs
type Cmd (line 18) | pub enum Cmd {
method run (line 33) | pub(crate) fn run(self) -> Result<()> {
function refresh_motd_fragment (line 47) | fn refresh_motd_fragment(reason: String) -> Result<()> {
function remove_motd_fragment (line 86) | fn remove_motd_fragment() -> Result<()> {
function test_deadend_motd_set (line 106) | fn test_deadend_motd_set() {
function test_deadend_motd_unset (line 138) | fn test_deadend_motd_unset() {
FILE: src/cli/ex.rs
type Cmd (line 10) | pub enum Cmd {
method run (line 26) | pub(crate) fn run(self) -> Result<()> {
type Experimental (line 52) | trait Experimental {
method last_refresh_time (line 54) | fn last_refresh_time(&self) -> zbus::Result<i64>;
method moo (line 57) | fn moo(&self, talkative: bool) -> zbus::Result<String>;
FILE: src/cli/mod.rs
type CliOptions (line 14) | pub(crate) struct CliOptions {
method loglevel (line 26) | pub(crate) fn loglevel(&self) -> LevelFilter {
method run (line 36) | pub(crate) fn run(self) -> Result<()> {
type CliCommand (line 48) | pub(crate) enum CliCommand {
function ensure_user (line 60) | fn ensure_user(user: &str, msg: &str) -> Result<()> {
FILE: src/config/fragments.rs
type ConfigFragment (line 10) | pub(crate) struct ConfigFragment {
type AgentFragment (line 23) | pub(crate) struct AgentFragment {
type AgentTiming (line 30) | pub(crate) struct AgentTiming {
type IdentityFragment (line 37) | pub(crate) struct IdentityFragment {
type CincinnatiFragment (line 48) | pub(crate) struct CincinnatiFragment {
type UpdateFragment (line 55) | pub(crate) struct UpdateFragment {
type UpdateFleetLock (line 70) | pub(crate) struct UpdateFleetLock {
type UpdatePeriodic (line 77) | pub(crate) struct UpdatePeriodic {
type UpdatePeriodicWindow (line 89) | pub(crate) struct UpdatePeriodicWindow {
function basic_dist_config_sample (line 104) | fn basic_dist_config_sample() {
FILE: src/config/inputs.rs
type ConfigInput (line 12) | pub(crate) struct ConfigInput {
method read_configs (line 22) | pub(crate) fn read_configs(
method merge_fragments (line 44) | pub(crate) fn merge_fragments(fragments: Vec<fragments::ConfigFragment...
type AgentInput (line 76) | pub(crate) struct AgentInput {
method from_fragments (line 81) | fn from_fragments(fragments: Vec<fragments::AgentFragment>) -> Self {
type CincinnatiInput (line 100) | pub(crate) struct CincinnatiInput {
method from_fragments (line 106) | fn from_fragments(fragments: Vec<fragments::CincinnatiFragment>) -> Se...
type IdentityInput (line 122) | pub(crate) struct IdentityInput {
method from_fragments (line 129) | fn from_fragments(fragments: Vec<fragments::IdentityFragment>) -> Self {
type UpdateInput (line 154) | pub(crate) struct UpdateInput {
method from_fragments (line 193) | fn from_fragments(fragments: Vec<fragments::UpdateFragment>) -> Self {
type FleetLockInput (line 169) | pub(crate) struct FleetLockInput {
type PeriodicInput (line 176) | pub(crate) struct PeriodicInput {
type PeriodicIntervalInput (line 186) | pub(crate) struct PeriodicIntervalInput {
FILE: src/config/mod.rs
type Settings (line 28) | pub(crate) struct Settings {
method assemble (line 46) | pub(crate) fn assemble() -> Result<Self> {
method refresh_metrics (line 59) | pub(crate) fn refresh_metrics(&self) {
method validate (line 68) | fn validate(cfg: inputs::ConfigInput) -> Result<Self> {
FILE: src/dbus/experimental.rs
type Experimental (line 10) | pub(crate) struct Experimental {
method moo (line 17) | fn moo(&self, talkative: bool) -> String {
method last_refresh_time (line 26) | fn last_refresh_time(&self) -> fdo::Result<i64> {
FILE: src/dbus/mod.rs
type DBusService (line 14) | pub struct DBusService {
method new (line 21) | fn new(agent_addr: Addr<UpdateAgent>) -> DBusService {
method start (line 29) | pub(crate) fn start(threads: usize, agent_addr: Addr<UpdateAgent>) -> ...
method start_object_server (line 34) | fn start_object_server(&mut self) -> Result<Connection> {
type Context (line 52) | type Context = SyncContext<Self>;
method started (line 54) | fn started(&mut self, _ctx: &mut Self::Context) {
FILE: src/fleet_lock/mock_tests.rs
function test_pre_reboot_lock (line 7) | fn test_pre_reboot_lock() {
function test_pre_reboot_error (line 36) | fn test_pre_reboot_error() {
function test_steady_state_lock (line 61) | fn test_steady_state_lock() {
function test_steady_state_error (line 89) | fn test_steady_state_error() {
FILE: src/fleet_lock/mod.rs
constant DEFAULT_HTTP_COMPLETION_TIMEOUT (line 20) | const DEFAULT_HTTP_COMPLETION_TIMEOUT: Duration = Duration::from_secs(30...
type RemoteJsonError (line 30) | pub struct RemoteJsonError {
type FleetLockError (line 39) | pub enum FleetLockError {
method error_kind (line 52) | pub fn error_kind(&self) -> String {
method error_value (line 62) | pub fn error_value(&self) -> String {
method status_code (line 72) | pub fn status_code(&self) -> Option<u16> {
method fmt (line 81) | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
type Client (line 93) | pub struct Client {
method pre_reboot (line 109) | pub fn pre_reboot(&self) -> impl Future<Output = Result<bool, FleetLoc...
method steady_state (line 126) | pub fn steady_state(&self) -> impl Future<Output = Result<bool, FleetL...
method new_request (line 140) | fn new_request<S: AsRef<str>>(
method map_response (line 155) | async fn map_response(response: reqwest::Response) -> Result<bool, Fle...
type ClientBuilder (line 172) | pub struct ClientBuilder {
method new (line 198) | pub(crate) fn new<T>(api_base: T, identity: &Identity) -> Self
method http_client (line 216) | pub fn http_client(self, hclient: Option<reqwest::Client>) -> Self {
method build (line 223) | pub fn build(self) -> Result<Client> {
type ClientIdentity (line 183) | pub struct ClientIdentity {
type ClientParameters (line 189) | pub struct ClientParameters {
function test_service_rejection_display (line 254) | fn test_service_rejection_display() {
function test_http_error_display (line 280) | fn test_http_error_display() {
FILE: src/identity/mod.rs
type Identity (line 37) | pub(crate) struct Identity {
method with_config (line 57) | pub(crate) fn with_config(cfg: inputs::IdentityInput) -> Result<Self> {
method try_default (line 93) | pub fn try_default() -> Result<Self> {
method url_variables (line 121) | pub fn url_variables(&self) -> HashMap<String, String> {
method cincinnati_params (line 133) | pub fn cincinnati_params(&self) -> HashMap<String, String> {
method mock_default (line 150) | pub(crate) fn mock_default() -> Self {
method validate_group_label (line 172) | fn validate_group_label(&self) -> Result<()> {
function compute_node_uuid (line 188) | fn compute_node_uuid(app_id: &id128::Id128) -> Result<id128::Id128> {
function identity_url_variables (line 199) | fn identity_url_variables() {
function identity_cincinnati_params (line 213) | fn identity_cincinnati_params() {
function identity_validate_group (line 242) | fn identity_validate_group() {
FILE: src/identity/platform.rs
function read_id (line 15) | pub(crate) fn read_id<T>(cmdline_path: T) -> Result<String>
function find_flag_value (line 38) | fn find_flag_value(flagname: &str, cmdline: &str) -> Option<String> {
function test_find_flag (line 68) | fn test_find_flag() {
FILE: src/main.rs
function main (line 38) | fn main() {
function run (line 44) | fn run() -> i32 {
function log_error_chain (line 73) | fn log_error_chain(err_chain: &anyhow::Error) {
FILE: src/metrics/mod.rs
type MetricsService (line 14) | pub struct MetricsService {
method bind_socket (line 20) | pub fn bind_socket() -> Result<Self> {
method bind_socket_at (line 25) | pub(crate) fn bind_socket_at(path: impl AsRef<Path>) -> Result<Self> {
method prometheus_text_encode (line 37) | fn prometheus_text_encode() -> Result<Vec<u8>> {
method error (line 93) | fn error(&mut self, _err: std::io::Error, _ctx: &mut Self::Context) ->...
method finished (line 97) | fn finished(&mut self, _ctx: &mut Self::Context) {}
method handle (line 101) | fn handle(&mut self, item: Connection, ctx: &mut actix::Context<Metric...
type Connection (line 49) | struct Connection {
type Result (line 54) | type Result = ();
type Context (line 58) | type Context = actix::Context<Self>;
method started (line 60) | fn started(&mut self, ctx: &mut actix::Context<Self>) {
function test_bind_socket_at (line 115) | fn test_bind_socket_at() {
FILE: src/rpm_ostree/actor.rs
type StatusCache (line 16) | pub struct StatusCache {
type RpmOstreeClient (line 23) | pub struct RpmOstreeClient {
method start (line 35) | pub fn start(threads: usize) -> Addr<Self> {
type Result (line 54) | type Result = Result<Release>;
method handle (line 56) | fn handle(&mut self, msg: StageDeployment, _ctx: &mut Self::Context) -...
type Result (line 132) | type Result = Result<Release>;
method handle (line 134) | fn handle(&mut self, msg: FinalizeDeployment, _ctx: &mut Self::Context...
type Result (line 154) | type Result = Result<BTreeSet<Release>>;
method handle (line 156) | fn handle(
type Result (line 177) | type Result = Result<Option<(Release, String)>>;
method handle (line 179) | fn handle(
type Result (line 201) | type Result = Result<()>;
method handle (line 203) | fn handle(&mut self, _msg: CleanupPendingDeployment, _ctx: &mut Self::...
type Result (line 219) | type Result = ();
method handle (line 221) | fn handle(&mut self, _msg: RegisterAsDriver, _ctx: &mut Self::Context)...
type Context (line 30) | type Context = SyncContext<Self>;
type StageDeployment (line 42) | pub struct StageDeployment {
type Result (line 50) | type Result = Result<Release>;
type FinalizeDeployment (line 122) | pub struct FinalizeDeployment {
type Result (line 128) | type Result = Result<Release>;
type QueryLocalDeployments (line 144) | pub struct QueryLocalDeployments {
type Result (line 150) | type Result = Result<BTreeSet<Release>>;
type QueryPendingDeploymentStream (line 170) | pub struct QueryPendingDeploymentStream {}
type Result (line 173) | type Result = Result<Option<(Release, String)>>;
type CleanupPendingDeployment (line 194) | pub struct CleanupPendingDeployment {}
type Result (line 197) | type Result = Result<()>;
type RegisterAsDriver (line 212) | pub struct RegisterAsDriver {}
type Result (line 215) | type Result = ();
FILE: src/rpm_ostree/cli_deploy.rs
constant DRIVER_NAME (line 11) | const DRIVER_NAME: &str = "Zincati";
function deploy_locked (line 36) | pub fn deploy_locked(
function deploy_register_driver (line 54) | pub fn deploy_register_driver() {
function invoke_cli_register (line 70) | fn invoke_cli_register() -> Result<()> {
function invoke_cli_deploy (line 102) | fn invoke_cli_deploy(
function invoke_cli_cleanup (line 144) | pub fn invoke_cli_cleanup() -> Result<()> {
function deploy_locked_err (line 164) | fn deploy_locked_err() {
function deploy_locked_ok (line 182) | fn deploy_locked_ok() {
function register_driver_err (line 199) | fn register_driver_err() {
FILE: src/rpm_ostree/cli_finalize.rs
function finalize_deployment (line 19) | pub fn finalize_deployment(release: Release) -> Result<Release> {
FILE: src/rpm_ostree/cli_status.rs
constant OSTREE_DEPLS_PATH (line 19) | const OSTREE_DEPLS_PATH: &str = "/ostree/deploy";
type SystemInoperable (line 50) | pub struct SystemInoperable(String);
method fmt (line 53) | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
type Status (line 62) | pub struct Status {
type Deployment (line 69) | pub struct Deployment {
method into_release (line 92) | pub fn into_release(self) -> Release {
method base_revision (line 105) | pub fn base_revision(&self) -> String {
method get_container_image_reference (line 114) | pub fn get_container_image_reference(&self) -> Option<OstreeImageRefer...
method get_container_image_reference_digest (line 123) | pub fn get_container_image_reference_digest(&self) -> Option<Reference> {
method get_fcos_update_stream (line 137) | pub fn get_fcos_update_stream(&self) -> Result<String> {
type BaseCommitMeta (line 83) | struct BaseCommitMeta {
function parse_booted (line 143) | pub fn parse_booted(status: &Status) -> Result<Release> {
function fedora_coreos_stream_from_deployment (line 148) | fn fedora_coreos_stream_from_deployment(deploy: &Deployment) -> Result<S...
function parse_booted_updates_stream (line 196) | pub fn parse_booted_updates_stream(status: &Status) -> Result<String> {
function parse_pending_deployment (line 202) | pub fn parse_pending_deployment(status: &Status) -> Result<Option<(Relea...
function get_staged_deployment (line 218) | pub fn get_staged_deployment(status: &Status) -> Option<Deployment> {
function parse_local_deployments (line 225) | fn parse_local_deployments(status: &Status, omit_staged: bool) -> BTreeS...
function local_deployments (line 239) | pub fn local_deployments(
function booted_status (line 250) | pub fn booted_status(status: &Status) -> Result<Deployment> {
function get_status (line 264) | fn get_status(client: &mut RpmOstreeClient) -> Result<Rc<Status>> {
function invoke_cli_status (line 289) | pub fn invoke_cli_status(booted_only: bool) -> Result<Status> {
function mock_status (line 346) | fn mock_status(path: &str) -> Result<Status> {
function mock_deployments (line 352) | fn mock_deployments() {
function mock_booted_updates_stream (line 376) | fn mock_booted_updates_stream() {
function mock_booted_oci_deployment (line 392) | fn mock_booted_oci_deployment() {
FILE: src/rpm_ostree/mock_tests.rs
function test_simple_graph (line 8) | fn test_simple_graph() {
function test_downgrade (line 59) | fn test_downgrade() {
FILE: src/rpm_ostree/mod.rs
type Release (line 25) | pub struct Release {
method cmp (line 37) | fn cmp(&self, other: &Self) -> Ordering {
method partial_cmp (line 61) | fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
method from_cincinnati (line 68) | pub fn from_cincinnati(node: Node) -> Result<Self> {
type Payload (line 34) | pub type Payload = Reference;
function release_from_cincinnati (line 106) | fn release_from_cincinnati() {
function invalid_node (line 119) | fn invalid_node() {
function release_cmp (line 157) | fn release_cmp() {
FILE: src/strategy/fleet_lock.rs
type StrategyFleetLock (line 28) | pub(crate) struct StrategyFleetLock {
constant LABEL (line 35) | pub const LABEL: &'static str = "fleet_lock";
method new (line 38) | pub fn new(cfg: inputs::UpdateInput, identity: &Identity) -> Result<Se...
method can_finalize (line 60) | pub(crate) fn can_finalize(&self) -> Pin<Box<dyn Future<Output = Resul...
method report_steady (line 75) | pub(crate) fn report_steady(&self) -> Pin<Box<dyn Future<Output = Resu...
function test_url_simple (line 97) | fn test_url_simple() {
function test_empty_url (line 117) | fn test_empty_url() {
FILE: src/strategy/immediate.rs
type StrategyImmediate (line 12) | pub(crate) struct StrategyImmediate {}
constant LABEL (line 16) | pub const LABEL: &'static str = "immediate";
method can_finalize (line 19) | pub(crate) fn can_finalize(&self) -> Pin<Box<dyn Future<Output = Resul...
method report_steady (line 26) | pub(crate) fn report_steady(&self) -> Pin<Box<dyn Future<Output = Resu...
function report_steady (line 40) | fn report_steady() {
function can_finalize (line 48) | fn can_finalize() {
FILE: src/strategy/mod.rs
type UpdateStrategy (line 50) | pub(crate) enum UpdateStrategy {
method with_config (line 59) | pub(crate) fn with_config(cfg: inputs::UpdateInput, identity: &Identit...
method record_details (line 73) | pub(crate) fn record_details(&self) {
method refresh_metrics (line 79) | pub(crate) fn refresh_metrics(&self) {
method configuration_label (line 95) | fn configuration_label(&self) -> &'static str {
method human_description (line 104) | pub(crate) fn human_description(&self) -> String {
method can_finalize (line 115) | pub(crate) fn can_finalize(&self) -> impl Future<Output = bool> {
method report_steady (line 148) | pub(crate) fn report_steady(&self) -> impl Future<Output = bool> {
method new_immediate (line 164) | fn new_immediate() -> Self {
method new_fleet_lock (line 170) | fn new_fleet_lock(cfg: inputs::UpdateInput, identity: &Identity) -> Re...
method new_periodic (line 176) | fn new_periodic(cfg: inputs::UpdateInput) -> Result<Self> {
method default (line 183) | fn default() -> Self {
FILE: src/strategy/periodic.rs
type StrategyPeriodic (line 20) | pub(crate) struct StrategyPeriodic {
constant LABEL (line 43) | pub const LABEL: &'static str = "periodic";
method new (line 47) | pub fn new(cfg: inputs::UpdateInput) -> Result<Self> {
method tz_name (line 76) | pub fn tz_name(&self) -> &str {
method get_time_zone_info_from_cfg (line 83) | fn get_time_zone_info_from_cfg(cfg: &inputs::PeriodicInput) -> Result<...
method schedule_length_minutes (line 121) | pub(crate) fn schedule_length_minutes(&self) -> u64 {
method human_next_window (line 126) | pub(crate) fn human_next_window(&self) -> String {
method human_remaining (line 144) | pub(crate) fn human_remaining(&self) -> String {
method calendar_summary (line 155) | pub(crate) fn calendar_summary(&self) -> String {
method can_finalize (line 170) | pub(crate) fn can_finalize(&self) -> Pin<Box<dyn Future<Output = Resul...
method report_steady (line 181) | pub(crate) fn report_steady(&self) -> Pin<Box<dyn Future<Output = Resu...
method default (line 31) | fn default() -> Self {
function test_default (line 196) | fn test_default() {
function test_empty_can_finalize (line 202) | fn test_empty_can_finalize() {
function test_report_steady (line 210) | fn test_report_steady() {
function test_periodic_config (line 218) | fn test_periodic_config() {
function test_non_utc_time (line 225) | fn test_non_utc_time() {
function test_localtime (line 271) | fn test_localtime() {
function parse_config_input (line 308) | fn parse_config_input(config_path: &str) -> inputs::ConfigInput {
FILE: src/update_agent/actor.rs
type Context (line 41) | type Context = Context<Self>;
method started (line 43) | fn started(&mut self, ctx: &mut Self::Context) {
type LastRefresh (line 55) | pub struct LastRefresh {}
type Result (line 58) | type Result = i64;
type Result (line 62) | type Result = i64;
method handle (line 64) | fn handle(&mut self, _msg: LastRefresh, _ctx: &mut Self::Context) -> Sel...
type RefreshTick (line 70) | pub(crate) struct RefreshTick {}
type Result (line 73) | type Result = Result<(), Error>;
type Result (line 77) | type Result = ResponseActFuture<Self, Result<(), Error>>;
method handle (line 79) | fn handle(&mut self, _msg: RefreshTick, _ctx: &mut Self::Context) -> Sel...
method tick_now (line 173) | pub fn tick_now(ctx: &mut Context<Self>) {
method tick_later (line 178) | pub fn tick_later(ctx: &mut Context<Self>, after: std::time::Duration) -...
method refresh_delay (line 187) | fn refresh_delay(
method should_tick_immediately (line 206) | fn should_tick_immediately(
method add_jitter (line 226) | fn add_jitter(period: std::time::Duration) -> std::time::Duration {
method log_excluded_depls (line 237) | fn log_excluded_depls(depls: &BTreeSet<Release>, actor: &UpdateAgentInfo) {
method tick_initialize (line 269) | async fn tick_initialize(&self, state: &mut UpdateAgentState) {
method tick_report_steady (line 300) | async fn tick_report_steady(&self, state: &mut UpdateAgentMachineState) {
method tick_check_updates (line 312) | async fn tick_check_updates(&self, state: &mut UpdateAgentState) {
method tick_stage_update (line 339) | async fn tick_stage_update(&self, state: &mut UpdateAgentState, release:...
method tick_finalize_update (line 362) | async fn tick_finalize_update(&self, state: &mut UpdateAgentMachineState...
method tick_end (line 405) | async fn tick_end(&self, state: &mut UpdateAgentMachineState, release: R...
method attempt_deploy (line 413) | async fn attempt_deploy(&self, release: Release) -> Result<Release> {
method deploy_attempt_failed (line 431) | fn deploy_attempt_failed(release: &Release, state: &mut UpdateAgentMachi...
method local_deployments (line 446) | async fn local_deployments(&self) -> Result<BTreeSet<Release>> {
method is_pending_deployment_on_correct_stream (line 465) | async fn is_pending_deployment_on_correct_stream(&self, release: Release...
method finalize_deployment (line 514) | async fn finalize_deployment(&self, release: Release) -> Result<Release> {
method register_as_driver (line 528) | async fn register_as_driver(&self) {
method cleanup_pending_deployment (line 539) | async fn cleanup_pending_deployment(&self) {
method confirm_valid_stream (line 556) | async fn confirm_valid_stream(
function test_should_tick_immediately (line 591) | fn test_should_tick_immediately() {
FILE: src/update_agent/mod.rs
constant DEFAULT_STEADY_INTERVAL_SECS (line 25) | pub(crate) const DEFAULT_STEADY_INTERVAL_SECS: u64 = 300;
constant END_INTERVAL_SECS (line 28) | const END_INTERVAL_SECS: u64 = 10800;
constant DEFAULT_REFRESH_PERIOD_SECS (line 31) | const DEFAULT_REFRESH_PERIOD_SECS: u64 = 300;
constant DEFAULT_POSTPONEMENT_TIME_SECS (line 35) | const DEFAULT_POSTPONEMENT_TIME_SECS: u64 = 60;
constant MAX_DEPLOY_ATTEMPTS (line 39) | const MAX_DEPLOY_ATTEMPTS: u8 = 12;
constant INTERACTIVE_SESSION_OVERRIDE (line 43) | const INTERACTIVE_SESSION_OVERRIDE: &str = "/run/zincati/override-intera...
constant MAX_FINALIZE_POSTPONEMENTS (line 47) | pub(crate) const MAX_FINALIZE_POSTPONEMENTS: u8 = 10;
type SessionJson (line 74) | pub struct SessionJson {
type InteractiveSession (line 80) | pub struct InteractiveSession {
type UpdateAgentMachineState (line 88) | enum UpdateAgentMachineState {
method transition_to (line 127) | fn transition_to(&mut self, state: Self) {
method initialized (line 137) | fn initialized(&mut self) {
method reported_steady (line 151) | fn reported_steady(&mut self) {
method no_new_update (line 165) | fn no_new_update(&mut self) {
method update_available (line 180) | fn update_available(&mut self, update: Release) {
method record_failed_deploy (line 199) | fn record_failed_deploy(&mut self) -> (bool, u8) {
method deploy_failed (line 217) | fn deploy_failed(&mut self, update: Release, fail_count: u8) {
method update_abandoned (line 224) | fn update_abandoned(&mut self) {
method update_staged (line 232) | fn update_staged(&mut self, update: Release) {
method usersessions_can_finalize (line 240) | fn usersessions_can_finalize(&mut self) -> bool {
method handle_interactive_sessions (line 260) | fn handle_interactive_sessions(&self, interactive_sessions: &[Interact...
method record_postponement (line 317) | fn record_postponement(&mut self) {
method reboot_postponed (line 332) | fn reboot_postponed(&mut self, update: Release, postponements_remainin...
method update_finalized (line 339) | fn update_finalized(&mut self, update: Release) {
method end (line 346) | fn end(&mut self) {
method get_refresh_delay (line 354) | fn get_refresh_delay(&self, steady_interval: Duration) -> (Duration, b...
method default (line 118) | fn default() -> Self {
type UpdateAgent (line 376) | pub(crate) struct UpdateAgent {
method with_config (line 419) | pub(crate) fn with_config(cfg: Settings, rpm_ostree_addr: Addr<RpmOstr...
type UpdateAgentState (line 391) | pub(crate) struct UpdateAgentState {
type UpdateAgentInfo (line 400) | pub(crate) struct UpdateAgentInfo {
function broadcast (line 438) | fn broadcast(msg: &str, sessions: &[InteractiveSession]) {
function get_interactive_user_sessions (line 473) | fn get_interactive_user_sessions() -> Result<Vec<InteractiveSession>> {
function format_reboot_warning (line 517) | fn format_reboot_warning(seconds: u64, release_ver: &str) -> String {
function format_seconds (line 532) | fn format_seconds(seconds: u64) -> String {
function default_state (line 565) | fn default_state() {
function state_machine_happy_path (line 573) | fn state_machine_happy_path() {
function test_fsm_abandon_update (line 639) | fn test_fsm_abandon_update() {
function test_fsm_postpone_finalize (line 670) | fn test_fsm_postpone_finalize() {
function test_format_seconds (line 741) | fn test_format_seconds() {
FILE: src/utils.rs
function update_unit_status (line 6) | pub(crate) fn update_unit_status(status: &str) {
function notify_ready (line 12) | pub(crate) fn notify_ready() {
function notify_stopping (line 17) | pub(crate) fn notify_stopping() {
function sd_notify (line 23) | fn sd_notify(state: &[NotifyState]) {
FILE: src/weekly/mod.rs
constant MAX_WEEKLY_MINS (line 22) | pub(crate) const MAX_WEEKLY_MINS: u32 = 7 * 24 * 60;
constant MAX_WEEKLY_SECS (line 25) | pub(crate) const MAX_WEEKLY_SECS: u64 = (MAX_WEEKLY_MINS as u64) * 60;
type MinuteInWeek (line 28) | pub(crate) type MinuteInWeek = u32;
type WeeklyCalendar (line 32) | pub struct WeeklyCalendar {
method new (line 39) | pub fn new(input: Vec<WeeklyWindow>) -> Self {
method contains_datetime (line 50) | pub fn contains_datetime(&self, datetime: &DateTime<impl TimeZone>) ->...
method next_window_minute_in_week (line 59) | pub fn next_window_minute_in_week(
method remaining_to_datetime (line 97) | pub fn remaining_to_datetime(&self, datetime: &DateTime<Utc>) -> Optio...
method human_remaining_duration (line 133) | pub fn human_remaining_duration(remaining: &chrono::Duration) -> Resul...
method length_minutes (line 162) | pub fn length_minutes(&self) -> u64 {
method is_empty (line 194) | pub fn is_empty(&self) -> bool {
method total_length_minutes (line 203) | pub fn total_length_minutes(&self) -> u64 {
method containing_windows (line 211) | pub fn containing_windows(&self, datetime: &DateTime<impl TimeZone>) -...
method serialize (line 221) | fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
method default (line 237) | fn default() -> Self {
type WeeklyWindow (line 246) | pub struct WeeklyWindow {
method parse_timespan (line 258) | pub fn parse_timespan(
method length_minutes (line 311) | pub fn length_minutes(&self) -> u32 {
method range_weekly_minutes (line 317) | pub fn range_weekly_minutes(&self) -> Range<MinuteInWeek> {
method start_minutes (line 327) | fn start_minutes(&self) -> MinuteInWeek {
method end_minutes (line 339) | fn end_minutes(&self) -> MinuteInWeek {
method contains_datetime (line 347) | pub fn contains_datetime(&self, datetime: &DateTime<Utc>) -> bool {
method cmp (line 354) | fn cmp(&self, other: &Self) -> Ordering {
method partial_cmp (line 363) | fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
method eq (line 369) | fn eq(&self, other: &Self) -> bool {
function window_basic (line 380) | fn window_basic() {
function window_split_timespan (line 392) | fn window_split_timespan() {
function calendar_basic (line 404) | fn calendar_basic() {
function window_contains_datetime (line 415) | fn window_contains_datetime() {
function window_week_boundary (line 441) | fn window_week_boundary() {
function calendar_contains_datetime (line 455) | fn calendar_contains_datetime() {
function calendar_whole_week (line 472) | fn calendar_whole_week() {
function calendar_containing_window (line 489) | fn calendar_containing_window() {
function calendar_length (line 507) | fn calendar_length() {
function datetime_remaining (line 528) | fn datetime_remaining() {
function human_remaining (line 552) | fn human_remaining() {
function test_next_window_minute_in_week (line 579) | fn test_next_window_minute_in_week() {
FILE: src/weekly/utils.rs
function weekly_minute_as_weekday_time (line 11) | pub(crate) fn weekly_minute_as_weekday_time(weekly_minute: MinuteInWeek)...
function datetime_as_weekly_minute (line 32) | pub(crate) fn datetime_as_weekly_minute(datetime: &DateTime<impl TimeZon...
function time_as_weekly_minute (line 45) | pub(crate) fn time_as_weekly_minute(day: chrono::Weekday, hour: u8, minu...
function check_duration (line 60) | pub(crate) fn check_duration(length: &Duration) -> Result<()> {
function weekday_from_string (line 72) | pub(crate) fn weekday_from_string(input: &str) -> Result<Weekday> {
function time_from_string (line 101) | pub(crate) fn time_from_string(input: &str) -> Result<(u8, u8)> {
function check_minutes (line 121) | pub(crate) fn check_minutes(minutes: u32) -> Result<Duration> {
function test_check_duration (line 134) | fn test_check_duration() {
function test_check_minutes (line 150) | fn test_check_minutes() {
function test_weekday_from_string (line 165) | fn test_weekday_from_string() {
function test_time_from_string (line 176) | fn test_time_from_string() {
function test_weekly_minute_as_weekday_time (line 190) | fn test_weekly_minute_as_weekday_time() {
Condensed preview — 101 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (484K chars).
[
{
"path": ".cci.jenkinsfile",
"chars": 637,
"preview": "// Documentation: https://github.com/coreos/coreos-ci/blob/main/README-upstream-ci.md\n\nproperties([\n // abort previou"
},
{
"path": ".gemini/config.yaml",
"chars": 321,
"preview": "# This config mainly overrides `summary: false` by default\n# as it's really noisy.\nhave_fun: true\ncode_review:\n disable"
},
{
"path": ".github/ISSUE_TEMPLATE/bug.md",
"chars": 258,
"preview": "---\nname: Bug report\nabout: Report an issue\n---\n\n# Bug Report #\n\n## Environment ##\n\nWhat hardware/cloud provider/hypervi"
},
{
"path": ".github/ISSUE_TEMPLATE/feature.md",
"chars": 151,
"preview": "---\nname: Feature request\nabout: Suggest an enhancement\n---\n\n# Feature Request #\n\n## Desired Feature ##\n\n## Example Usag"
},
{
"path": ".github/ISSUE_TEMPLATE/release-checklist.md",
"chars": 6300,
"preview": "---\nname: release checklist\nabout: release checklist template\ntitle: New release for zincati\nlabels: jira,kind/release\nw"
},
{
"path": ".github/dependabot.yml",
"chars": 661,
"preview": "# Maintained in https://github.com/coreos/repo-templates\n# Do not edit downstream.\n\n# Updates are grouped together by ec"
},
{
"path": ".github/workflows/containers.yml",
"chars": 402,
"preview": "---\nname: Containers\n\non:\n push:\n branches: [main]\n pull_request:\n branches: [main]\n\npermissions:\n contents: re"
},
{
"path": ".github/workflows/rust.yml",
"chars": 3821,
"preview": "# Maintained in https://github.com/coreos/repo-templates\n# Do not edit downstream.\n\nname: Rust\non:\n push:\n branches:"
},
{
"path": ".gitignore",
"chars": 62,
"preview": "/target\n**/*.rs.bk\nvendor\njsonnetfile.lock.json\ndashboard_out\n"
},
{
"path": ".packit.yaml",
"chars": 912,
"preview": "# See the documentation for more information:\n# https://packit.dev/docs/configuration/\nactions:\n changelog-entry:\n -"
},
{
"path": "COPYRIGHT",
"chars": 203,
"preview": "Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/\nUpstream-Name: zincati\nSource: https://www.gi"
},
{
"path": "Cargo.toml",
"chars": 1895,
"preview": "[package]\nname = \"zincati\"\nversion = \"0.0.32\"\ndescription = \"Update agent for Fedora CoreOS\"\nhomepage = \"https://coreos."
},
{
"path": "DCO",
"chars": 1422,
"preview": "Developer Certificate of Origin\nVersion 1.1\n\nCopyright (C) 2004, 2006 The Linux Foundation and its contributors.\n660 Yor"
},
{
"path": "LICENSE",
"chars": 11358,
"preview": "\n Apache License\n Version 2.0, January 2004\n "
},
{
"path": "Makefile",
"chars": 1063,
"preview": "RELEASE ?= 0\nTARGETDIR ?= target\n\nifeq ($(RELEASE),1)\n\tPROFILE ?= release\n\tCARGO_ARGS = --release\nelse\n\tPROFILE ?= debug"
},
{
"path": "README.md",
"chars": 1581,
"preview": "# Zincati\n\n[](https://crates.io/crates/zincati)\n\nZincati is an "
},
{
"path": "contrib/monitoring-mixins/README.md",
"chars": 696,
"preview": "# Requirements\n\nIn order to customize and generate monitoring artifacts, the following tools are required:\n\n * `jb` avai"
},
{
"path": "contrib/monitoring-mixins/dashboards/dashboards.libsonnet",
"chars": 3230,
"preview": "local grafana = import 'github.com/grafana/grafonnet-lib/grafonnet/grafana.libsonnet';\nlocal dashboard = grafana.dashboa"
},
{
"path": "contrib/monitoring-mixins/jsonnetfile.json",
"chars": 296,
"preview": "{\n \"version\": 1,\n \"dependencies\": [\n {\n \"source\": {\n \"git\": {\n \"remote\": \"https://github.com/g"
},
{
"path": "contrib/monitoring-mixins/mixin.libsonnet",
"chars": 43,
"preview": "(import 'dashboards/dashboards.libsonnet')\n"
},
{
"path": "dist/bin/zincati-update-now",
"chars": 627,
"preview": "#!/bin/bash\nset -euo pipefail\n\necho \"WARN: This command is experimental and subject to change.\" >&2\n\nif [ \"$EUID\" != \"0\""
},
{
"path": "dist/config.d/10-agent.toml",
"chars": 139,
"preview": "# Configure agent timing.\n[agent.timing]\n\n# Pausing interval between updates checks in steady mode, in seconds.\nsteady_i"
},
{
"path": "dist/config.d/10-auto-updates.toml",
"chars": 358,
"preview": "# Enable auto-updates.\n[updates]\n\n# Boolean to enable auto-update logic.\n# There is almost no case where disabling this "
},
{
"path": "dist/config.d/10-identity.toml",
"chars": 583,
"preview": "# Configure agent identity.\n[identity]\n\n# Node group, used for cluster-wide reboot orchestration (e.g. Airlock).\ngroup ="
},
{
"path": "dist/config.d/30-updates-strategy.toml",
"chars": 697,
"preview": "# How to finalize updates.\n[updates]\n\n# String to customize update strategy.\n# Default strategy is to immediately finali"
},
{
"path": "dist/config.d/50-fedora-coreos-cincinnati.toml",
"chars": 101,
"preview": "# Fedora CoreOS Cincinnati backend\n[cincinnati]\nbase_url= \"https://updates.coreos.fedoraproject.org\"\n"
},
{
"path": "dist/dbus-1/system.d/org.coreos.zincati.conf",
"chars": 1106,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?> <!-- -*- XML -*- -->\n\n<!DOCTYPE busconfig PUBLIC\n \"-//freedesktop//DTD D"
},
{
"path": "dist/polkit-1/actions/org.coreos.zincati.deadend.policy",
"chars": 697,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE policyconfig PUBLIC\n \"-//freedesktop//DTD PolicyKit Policy Configuratio"
},
{
"path": "dist/polkit-1/rules.d/zincati.rules",
"chars": 751,
"preview": "// Allow Zincati to deploy, finalize, and cleanup a staged deployment through rpm-ostree.\npolkit.addRule(function(action"
},
{
"path": "dist/systemd/system/zincati.service",
"chars": 936,
"preview": "[Unit]\nDescription=Zincati Update Agent\nDocumentation=https://github.com/coreos/zincati\n# Skip live systems not meant to"
},
{
"path": "dist/sysusers.d/50-zincati.conf",
"chars": 125,
"preview": "# Zincati - https://github.com/coreos/zincati\n# Type Name ID GECOS\nu zincati - \"Zincati user for auto-upda"
},
{
"path": "dist/tmpfiles.d/zincati.conf",
"chars": 530,
"preview": "#Type Path Mode User Group Age Argument\nd /run/zincati 0775 zincati zincati - -"
},
{
"path": "docs/_config.yml",
"chars": 1300,
"preview": "# Template generated by https://github.com/coreos/repo-templates; do not edit downstream\n\n# To test documentation change"
},
{
"path": "docs/_sass/color_schemes/coreos.scss",
"chars": 22,
"preview": "$link-color: #53a3da;\n"
},
{
"path": "docs/contributing.md",
"chars": 507,
"preview": "---\nnav_order: 9\n---\n\n# Contributing\n\n## Project architecture\n\n[Development doc-pages][devdocs] cover several aspects of"
},
{
"path": "docs/development/agent-actor-system.md",
"chars": 3333,
"preview": "---\nnav_order: 2\nparent: Development\n---\n\n# Actor model and agent subsystems\n\nThe Zincati `agent` command provides a lon"
},
{
"path": "docs/development/cincinnati/protocol.md",
"chars": 4196,
"preview": "---\ntitle: Cincinnati for Fedora CoreOS\nparent: Development\nnav_order: 3\n---\n\n# Cincinnati for Fedora CoreOS\n\nCincinnati"
},
{
"path": "docs/development/cincinnati/response.json",
"chars": 1185,
"preview": "{\n \"nodes\": [\n {\n \"version\": \"32.20200517.1.0\",\n \"metadata\": {\n \"org.fedoraproject.coreos.releases."
},
{
"path": "docs/development/fleetlock/protocol.md",
"chars": 4524,
"preview": "---\nparent: Development\nnav_order: 4\n---\n\n# FleetLock protocol\n\nThis document describes an HTTP-based protocol for orche"
},
{
"path": "docs/development/os-metadata.md",
"chars": 1257,
"preview": "---\nnav_order: 5\nparent: Development\n---\n\n# OS metadata and agent identity\n\nThe agent needs to derive its own identity f"
},
{
"path": "docs/development/quickstart.md",
"chars": 3094,
"preview": "---\nnav_order: 1\nparent: Development\n---\n\n# Development quickstart\n\nThis is quick start guide for developing and buildin"
},
{
"path": "docs/development/testing.md",
"chars": 1944,
"preview": "---\nnav_order: 7\nparent: Development\n---\n\n# Testing\n\n## Unit Tests\nUnit tests can be run using `make check` (via `cargo "
},
{
"path": "docs/development/update-strategy-periodic.md",
"chars": 2729,
"preview": "---\nnav_order: 6\nparent: Development\n---\n\n# Periodic update strategy\n\nThe agent supports a `periodic` strategy, which al"
},
{
"path": "docs/development.md",
"chars": 55,
"preview": "---\nnav_order: 3\nhas_children: true\n---\n\n# Development\n"
},
{
"path": "docs/images/zincati-actors.dot",
"chars": 3020,
"preview": "# Render with: `dot -T png -o zincati-actors.png zincati-actors.dot`\n\ndigraph actors_messages {\n newrank = true;\n "
},
{
"path": "docs/images/zincati-fleetlock.msc",
"chars": 2015,
"preview": "# Render with: `mscgen -T svg -i zincati-fleetlock.msc`\n\nmsc {\n \"OS\", \"Zincati agent\", \"FleetLock service\", \"rpm-ostree"
},
{
"path": "docs/images/zincati-fsm.dot",
"chars": 1480,
"preview": "# Render with: `dot -T png -o zincati-fsm.png zincati-fsm.dot`\n# The `dot` program is included in Graphviz: https://grap"
},
{
"path": "docs/index.md",
"chars": 1532,
"preview": "---\nnav_order: 1\n---\n\n# Zincati\n\n[](https://crates.io/crates/zi"
},
{
"path": "docs/usage/agent-identity.md",
"chars": 2173,
"preview": "---\nparent: Usage\n---\n\n# Agent identity\n\nZincati agent tries to derive a unique identity for the machine it is running o"
},
{
"path": "docs/usage/auto-updates.md",
"chars": 5170,
"preview": "---\nparent: Usage\n---\n\n# Auto-updates\n\nAvailable updates are discovered by periodically polling a [Cincinnati] server.\nO"
},
{
"path": "docs/usage/configuration.md",
"chars": 1946,
"preview": "---\nparent: Usage\n---\n\n# Configuration\n\nZincati supports runtime customization via configuration fragments (dropins), al"
},
{
"path": "docs/usage/logging.md",
"chars": 1938,
"preview": "---\nparent: Usage\n---\n\n# Logging\n\nZincati supports logging at multiple levels (trace, debug, info, warning, error). Usua"
},
{
"path": "docs/usage/metrics.md",
"chars": 1591,
"preview": "---\nparent: Usage\n---\n\n# Metrics\n\nZincati tracks and exposes some of its internal metrics, in order to ease monitoring t"
},
{
"path": "docs/usage/updates-strategy.md",
"chars": 9326,
"preview": "---\nparent: Usage\n---\n\n# Updates strategy\n\nTo minimize service disruption, Zincati allows administrators to control when"
},
{
"path": "docs/usage.md",
"chars": 49,
"preview": "---\nnav_order: 2\nhas_children: true\n---\n\n# Usage\n"
},
{
"path": "src/cincinnati/client.rs",
"chars": 10045,
"preview": "//! Asynchronous Cincinnati client.\n//!\n//! This client implements the [Cincinnati protocol] for update-hints.\n//!\n//! ["
},
{
"path": "src/cincinnati/mock_tests.rs",
"chars": 806,
"preview": "use crate::cincinnati::*;\nuse crate::identity::Identity;\nuse mockito::{self, Matcher};\nuse std::collections::BTreeSet;\nu"
},
{
"path": "src/cincinnati/mod.rs",
"chars": 14393,
"preview": "//! Asynchronous Cincinnati client.\n\n// Cincinnati client.\nmod client;\npub use client::{CincinnatiError, Node};\n\n#[cfg(t"
},
{
"path": "src/cli/agent.rs",
"chars": 2877,
"preview": "//! Logic for the `agent` subcommand.\n\nuse super::ensure_user;\nuse crate::{config, dbus, metrics, rpm_ostree, update_age"
},
{
"path": "src/cli/deadend.rs",
"chars": 4997,
"preview": "//! Logic for the `deadend` subcommand.\n\nuse super::ensure_user;\nuse anyhow::{Context, Result};\nuse clap::Subcommand;\nus"
},
{
"path": "src/cli/ex.rs",
"chars": 1588,
"preview": "//! Logic for the ex subcommand.\n\nuse super::ensure_user;\nuse anyhow::Result;\nuse clap::Subcommand;\nuse fn_error_context"
},
{
"path": "src/cli/mod.rs",
"chars": 1732,
"preview": "//! Command-Line Interface (CLI) logic.\n\nmod agent;\nmod deadend;\nmod ex;\n\nuse anyhow::Result;\nuse clap::{ArgAction, Pars"
},
{
"path": "src/config/fragments.rs",
"chars": 5390,
"preview": "//! TOML configuration fragments.\n\nuse ordered_float::NotNan;\nuse serde::Deserialize;\nuse std::collections::BTreeSet;\nus"
},
{
"path": "src/config/inputs.rs",
"chars": 7351,
"preview": "use crate::config::fragments;\nuse crate::update_agent::DEFAULT_STEADY_INTERVAL_SECS;\nuse anyhow::{Context, Result};\nuse "
},
{
"path": "src/config/mod.rs",
"chars": 2846,
"preview": "//! Configuration parsing and validation.\n//!\n//! This module contains the following logical entities:\n//! * Fragments:"
},
{
"path": "src/dbus/experimental.rs",
"chars": 1344,
"preview": "//! Experimental interface.\n\nuse crate::update_agent::{LastRefresh, UpdateAgent};\nuse actix::Addr;\nuse futures::prelude:"
},
{
"path": "src/dbus/mod.rs",
"chars": 1789,
"preview": "//! D-Bus service actor.\n\nmod experimental;\nuse experimental::Experimental;\n\nuse crate::update_agent::UpdateAgent;\nuse a"
},
{
"path": "src/fleet_lock/mock_tests.rs",
"chars": 2978,
"preview": "use crate::fleet_lock::*;\nuse crate::identity::Identity;\nuse mockito::Matcher;\nuse tokio::runtime as rt;\n\n#[test]\nfn tes"
},
{
"path": "src/fleet_lock/mod.rs",
"chars": 9743,
"preview": "//! Asynchronous FleetLock client, remote lock management.\n//!\n//! This module implements a client for FleetLock, a bare"
},
{
"path": "src/identity/mod.rs",
"chars": 8815,
"preview": "mod platform;\n\nuse crate::config::inputs;\nuse crate::rpm_ostree;\nuse anyhow::{anyhow, ensure, Context, Result};\nuse fn_e"
},
{
"path": "src/identity/platform.rs",
"chars": 2854,
"preview": "//! Kernel cmdline parsing - utility functions\n//!\n//! NOTE(lucab): this is not a complete/correct cmdline parser, as it"
},
{
"path": "src/main.rs",
"chars": 1979,
"preview": "//! Agent for Fedora CoreOS auto-updates.\n\n#![deny(missing_debug_implementations)]\n#![deny(missing_docs)]\n\n#[macro_use(f"
},
{
"path": "src/metrics/mod.rs",
"chars": 4122,
"preview": "//! Metrics endpoint over a Unix-domain socket.\n\nuse actix::prelude::*;\nuse anyhow::{bail, Context, Result};\nuse std::os"
},
{
"path": "src/rpm_ostree/actor.rs",
"chars": 8239,
"preview": "//! rpm-ostree client actor.\n\nuse super::cli_status::Status;\nuse super::Release;\nuse actix::prelude::*;\nuse anyhow::{Con"
},
{
"path": "src/rpm_ostree/cli_deploy.rs",
"chars": 6715,
"preview": "//! Interface to `rpm-ostree deploy --lock-finalization` and\n//! `rpm-ostree deploy --register-driver`.\n\nuse crate::rpm_"
},
{
"path": "src/rpm_ostree/cli_finalize.rs",
"chars": 1365,
"preview": "//! Interface to `rpm-ostree finalize-deployment`.\n\nuse super::Release;\nuse anyhow::{anyhow, bail, Context, Result};\nuse"
},
{
"path": "src/rpm_ostree/cli_status.rs",
"chars": 15234,
"preview": "//! Interface to `rpm-ostree status --json`.\n\nuse super::actor::{RpmOstreeClient, StatusCache};\nuse super::Release;\nuse "
},
{
"path": "src/rpm_ostree/mock_tests.rs",
"chars": 2873,
"preview": "use crate::cincinnati::Cincinnati;\nuse crate::identity::Identity;\nuse mockito::{self, Matcher};\nuse std::collections::BT"
},
{
"path": "src/rpm_ostree/mod.rs",
"chars": 5832,
"preview": "mod cli_deploy;\nmod cli_finalize;\nmod cli_status;\npub use cli_status::{\n invoke_cli_status, parse_booted, parse_boote"
},
{
"path": "src/strategy/fleet_lock.rs",
"chars": 4409,
"preview": "//! Strategy for fleet-wide coordinated updates (FleetLock protocol).\n\nuse crate::config::inputs;\nuse crate::fleet_lock:"
},
{
"path": "src/strategy/immediate.rs",
"chars": 1469,
"preview": "//! Strategy for immediate updates.\n\nuse anyhow::Error;\nuse futures::future;\nuse futures::prelude::*;\nuse log::trace;\nus"
},
{
"path": "src/strategy/mod.rs",
"chars": 6585,
"preview": "//! Update and reboot strategies.\n\nuse crate::config::inputs;\nuse crate::identity::Identity;\nuse anyhow::Result;\nuse fn_"
},
{
"path": "src/strategy/periodic.rs",
"chars": 12682,
"preview": "//! Strategy for periodic (weekly) updates.\n\nuse crate::config::inputs;\nuse crate::weekly::{utils, WeeklyCalendar, Weekl"
},
{
"path": "src/update_agent/actor.rs",
"chars": 24049,
"preview": "//! Update agent actor.\n\nuse super::{UpdateAgent, UpdateAgentInfo, UpdateAgentMachineState, UpdateAgentState};\nuse crate"
},
{
"path": "src/update_agent/mod.rs",
"chars": 28244,
"preview": "//! Update agent.\n\nmod actor;\npub use actor::LastRefresh;\n\nuse crate::cincinnati::Cincinnati;\nuse crate::config::Setting"
},
{
"path": "src/utils.rs",
"chars": 1082,
"preview": "//! Miscellaneous utility functions.\n\nuse libsystemd::daemon::{notify, NotifyState};\n\n/// Helper function to update unit"
},
{
"path": "src/weekly/mod.rs",
"chars": 23218,
"preview": "//! Calendar-windows for events recurring on weekly basis.\n//!\n//! This contains helper logic to handle intervals of tim"
},
{
"path": "src/weekly/utils.rs",
"chars": 7344,
"preview": "//! Utilities for weekly-time related logic.\n\nuse crate::weekly::{MinuteInWeek, MAX_WEEKLY_MINS, MAX_WEEKLY_SECS};\nuse a"
},
{
"path": "tests/fixtures/00-config-sample.toml",
"chars": 572,
"preview": "[agent.timing]\nsteady_interval_secs = 35\n\n[identity]\ngroup = \"workers\"\nnode_uuid = \"27e3ac02af3946af995c9940e18b0cce\"\nro"
},
{
"path": "tests/fixtures/20-periodic-sample.toml",
"chars": 315,
"preview": "[[updates.periodic.window]]\ndays = [ \"Tue\", \"Wed\" ]\nstart_time = \"23:00\"\nlength_minutes = 120\n\n[[updates.periodic.window"
},
{
"path": "tests/fixtures/30-periodic-sample-non-utc.toml",
"chars": 169,
"preview": "[updates]\nstrategy = \"periodic\"\n\n[updates.periodic]\ntime_zone = \"America/Toronto\"\n\n[[updates.periodic.window]]\ndays = [ "
},
{
"path": "tests/fixtures/31-periodic-sample-non-utc.toml",
"chars": 162,
"preview": "[updates]\nstrategy = \"periodic\"\n\n[updates.periodic]\ntime_zone = \"localtime\"\n\n[[updates.periodic.window]]\ndays = [ \"Wed\" "
},
{
"path": "tests/fixtures/rpm-ostree-staged.json",
"chars": 54949,
"preview": "{\n \"deployments\" : [\n {\n \"unlocked\" : \"none\",\n \"requested-local-packages\" : [],\n \"base-commit-meta\" :"
},
{
"path": "tests/fixtures/rpm-ostree-status-annotation.json",
"chars": 26795,
"preview": "{\n \"deployments\" : [\n {\n \"unlocked\" : \"none\",\n \"requested-local-packages\" : [],\n \"base-commit-meta\" :"
},
{
"path": "tests/fixtures/rpm-ostree-status.json",
"chars": 26835,
"preview": "{\n \"deployments\" : [\n {\n \"unlocked\" : \"none\",\n \"requested-local-packages\" : [],\n \"base-commit-meta\" :"
},
{
"path": "tests/kola/common/libtest.sh",
"chars": 1508,
"preview": "# Source library for shell script tests\n# Copyright (C) 2020 Red Hat, Inc.\n# SPDX-License-Identifier: Apache-2.0\n\nrunv()"
},
{
"path": "tests/kola/dbus/test-experimental.sh",
"chars": 1546,
"preview": "#!/bin/bash \n\n# Tests for the `org.coreos.zincati.Experimental` interface.\n\nset -xeuo pipefail\n\n. ${KOLA_EXT_DATA}/l"
},
{
"path": "tests/kola/misc/test-status.sh",
"chars": 1445,
"preview": "#!/bin/bash \n\n# Simple sanity checks for systemd unit status updates.\n\nset -xeuo pipefail\n\n. ${KOLA_EXT_DATA}/libtes"
},
{
"path": "tests/kola/server/config.fcc",
"chars": 856,
"preview": "variant: fcos\nversion: 1.1.0\nsystemd:\n units:\n - name: kolet-httpd.path\n enabled: true\n contents: |\n "
},
{
"path": "tests/kola/server/test-deadend-release.sh",
"chars": 1466,
"preview": "#!/bin/bash \n\n# Test to check for correct detection of a dead-end release.\n\nset -xeuo pipefail\n\ncd $(mktemp -d)\n\n# P"
},
{
"path": "tests/kola/server/test-stream.sh",
"chars": 4841,
"preview": "#!/bin/bash \n\n# Test to check for correct detection of incorrect stream metadata in new releases and denylist functi"
}
]
About this extraction
This page contains the full source code of the coreos/zincati GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 101 files (442.2 KB), approximately 126.9k tokens, and a symbol index with 390 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.