countme: Add DNF Count Me support
Add support for the DNF Count Me feature [1,2,3] as a standalone rpm-ostree subcommand called weekly via a systemd timer. [1] https://fedoraproject.org/wiki/Changes/DNF_Better_Counting [2] https://dnf.readthedocs.io/en/latest/conf_ref.html?highlight=countme#options-for-both-main-and-repo [3] https://github.com/rpm-software-management/ci-dnf-stack/blob/master/dnf-behave-tests/features/countme.feature
This commit is contained in:
parent
0e9941495e
commit
6cfdb1f585
@ -7,7 +7,7 @@ edition = "2018"
|
||||
[dependencies]
|
||||
anyhow = "1.0.35"
|
||||
paste = "1.0"
|
||||
serde = "1.0.118"
|
||||
serde = { version = "1.0.118", features = ["derive"] }
|
||||
serde_derive = "1.0.118"
|
||||
serde_json = "1.0.60"
|
||||
serde_yaml = "0.8.14"
|
||||
@ -38,6 +38,8 @@ subprocess = "0.2.6"
|
||||
chrono = { version = "0.4.19", features = ["serde"] }
|
||||
libdnf-sys = { path = "rust/libdnf-sys", version = "0.1.0" }
|
||||
memfd = "0.3.0"
|
||||
rust-ini = "0.16.0"
|
||||
os-release = "0.1.0"
|
||||
|
||||
[build-dependencies]
|
||||
cbindgen = "0.16.0"
|
||||
|
@ -17,7 +17,11 @@
|
||||
|
||||
man1_MANS = rpm-ostree.1
|
||||
man5_MANS = rpm-ostreed.conf.5
|
||||
man8_MANS = rpm-ostreed-automatic.service.8 rpm-ostreed-automatic.timer.8
|
||||
man8_MANS = rpm-ostreed-automatic.service.8 \
|
||||
rpm-ostreed-automatic.timer.8 \
|
||||
rpm-ostree-countme.service.8 \
|
||||
rpm-ostree-countme.timer.8
|
||||
|
||||
|
||||
XSLTPROC_FLAGS = \
|
||||
--nonet \
|
||||
@ -39,4 +43,7 @@ rpm-ostreed.conf.5: man/rpm-ostreed.conf.xml Makefile
|
||||
rpm-ostreed-automatic.service.8 rpm-ostreed-automatic.timer.8: man/rpm-ostreed-automatic.xml Makefile
|
||||
$(AM_V_GEN) $(XSLTPROC) $(XSLTPROC_FLAGS_MAN) $<
|
||||
|
||||
rpm-ostree-countme.service.8 rpm-ostree-countme.timer.8: man/rpm-ostree-countme.xml Makefile
|
||||
$(AM_V_GEN) $(XSLTPROC) $(XSLTPROC_FLAGS_MAN) $<
|
||||
|
||||
CLEANFILES += $(man1_MANS) $(man5_MANS) $(man8_MANS)
|
||||
|
@ -32,8 +32,9 @@ librpmostreeinternals_la_SOURCES = \
|
||||
src/app/rpmostree-builtin-reload.cxx \
|
||||
src/app/rpmostree-builtin-rebase.cxx \
|
||||
src/app/rpmostree-builtin-cancel.cxx \
|
||||
src/app/rpmostree-builtin-cliwrap.cxx \
|
||||
src/app/rpmostree-builtin-cleanup.cxx \
|
||||
src/app/rpmostree-builtin-cliwrap.cxx \
|
||||
src/app/rpmostree-builtin-countme.cxx \
|
||||
src/app/rpmostree-builtin-initramfs.cxx \
|
||||
src/app/rpmostree-builtin-initramfs-etc.cxx \
|
||||
src/app/rpmostree-builtin-livefs.cxx \
|
||||
|
41
docs/countme.md
Normal file
41
docs/countme.md
Normal file
@ -0,0 +1,41 @@
|
||||
---
|
||||
nav_order: 4
|
||||
---
|
||||
|
||||
# DNF Count Me support
|
||||
{: .no_toc }
|
||||
|
||||
1. TOC
|
||||
{:toc}
|
||||
|
||||
Classic DNF based operating systems can use the [DNF Count Me feature][countme]
|
||||
to anonymously report how long a system has been running without impacting the
|
||||
user privacy. This is implemented as an additional `countme` variable added to
|
||||
requests made to fetch RPM repository metadata. On those systems, this value is
|
||||
added randomly to requests made automatically via the `dnf-makecache.timer` or
|
||||
via explicit calls to `dnf update` or `dnf install`.
|
||||
|
||||
However, this does not work for `rpm-ostree` based systems as in the default
|
||||
case (no package overlayed on top of the base commit), `rpm-ostree` will not
|
||||
fetch any RPM repository metadata at all.
|
||||
|
||||
Thus `rpm-ostree` includes a distinct timer (`rpm-ostree-countme.timer`),
|
||||
triggered weekly, that implement the DNF Count Me functionality in a
|
||||
standalone way.
|
||||
|
||||
## Disabling DNF Count Me on a system
|
||||
|
||||
To disable this feature, you need to update the Count Me option in all RPM
|
||||
repository configuration files:
|
||||
|
||||
```
|
||||
sed -i 's/countme=1/countme=0/g' /etc/yum.repos.d/*.repo
|
||||
```
|
||||
|
||||
You should also stop and disable the `rpm-ostree-countme.timer`:
|
||||
|
||||
```
|
||||
$ systemctl disable --now rpm-ostree-countme.timer
|
||||
```
|
||||
|
||||
[countme]: https://fedoraproject.org/wiki/Changes/DNF_Better_Counting
|
80
man/rpm-ostree-countme.xml
Normal file
80
man/rpm-ostree-countme.xml
Normal file
@ -0,0 +1,80 @@
|
||||
<?xml version='1.0'?> <!--*-nxml-*-->
|
||||
<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.2//EN"
|
||||
"http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd">
|
||||
|
||||
<!--
|
||||
Copyright 2020 Colin Walters <walters@verbum.org>
|
||||
Copyright 2020 Timothée Ravier <travier@redhat.com>
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 2 of the License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library; if not, write to the
|
||||
Free Software Foundation, Inc., 59 Temple Place - Suite 330,
|
||||
Boston, MA 02111-1307, USA.
|
||||
-->
|
||||
|
||||
<refentry id="rpm-ostree-countme.service">
|
||||
|
||||
<refentryinfo>
|
||||
<title>rpm-ostree-countme</title>
|
||||
<productname>rpm-ostree</productname>
|
||||
|
||||
<authorgroup>
|
||||
<author>
|
||||
<contrib>Developer</contrib>
|
||||
<firstname>Timothée</firstname>
|
||||
<surname>Ravier</surname>
|
||||
<email>travier@redhat.com</email>
|
||||
</author>
|
||||
</authorgroup>
|
||||
</refentryinfo>
|
||||
|
||||
<refmeta>
|
||||
<refentrytitle>rpm-ostree-countme</refentrytitle>
|
||||
<manvolnum>8</manvolnum>
|
||||
</refmeta>
|
||||
|
||||
<refnamediv>
|
||||
<refname>rpm-ostree-countme.service</refname>
|
||||
<refname>rpm-ostree-countme.timer</refname>
|
||||
<refpurpose>DNF Count Me support in rpm-ostree</refpurpose>
|
||||
</refnamediv>
|
||||
|
||||
<refsynopsisdiv>
|
||||
<para><filename>rpm-ostree-countme.service</filename></para>
|
||||
<para><filename>rpm-ostree-countme.timer</filename></para>
|
||||
</refsynopsisdiv>
|
||||
|
||||
<refsect1>
|
||||
<title>Description</title>
|
||||
<para>
|
||||
The service unit enacts the Count Me reporting implemented as a hidden command in rpm-ostree.
|
||||
</para>
|
||||
|
||||
<para>
|
||||
The timer unit triggers Count Me reporting weekly at a random time.
|
||||
Disabling or masking this unit effectively disables Count Me reporting, regardless of the setting in RPM repository configuration files.
|
||||
See <citerefentry><refentrytitle>systemd.timer</refentrytitle><manvolnum>5</manvolnum></citerefentry>
|
||||
for more information on how to control systemd timers.
|
||||
</para>
|
||||
</refsect1>
|
||||
|
||||
<refsect1>
|
||||
<title>See Also</title>
|
||||
|
||||
<para>
|
||||
<citerefentry><refentrytitle>rpm-ostree</refentrytitle><manvolnum>1</manvolnum></citerefentry>,
|
||||
<citerefentry><refentrytitle>dnf</refentrytitle><manvolnum>8</manvolnum></citerefentry>
|
||||
</para>
|
||||
</refsect1>
|
||||
|
||||
</refentry>
|
124
rust/src/countme.rs
Normal file
124
rust/src/countme.rs
Normal file
@ -0,0 +1,124 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Red Hat, Inc.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use curl::easy::Easy;
|
||||
use os_release::OsRelease;
|
||||
use std::path;
|
||||
|
||||
mod cookie;
|
||||
mod repo;
|
||||
|
||||
/// Default variant name used in User Agent
|
||||
const DEFAULT_VARIANT_ID: &str = "unknown";
|
||||
|
||||
/// Send a request to 'url' with 'ua' as User Agent.
|
||||
/// This sends a GET request and discards the body as this is what is currently
|
||||
/// expected on the Fedora infrastructure side.
|
||||
/// Once this is fixed, we can switch to a HEAD request to reduce the footprint:
|
||||
/// let mut handle = Easy::new().nobody(true)?;
|
||||
fn send_countme(url: &str, ua: &str) -> Result<()> {
|
||||
println!("Sending request to: {}", url);
|
||||
let mut handle = Easy::new();
|
||||
handle.follow_location(true)?;
|
||||
handle.fail_on_error(true)?;
|
||||
handle.url(&url)?;
|
||||
handle.useragent(&ua)?;
|
||||
{
|
||||
let mut transfer = handle.transfer();
|
||||
transfer.write_function(|new_data| Ok(new_data.len()))?;
|
||||
transfer.perform()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Main entrypoint for countme
|
||||
pub(crate) fn countme_entrypoint(_args: Vec<String>) -> Result<()> {
|
||||
// Silently skip if we are not run on an ostree booted system
|
||||
if !path::Path::new("/run/ostree-booted").exists() {
|
||||
bail!("Not running on an ostree based system");
|
||||
}
|
||||
|
||||
// Load repo configs and keep only those enabled, with a metalink and countme=1
|
||||
let repos: Vec<_> = self::repo::all()?
|
||||
.into_iter()
|
||||
.filter(|r| r.count_me())
|
||||
.collect();
|
||||
if repos.is_empty() {
|
||||
println!("No enabled repositories with countme=1");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Load timestamp cookie
|
||||
let cookie = cookie::Cookie::new().context("Could not read existing cookie")?;
|
||||
|
||||
// Skip this run if we are not in a new counting window
|
||||
if cookie.existing_window() {
|
||||
println!("Skipping: Not in a new counting window");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Read /etc/os-release
|
||||
let release: OsRelease = OsRelease::new()?;
|
||||
let variant: &str = release
|
||||
.extra
|
||||
.get("VARIANT_ID")
|
||||
.map_or(DEFAULT_VARIANT_ID, |s| s);
|
||||
|
||||
// Setup User Agent. The format is:
|
||||
// libdnf (NAME VERSION_ID; VARIANT_ID; OS.BASEARCH)
|
||||
// libdnf (Fedora 31; server; Linux.x86_64)
|
||||
// See `user_agent` option in:
|
||||
// https://dnf.readthedocs.io/en/latest/conf_ref.html?highlight=user_agent#options-for-both-main-and-repo
|
||||
let ua = format!(
|
||||
"rpm-ostree ({} {}; {}; {}.{})",
|
||||
release.name,
|
||||
release.version_id,
|
||||
variant,
|
||||
"Linux",
|
||||
std::env::consts::ARCH
|
||||
);
|
||||
println!("Using User Agent: {}", ua);
|
||||
|
||||
// Compute the value to send as window counter
|
||||
let counter = cookie.get_window_counter();
|
||||
|
||||
// Send Get requests, track successfully ones and do not exit on failures
|
||||
let successful = repos.iter().fold(0, |acc, r| {
|
||||
let url = format!("{}&countme={}", &r.metalink(&release.version_id), counter);
|
||||
match send_countme(&url, &ua) {
|
||||
Ok(_) => acc + 1,
|
||||
Err(e) => {
|
||||
eprintln!("Request '{}' failed: {}", url, e);
|
||||
acc
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Update cookie timestamp only if at least one request is successful
|
||||
if successful == 0 {
|
||||
bail!("No request successful");
|
||||
}
|
||||
println!("Successful requests: {}/{}", successful, repos.len());
|
||||
if let Err(e) = cookie.persist() {
|
||||
// Do not exit with a non zero code here as we have still made at least
|
||||
// one successful request thus we have been counted.
|
||||
eprintln!("Failed to persist cookie: {}", e);
|
||||
}
|
||||
Ok(())
|
||||
}
|
250
rust/src/countme/cookie.rs
Normal file
250
rust/src/countme/cookie.rs
Normal file
@ -0,0 +1,250 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Red Hat, Inc.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use chrono::prelude::*;
|
||||
use openat_ext::OpenatDirExt;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::io::Read;
|
||||
|
||||
/// State directory used to store the countme cookie
|
||||
const STATE_DIR: &str = "/var/lib/rpm-ostree-countme";
|
||||
/// Cookie file name
|
||||
const COUNTME_COOKIE: &str = "countme";
|
||||
|
||||
/// Width of the sliding time window (in seconds): 1 week
|
||||
const COUNTME_WINDOW: i64 = 7 * 24 * 60 * 60;
|
||||
/// Starting point of the sliding time window relative to the UNIX epoch:
|
||||
/// Monday (1970-01-05 00:00:00 UTC)
|
||||
/// Allows for aligning the window with a specific weekday
|
||||
const COUNTME_OFFSET: i64 = 345600;
|
||||
|
||||
/// Cookie v0 JSON file format
|
||||
///
|
||||
/// The values stored in this cookie are the same as the ones used in the DNF
|
||||
/// Count Me Cookie with the exception of the COUNTME_BUDGET which is not
|
||||
/// included as this implementation directly queries all repo metalinks once:
|
||||
/// - epoch: the timestamp of the first ever counted window
|
||||
/// - window: the timestamp of the last successfully counted window
|
||||
///
|
||||
/// See the libdnf implementation at:
|
||||
/// https://github.com/rpm-software-management/libdnf/blob/95b88b141a3f97feb94eadb2480f6857b6d1fcae/libdnf/repo/Repo.cpp#L1038
|
||||
///
|
||||
/// This cookie is marked as version 0 similarly to how libdnf defines a version
|
||||
/// for their cookie to simplify potential future updates but no additional code
|
||||
/// has been added yet to handle multiple versions as we do not need it yet.
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
||||
struct CookieV0 {
|
||||
epoch: i64,
|
||||
window: i64,
|
||||
}
|
||||
|
||||
impl CookieV0 {
|
||||
/// Try to parse a string as a v0 cookie
|
||||
fn new(string: &str) -> Result<Self> {
|
||||
let c: CookieV0 = serde_json::from_str(string)?;
|
||||
if c.epoch <= 0 {
|
||||
bail!("Invalid value for epoch: '{}'", c.epoch);
|
||||
}
|
||||
if c.window <= 0 {
|
||||
bail!("Invalid value for window: '{}'", c.window);
|
||||
}
|
||||
Ok(c)
|
||||
}
|
||||
|
||||
/// Export a v0 cookie to JSON for persistent storage
|
||||
fn to_json(epoch: i64, window: i64) -> Result<String> {
|
||||
Ok(serde_json::to_string(&CookieV0 { epoch, window })?)
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal representation of the values loaded from the versioned cookie
|
||||
/// format.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Cookie {
|
||||
epoch: i64,
|
||||
window: i64,
|
||||
now: i64,
|
||||
}
|
||||
|
||||
impl Cookie {
|
||||
/// Load cookie timestamps from persistent directory if it exists.
|
||||
/// Returns an error if we can not read an existing cookie
|
||||
/// Returns a default cookie (counting never started) in all other cases.
|
||||
pub fn new() -> Result<Self> {
|
||||
// Start default window at COUNTME_OFFSET to avoid negative values
|
||||
let now = Utc::now().timestamp();
|
||||
let mut c = Cookie {
|
||||
epoch: now,
|
||||
window: COUNTME_OFFSET,
|
||||
now,
|
||||
};
|
||||
|
||||
// Read cookie values from the state persisted on the filesystem
|
||||
let mut content = String::new();
|
||||
match openat::Dir::open(STATE_DIR)?.open_file_optional(COUNTME_COOKIE)? {
|
||||
Some(mut f) => f.read_to_string(&mut content)?,
|
||||
None => return Ok(c),
|
||||
};
|
||||
match CookieV0::new(&content) {
|
||||
Err(e) => eprintln!("Ignoring existing cookie: {}", e),
|
||||
Ok(cookie_v0) => {
|
||||
c.epoch = cookie_v0.epoch;
|
||||
c.window = cookie_v0.window;
|
||||
}
|
||||
};
|
||||
|
||||
// Reset epoch if it is in the future
|
||||
if c.epoch > c.now {
|
||||
c.epoch = c.now;
|
||||
}
|
||||
|
||||
Ok(c)
|
||||
}
|
||||
|
||||
/// Compute the Count Me window for the epoch stored in the cookie
|
||||
fn epoch_window(&self) -> i64 {
|
||||
(self.epoch - COUNTME_OFFSET) / COUNTME_WINDOW
|
||||
}
|
||||
|
||||
/// Compute the Count Me window for the previous window stored in the cookie
|
||||
fn previous_window(&self) -> i64 {
|
||||
(self.window - COUNTME_OFFSET) / COUNTME_WINDOW
|
||||
}
|
||||
|
||||
/// Compute the Count Me window for current time
|
||||
fn current_window(&self) -> i64 {
|
||||
(self.now - COUNTME_OFFSET) / COUNTME_WINDOW
|
||||
}
|
||||
|
||||
/// Are we still in the same window as the one we loaded from the cookie?
|
||||
pub fn existing_window(&self) -> bool {
|
||||
self.current_window() <= self.previous_window()
|
||||
}
|
||||
|
||||
// Count Me window logic
|
||||
// https://dnf.readthedocs.io/en/latest/conf_ref.html?highlight=countme#options-for-both-main-and-repo
|
||||
// https://github.com/rpm-software-management/libdnf/blob/95b88b141a3f97feb94eadb2480f6857b6d1fcae/libdnf/repo/Repo.cpp#L1038
|
||||
pub fn get_window_counter(&self) -> i64 {
|
||||
// Start counting windows from 1
|
||||
let mut counter = self.current_window() - self.epoch_window() + 1;
|
||||
if counter <= 0 {
|
||||
// Should never happen as this is checked at creation time
|
||||
counter = 1;
|
||||
}
|
||||
match counter {
|
||||
1 => 1,
|
||||
2..=4 => 2,
|
||||
5..=24 => 3,
|
||||
_ => 4, // >= 25
|
||||
}
|
||||
}
|
||||
|
||||
/// Update cookie timestamps that are persisted on disk
|
||||
pub fn persist(&self) -> Result<()> {
|
||||
openat::Dir::open(STATE_DIR)?.write_file_contents(
|
||||
COUNTME_COOKIE,
|
||||
0o644,
|
||||
CookieV0::to_json(self.epoch, self.now)?,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_cookie_v0() {
|
||||
let line = r#"{ "epoch": 1607706068, "window": 1607706068 }"#;
|
||||
assert_eq!(
|
||||
CookieV0::new(line).unwrap(),
|
||||
CookieV0 {
|
||||
epoch: 1607706068,
|
||||
window: 1607706068,
|
||||
}
|
||||
);
|
||||
|
||||
let line = r#"{ "epoch": 1600006068, "window": 1607706068 }"#;
|
||||
assert_eq!(
|
||||
CookieV0::new(line).unwrap(),
|
||||
CookieV0 {
|
||||
epoch: 1600006068,
|
||||
window: 1607706068,
|
||||
}
|
||||
);
|
||||
|
||||
let line = r#"{ "epoch": -1, "window": 1607706068 }"#;
|
||||
assert!(CookieV0::new(line).is_err());
|
||||
|
||||
let line = r#"{ "epoch": "1600006068", window: -1 }"#;
|
||||
assert!(CookieV0::new(line).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_window_counter() {
|
||||
// Invalid cookie (epoch is in the future)
|
||||
let cookie = Cookie {
|
||||
epoch: 1607703629, // Friday, December 11, 2020 4:20:29 PM
|
||||
window: COUNTME_OFFSET, // Monday, January 5, 1970 12:00:00 AM
|
||||
now: 976724429, // Wednesday, December 13, 2000 4:20:29 PM
|
||||
};
|
||||
assert_eq!(cookie.get_window_counter(), 1);
|
||||
|
||||
// Fresh cookie (epoch = now)
|
||||
let cookie = Cookie {
|
||||
epoch: 1607703629, // Friday, December 11, 2020 4:20:29 PM
|
||||
window: COUNTME_OFFSET, // Monday, January 5, 1970 12:00:00 AM
|
||||
now: 1607703629, // Friday, December 11, 2020 4:20:29 PM
|
||||
};
|
||||
assert_eq!(cookie.get_window_counter(), 1);
|
||||
|
||||
// First week
|
||||
let cookie = Cookie {
|
||||
epoch: 1607703629, // Friday, December 11, 2020 4:20:29 PM
|
||||
window: COUNTME_OFFSET, // Monday, January 5, 1970 12:00:00 AM
|
||||
now: 1607876429, // Sunday, December 13, 2020 4:20:29 PM
|
||||
};
|
||||
assert_eq!(cookie.get_window_counter(), 1);
|
||||
|
||||
// First month
|
||||
let cookie = Cookie {
|
||||
epoch: 1607703629, // Friday, December 11, 2020 4:20:29 PM
|
||||
window: COUNTME_OFFSET, // Monday, January 5, 1970 12:00:00 AM
|
||||
now: 1608135629, // Wednesday, December 16, 2020 4:20:29 PM
|
||||
};
|
||||
assert_eq!(cookie.get_window_counter(), 2);
|
||||
|
||||
// First six months
|
||||
let cookie = Cookie {
|
||||
epoch: 1607703629, // Friday, December 11, 2020 4:20:29 PM
|
||||
window: COUNTME_OFFSET, // Monday, January 5, 1970 12:00:00 AM
|
||||
now: 1613838029, // Saturday, February 20, 2021 4:20:29 PM
|
||||
};
|
||||
assert_eq!(cookie.get_window_counter(), 3);
|
||||
|
||||
// More than six months
|
||||
let cookie = Cookie {
|
||||
epoch: 1607703629, // Friday, December 11, 2020 4:20:29 PM
|
||||
window: COUNTME_OFFSET, // Monday, January 5, 1970 12:00:00 AM
|
||||
now: 1647793229, // Sunday, March 20, 2022 4:20:29 PM
|
||||
};
|
||||
assert_eq!(cookie.get_window_counter(), 4);
|
||||
}
|
||||
}
|
119
rust/src/countme/repo.rs
Normal file
119
rust/src/countme/repo.rs
Normal file
@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Red Hat, Inc.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use ini::Ini;
|
||||
|
||||
use crate::utils;
|
||||
|
||||
/// Location for DNF repositories configuration
|
||||
pub const YUM_REPOS_D: &str = "/etc/yum.repos.d";
|
||||
|
||||
/// Repository configuration
|
||||
/// Not exhaustive and only includes the options needed for Count Me support.
|
||||
#[derive(Debug)]
|
||||
pub struct Repo {
|
||||
name: String,
|
||||
enabled: bool,
|
||||
count_me: bool,
|
||||
meta_link: String,
|
||||
}
|
||||
|
||||
/// From https://github.com/rpm-software-management/libdnf/blob/45981d5f53980dac362900df65bcb2652aa8d7c7/libdnf/conf/OptionBool.hpp#L30-L31
|
||||
fn is_true(string: &str) -> bool {
|
||||
string == "1" || string == "yes" || string == "true" || string == "on"
|
||||
}
|
||||
|
||||
/// Read all repository configuration files from the default location
|
||||
pub fn all() -> Result<Vec<Repo>> {
|
||||
let configs = fs::read_dir(YUM_REPOS_D)
|
||||
.with_context(|| format!("Could not list files in: {}", YUM_REPOS_D))?;
|
||||
let mut repos = Vec::new();
|
||||
for c in configs {
|
||||
let path = c?.path();
|
||||
match parse_repo_file(&path) {
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"Failed to parse repo config file '{}': {}",
|
||||
path.display(),
|
||||
e
|
||||
)
|
||||
}
|
||||
Ok(mut r) => repos.append(&mut r),
|
||||
}
|
||||
}
|
||||
Ok(repos)
|
||||
}
|
||||
|
||||
/// Read repository configuration from a file
|
||||
fn parse_repo_file(path: &PathBuf) -> Result<Vec<Repo>> {
|
||||
let i = Ini::load_from_file(path).with_context(|| "Could not parse as INI".to_string())?;
|
||||
let mut repos = Vec::new();
|
||||
for (sec, prop) in i.iter() {
|
||||
let mut repo = match sec {
|
||||
None => {
|
||||
continue;
|
||||
}
|
||||
Some(s) => Repo {
|
||||
name: String::from(s),
|
||||
enabled: false,
|
||||
count_me: false,
|
||||
meta_link: "".to_string(),
|
||||
},
|
||||
};
|
||||
for (k, v) in prop.iter() {
|
||||
match k {
|
||||
"countme" => {
|
||||
if is_true(v) {
|
||||
repo.count_me = true
|
||||
}
|
||||
}
|
||||
"enabled" => {
|
||||
if is_true(v) {
|
||||
repo.enabled = true
|
||||
}
|
||||
}
|
||||
"metalink" => repo.meta_link = String::from(v),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
repos.push(repo);
|
||||
}
|
||||
Ok(repos)
|
||||
}
|
||||
|
||||
impl Repo {
|
||||
/// Returns true if this repo is
|
||||
/// - enabled
|
||||
/// - configured for sending a Count Me request
|
||||
/// - has a non-empty metalink URL
|
||||
pub fn count_me(&self) -> bool {
|
||||
self.enabled && self.count_me && !self.meta_link.is_empty()
|
||||
}
|
||||
|
||||
/// Get the metalink URL for the repo with variables replaced
|
||||
pub fn metalink(&self, version_id: &str) -> String {
|
||||
self.meta_link
|
||||
.clone()
|
||||
.replace("$releasever", &version_id)
|
||||
.replace("$basearch", &utils::get_rpm_basearch())
|
||||
}
|
||||
}
|
@ -129,12 +129,19 @@ mod ffi {
|
||||
fn add_group_content(self: &mut PasswdDB, rootfs: i32, group_path: &str) -> Result<()>;
|
||||
fn add_passwd_content(self: &mut PasswdDB, rootfs: i32, passwd_path: &str) -> Result<()>;
|
||||
}
|
||||
|
||||
// countme.rs
|
||||
extern "Rust" {
|
||||
fn countme_entrypoint(argv: Vec<String>) -> Result<()>;
|
||||
}
|
||||
}
|
||||
|
||||
mod client;
|
||||
pub(crate) use client::*;
|
||||
mod cliwrap;
|
||||
pub use cliwrap::*;
|
||||
mod countme;
|
||||
pub use countme::*;
|
||||
mod composepost;
|
||||
pub(crate) use composepost::*;
|
||||
mod core;
|
||||
|
@ -321,3 +321,17 @@ pub(crate) fn sealed_memfd(description: &str, content: &[u8]) -> Result<i32> {
|
||||
mfd.add_seals(&seals)?;
|
||||
Ok(mfd.into_file().into_raw_fd())
|
||||
}
|
||||
|
||||
/// Map Rust architecture constants to the ones used by DNF.
|
||||
/// Rust architecture constants: https://doc.rust-lang.org/std/env/consts/constant.ARCH.html
|
||||
/// DNF mapping: https://github.com/rpm-software-management/dnf/blob/4.5.2/dnf/rpm/__init__.py#L88
|
||||
pub(crate) fn get_rpm_basearch() -> String {
|
||||
match std::env::consts::ARCH {
|
||||
"arm" => "armhfp".to_string(),
|
||||
"powerpc" => "ppc".to_string(),
|
||||
"powerpc64" => "ppc64".to_string(),
|
||||
"sparc64" => "sparc".to_string(),
|
||||
"x86" => "i386".to_string(),
|
||||
s => s.to_string(),
|
||||
}
|
||||
}
|
||||
|
@ -132,6 +132,8 @@ static RpmOstreeCommand commands[] = {
|
||||
NULL, rpmostree_builtin_finalize_deployment },
|
||||
{ "cliwrap", static_cast<RpmOstreeBuiltinFlags>(RPM_OSTREE_BUILTIN_FLAG_LOCAL_CMD | RPM_OSTREE_BUILTIN_FLAG_HIDDEN),
|
||||
NULL, rpmostree_builtin_cliwrap },
|
||||
{ "countme", static_cast<RpmOstreeBuiltinFlags>(RPM_OSTREE_BUILTIN_FLAG_LOCAL_CMD | RPM_OSTREE_BUILTIN_FLAG_HIDDEN),
|
||||
NULL, rpmostree_builtin_countme },
|
||||
{ NULL }
|
||||
};
|
||||
|
||||
|
12
src/app/rpm-ostree-countme.service
Normal file
12
src/app/rpm-ostree-countme.service
Normal file
@ -0,0 +1,12 @@
|
||||
[Unit]
|
||||
Description=Weekly rpm-ostree Count Me reporting
|
||||
Documentation=man:rpm-ostree-countme.service(8)
|
||||
ConditionPathExists=/run/ostree-booted
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User=rpm-ostree
|
||||
DynamicUser=yes
|
||||
StateDirectory=rpm-ostree-countme
|
||||
StateDirectoryMode=750
|
||||
ExecStart=/usr/bin/rpm-ostree countme
|
13
src/app/rpm-ostree-countme.timer
Normal file
13
src/app/rpm-ostree-countme.timer
Normal file
@ -0,0 +1,13 @@
|
||||
[Unit]
|
||||
Description=Weekly rpm-ostree Count Me timer
|
||||
Documentation=man:rpm-ostree-countme.timer(8)
|
||||
ConditionPathExists=/run/ostree-booted
|
||||
|
||||
[Timer]
|
||||
# Trigger weekly with a random delay
|
||||
OnCalendar=weekly UTC
|
||||
AccuracySec=1u
|
||||
RandomizedDelaySec=1w
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
48
src/app/rpmostree-builtin-countme.cxx
Normal file
48
src/app/rpmostree-builtin-countme.cxx
Normal file
@ -0,0 +1,48 @@
|
||||
/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
|
||||
*
|
||||
* Copyright (C) 2020 Red Hat, Inc.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published
|
||||
* by the Free Software Foundation; either version 2 of the licence or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General
|
||||
* Public License along with this library; if not, write to the
|
||||
* Free Software Foundation, Inc., 59 Temple Place, Suite 330,
|
||||
* Boston, MA 02111-1307, USA.
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <string.h>
|
||||
#include <glib-unix.h>
|
||||
#include <gio/gio.h>
|
||||
|
||||
#include "rpmostree-builtins.h"
|
||||
#include "rpmostree-libbuiltin.h"
|
||||
#include "rpmostree-cxxrs.h"
|
||||
|
||||
#include <libglnx.h>
|
||||
|
||||
gboolean
|
||||
rpmostree_builtin_countme (int argc,
|
||||
char **argv,
|
||||
RpmOstreeCommandInvocation *invocation,
|
||||
GCancellable *cancellable,
|
||||
GError **error)
|
||||
{
|
||||
if (argc > 1)
|
||||
return glnx_throw (error, "countme: No argument supported");
|
||||
|
||||
rust::Vec<rust::String> rustargv;
|
||||
for (int i = 1; i < argc; i++)
|
||||
rustargv.push_back(std::string(argv[i]));
|
||||
rpmostreecxx::countme_entrypoint (rustargv);
|
||||
return TRUE;
|
||||
}
|
@ -30,8 +30,9 @@ G_BEGIN_DECLS
|
||||
RpmOstreeCommandInvocation *invocation, \
|
||||
GCancellable *cancellable, GError **error)
|
||||
|
||||
BUILTINPROTO(compose);
|
||||
BUILTINPROTO(cliwrap);
|
||||
BUILTINPROTO(compose);
|
||||
BUILTINPROTO(countme);
|
||||
BUILTINPROTO(upgrade);
|
||||
BUILTINPROTO(reload);
|
||||
BUILTINPROTO(usroverlay);
|
||||
|
Loading…
Reference in New Issue
Block a user