From a41af5ab7871fd37c11295f8639f5261503efe66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ziya=20Ye=C5=9Filbah=C3=A7e?= Date: Fri, 6 Nov 2026 21:42:45 +0300 Subject: [PATCH 1/2] =?UTF-8?q?Saat=20sapmas=C4=B1:=20chrony=20=C3=B6l?= =?UTF-8?q?=C3=A7=C3=BCm=C3=BC,=20tek=20seferlik=20makestep;=20timedated?= =?UTF-8?q?=20yeniden=20ba=C5=9Flatma?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit durum sapma_ms/secili_kaynak, preview 'saat' planı, apply bekleyip tek seferlik chronyc makestep (kalıcı ayar yok), utility_ntp_makestep. apply/geri_yukle sonrası systemd-timedated try-restart. Birim ve LAB testleri (sahte chronyc). Co-Authored-By: Claude Sonnet 5.5 --- .github/workflows/ci.yml | 2 +- CHANGELOG.md | 13 ++++ README.md | 6 +- component.json | 15 +++-- playbooks/apply.yml | 2 + playbooks/durum.yml | 16 ++++- playbooks/geri_yukle.yml | 7 +++ roles/common/tasks/preflight.yml | 1 + roles/common/tasks/warnings.yml | 15 +++++ roles/ntp/defaults/main.yml | 5 ++ roles/ntp/files/utility_clock.py | 104 +++++++++++++++++++++++++++++++ roles/ntp/tasks/drift.yml | 14 +++++ roles/ntp/tasks/plan.yml | 14 +++++ roles/ntp/tasks/step.yml | 60 ++++++++++++++++++ roles/timezone/tasks/apply.yml | 7 +++ test/clock.bats | 87 ++++++++++++++++++++++++++ test/e2e/lab.bats | 65 +++++++++++++++++++ test/e2e/lab/chronyc-stub.sh | 11 ++++ test/run-all.sh | 2 +- 19 files changed, 435 insertions(+), 11 deletions(-) create mode 100755 roles/ntp/files/utility_clock.py create mode 100644 roles/ntp/tasks/drift.yml create mode 100644 roles/ntp/tasks/step.yml create mode 100644 test/clock.bats create mode 100755 test/e2e/lab/chronyc-stub.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d1bfd9e..c7b4500 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,6 +37,6 @@ jobs: run: | set -eu sudo apt-get install -y --no-install-recommends bats >/dev/null - bats test/backup.bats + bats test/backup.bats test/clock.bats bash test/test_release_script.sh bash test/test_release_publish.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index b65bf4a..d71a420 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,19 @@ Biçim [Keep a Changelog](https://keepachangelog.com/) esas alınmıştır. ## [Unreleased] +Sürüm numarası bekliyor (öneri: 0.2.0: yeni değişken, yeni `durum` alanları, saat adımlama davranışı). + +### Eklendi +- **Saat sapması düzeltme.** chrony çalışırken saat 1 sn'den fazla sapmışsa `preview` plana `saat` ekler ve nedenini yazar; `apply` kaynaklar erişilebilir olunca (en çok 60 sn bekler) TEK SEFERLİK `chronyc makestep` yapar ve eşik altına inmesini doğrular. Kalıcı `makestep` ayarı YAZILMAZ. Ölçülemezse (erişilebilir kaynak yok) uyarır; ölçüldü ama düzelmediyse hata verir. Kapatmak için `utility_ntp_makestep=false` (varsayılan `true`). Uyarı metinleri adımlamanın etkisini (log/cron/auditd `time-change`, ssh/TOTP) söyler. +- `durum`: `sapma_ms` ve `secili_kaynak` alanları. `ntp_senkron=yes` artık chrony çalışırken seçili kaynak ve sapma <= 1 sn gerektirir (önceden yalnızca `timedatectl`'e bakılırdı; snapshot'tan geri açılan, 13 gün geride bir VM'de yanıltıcı `yes` görülmüştü). +- Birim testleri `test/clock.bats` (sahte `chronyc`), LAB testleri (sahte `chronyc` ile). + +### Düzeltildi +- `apply` ve `geri_yukle` saat dilimi dosyalarını değiştirince `systemd-timedated` önbelleği eski değeri tutuyordu (`timedatectl show -p Timezone` yanlış; `date` ve `/etc/localtime` doğruydu). Artık `systemctl try-restart systemd-timedated` yapılır. Gerçek Ubuntu 26.04 VM'de ve LAB'da yeniden üretildi; düzeltme yalnız LAB'da sınandı. + +### Denenmedi +- Saat adımlama gerçek VM'de DENENMEDİ: LAB konteynerinde chronyd ve saat adımlama yoktur, testler SAHTE `chronyc` ile yalnızca betik/playbook akışını sınar. Elle `chronyc makestep`'in kaynaklar Reach alınca gerçek VM'de işe yaradığı gözlendi, otomatik yol gözlenmedi. + ## [0.1.0] - 2026-11-05 İlk sürüm. diff --git a/README.md b/README.md index 7d6193e..eab7074 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Düşük riskli, dar kapsamlı bir pakettir: ssh, ufw ve PAM'a dokunmaz, erişim **Proxmox VE bilerek reddedilir:** Proxmox VE 9 Debian 13 tabanlıdır ve OS eşlemesinden geçerdi; bu yüzden ön kontrol `/usr/bin/pveversion` ya da `/etc/pve` varsa (`--check` dahil) "Proxmox VE v0.1 kapsam dışı" diye durur. Bu koruma sahte `pveversion` ile LAB'da test edildi, **gerçek Proxmox'ta denenmedi**. -Kapsam dışı (v0.1): Proxmox VE, Raspberry Pi, hostname, apt kaynakları, NTS, `makestep`, `LC_*`. +Kapsam dışı: Proxmox VE, Raspberry Pi, hostname, apt kaynakları, NTS, kalıcı `makestep` ayarı, `LC_*`. (Tek seferlik saat adımlama kapsamdadır, aşağıya bakın.) ## Eylemler | Eylem | Etki | Sudo | Ne yapar | @@ -29,6 +29,7 @@ Kapsam dışı (v0.1): Proxmox VE, Raspberry Pi, hostname, apt kaynakları, NTS, | `utility_timezone` | string | boş | Örn. `Europe/Istanbul`. Boş = dokunma | | `utility_locale` | string | boş | Yalnızca `LANG`, örn. `en_US.UTF-8`. Boş = dokunma. Gerekirse `locales` kurulur ve locale üretilir | | `utility_ntp_servers` | list[string] | `[]` | En çok 8: `host`, `server host` veya `pool host` (`iburst` eklenir). Boş = dağıtım varsayılanı; dosya yazılmaz, chrony kurulmaz | +| `utility_ntp_makestep` | bool | `true` | Saat 1 sn'den fazla sapmışsa `apply` chrony çalışırken TEK SEFERLİK `chronyc makestep` yapar (kaynaklar erişilebilir olana kadar en çok 60 sn bekler). `false` = sapmaya dokunma. Kalıcı ayar yazılmaz | | `utility_backup` | bool | `true` | `false` = yedek alınmaz (ve `geri_yukle` o değişikliği geri alamaz) | | `utility_restore_from` | string | boş | `geri_yukle` için yedek adı (`20261105T101500Z`); boş = en son yedek | @@ -71,7 +72,8 @@ LAB'ın sınırları: chrony.service konteynerde başlamaz (`ConditionVirtualiza ## Sınırlar ve v0.2 için bilinenler - NTP kaynakları dağıtımın varsayılanlarının **yanına eklenir**; mevcut kaynakları (Ubuntu `ubuntu-ntp-pools.sources`, Debian `chrony.conf` pool satırları) silmez ve devre dışı bırakmaz. Biçim denetimi yalnızca `host`, `server host`, `pool host` kabul eder. - v0.2 adayları (kod yok): (a) yerel NTP için dağıtım varsayılan kaynaklarını devre dışı bırakma seçeneği, (b) `prefer`, `minpoll` gibi kaynak seçenekleri. -- `durum`: chrony kurulu değilken `ntp_senkron=yes` görünebilir, çünkü `timedatectl` `systemd-timesyncd`'nin senkronunu da sayar (`ntp_servis=inactive` ile birlikte okuyun). +- `durum`: chrony kurulu değilken `ntp_senkron=yes` görünebilir, çünkü `timedatectl` `systemd-timesyncd`'nin senkronunu da sayar (`ntp_servis=inactive` ile birlikte okuyun). chrony çalışırken `yes` için seçili kaynak ve sapma <= 1 sn de gerekir; `sapma_ms` ve `secili_kaynak` alanları bunu gösterir. +- **Saat adımlama (sapmış saat).** Ölçüm: seçili kaynak varsa `chronyc tracking` farkı, yoksa erişilebilir (reach>0) kaynakların en küçük mutlak farkı. Etkisi bir zaman sıçramasıdır (log damgaları, cron, auditd `time-change`, ssh/TOTP/sertifika). chrony bu koşuda kurulacaksa `preview` sapmayı ölçemez; `apply` chrony başlayınca ölçer. Gerçek VM'de DENENMEDİ (yalnız sahte `chronyc` ile LAB ve birim testi); elle `chronyc makestep` gerçek VM'de işe yaramıştı. ## Bilinen sorunlar - Ubuntu 26.04'te doğrudan `ansible-playbook --ask-become-pass` ile `Timeout (12s) waiting for privilege escalation prompt` alınır (sudo-rs): `-e ansible_become_exe=/usr/bin/sudo.ws` ekleyin. sshpocket menüsünün bunu kendi çözüp çözmediği doğrulanmadı. Ayrıntı: [docs/vm-turu.md](docs/vm-turu.md). diff --git a/component.json b/component.json index 077992a..baa183a 100644 --- a/component.json +++ b/component.json @@ -17,8 +17,8 @@ "effects": [], "requires_sudo": false, "confirm": "none", - "warnings": ["Salt okunur: saat dilimi, LANG, chrony durumu ve ofsetini okur; hiçbir şeyi değiştirmez. chrony kurulu değilse ntp_servis=inactive/unknown, ofset unknown görünür."], - "outputs": { "format": "keyvalue-line", "prefix": "SSHPOCKET_RESULT utility.durum", "keys": ["timezone", "locale", "ntp_servis", "ntp_senkron", "offset_ms", "kaynak_sayisi"] } + "warnings": ["Salt okunur: saat dilimi, LANG, chrony durumu, ofseti ve gerçek sapmayı (seçili kaynak yoksa erişilebilir kaynakların farkı) okur; hiçbir şeyi değiştirmez. ntp_senkron=yes için chrony çalışırken seçili kaynak ve sapma <= 1 sn gerekir. chrony kurulu değilse ntp_servis=inactive/unknown, ofset ve sapma unknown görünür."], + "outputs": { "format": "keyvalue-line", "prefix": "SSHPOCKET_RESULT utility.durum", "keys": ["timezone", "locale", "ntp_servis", "ntp_senkron", "offset_ms", "kaynak_sayisi", "sapma_ms", "secili_kaynak"] } }, "preview": { "cmd": "ansible-playbook playbooks/preview.yml --check --diff", @@ -27,7 +27,8 @@ "confirm": "none", "warnings": [ "Önizleme yalnızca dosya farkını ve plan adlarını gösterir: timedatectl, locale-gen, paket kurulumu ve chrony yeniden başlatması --check'te çalışmaz, gösterilmez.", - "Boş bırakılan utility_timezone / utility_locale / utility_ntp_servers o alana hiç dokunulmayacağı anlamına gelir." + "Boş bırakılan utility_timezone / utility_locale / utility_ntp_servers o alana hiç dokunulmayacağı anlamına gelir.", + "chrony çalışıyorsa ve saat 1 sn'den fazla sapmışsa plana 'saat' girer: apply tek seferlik adımlar. chrony bu koşuda kurulacaksa sapma önizlemede ölçülemez." ], "outputs": { "format": "keyvalue-line", "prefix": "SSHPOCKET_RESULT utility.preview", "keys": ["degisen", "plan"] } }, @@ -42,7 +43,9 @@ "NTP kaynakları, dağıtımın varsayılan kaynaklarının YERİNE değil yanına eklenir (sources.d/utility.sources); chrony.conf'a dokunulmaz. Değişince chrony yeniden başlatılır.", "Yanlış NTP kaynağının en kötü sonucu saat senkronunun kaybıdır (ssh sertifikaları, TOTP, apt 'Not live until' etkilenebilir); erişim kaybı değil.", "auditd açıksa saat dilimi değişimi 'time-change' kaydı üretir (beklenen). ufw giden trafiği reddediyorsa (hardening_ufw_outgoing_deny) NTP için 123/udp çıkışı gerekir.", - "locale-gen ve chrony kurulumu gerekirse apt ile paket kurar (locales, chrony); geri_yukle paketleri kaldırmaz." + "locale-gen ve chrony kurulumu gerekirse apt ile paket kurar (locales, chrony); geri_yukle paketleri kaldırmaz.", + "Saat 1 sn'den fazla sapmışsa (chrony çalışıyorken ölçülür) apply kaynaklar erişilebilir olunca TEK SEFERLİK `chronyc makestep` yapar (en çok 60 sn bekler; kalıcı makestep ayarı yazılmaz). Adımlama zaman sıçramasıdır: log damgaları, cron işleri, auditd 'time-change' kaydı, ssh/TOTP/sertifika geçerliliği etkilenebilir. Kapatmak için utility_ntp_makestep=false.", + "Saat dilimi değişince systemd-timedated yeniden başlatılır (timedatectl önbelleği eski değeri tutmasın)." ], "outputs": { "format": "keyvalue-line", "prefix": "SSHPOCKET_RESULT utility.apply", "keys": ["degisen", "plan", "yedek"] } }, @@ -54,7 +57,8 @@ "warnings": [ "Yalnızca DOSYALARI geri yazar (/etc/localtime, /etc/timezone, /etc/default/locale, /etc/locale.gen, chrony conf.d ve sources.d); yedek yokken var olmayan dosyayı siler. utility_restore_from boşsa EN SON yedek kullanılır.", "Kurulan paketleri (chrony, locales), üretilmiş locale'leri, servis durumunu ve geçmiş log zaman damgalarını geri ALMAZ. chrony drop-in'i değiştiyse chrony yeniden başlatılır.", - "Yedekten sonra elle yapılan değişikliklerin üzerine yazar." + "Yedekten sonra elle yapılan değişikliklerin üzerine yazar.", + "Saat dilimi dosyaları geri yazılınca systemd-timedated yeniden başlatılır. Adımlanmış saati geri ALMAZ." ], "outputs": { "format": "keyvalue-line", "prefix": "SSHPOCKET_RESULT utility.geri_yukle", "keys": ["yedek", "geri_yuklenen", "degisen"] } } @@ -63,6 +67,7 @@ { "name": "utility_timezone", "type": "string", "default": "", "group": "Zaman", "description": "Saat dilimi (örn. Europe/Istanbul). Boş = saat dilimine dokunma. /usr/share/zoneinfo altında var olmalı." }, { "name": "utility_locale", "type": "string", "default": "", "group": "Dil", "description": "Sistem dili: yalnızca LANG (örn. en_US.UTF-8). Boş = dokunma. LC_* yönetilmez. Gerekirse locales kurulur ve locale üretilir." }, { "name": "utility_ntp_servers", "type": "list[string]", "default": [], "group": "NTP", "description": "NTP kaynakları (en çok 8): 'host', 'server host' ya da 'pool host'; iburst otomatik eklenir. Boş = dağıtım varsayılanı (dosya yazılmaz, chrony kurulmaz). Dağıtımın varsayılan kaynakları kalır." }, + { "name": "utility_ntp_makestep", "type": "bool", "default": true, "group": "NTP", "description": "Saat 1 sn'den fazla sapmışsa apply chrony çalışırken TEK SEFERLİK 'chronyc makestep' yapar (kalıcı ayar yazılmaz). false = sapmaya dokunma." }, { "name": "utility_backup", "type": "bool", "default": true, "group": "Yedek", "description": "Değiştirmeden önce /var/backups/utility// altına yedek al. false = yedek alınmaz ve geri_yukle yeni değişikliği geri alamaz." }, { "name": "utility_restore_from", "type": "string", "default": "", "group": "Yedek", "description": "geri_yukle için yedek adı (örn. 20261105T101500Z). Boş = en son yedek." } ] diff --git a/playbooks/apply.yml b/playbooks/apply.yml index 6bf3ba1..3eabe57 100644 --- a/playbooks/apply.yml +++ b/playbooks/apply.yml @@ -25,6 +25,8 @@ ansible.builtin.include_role: {name: locale, tasks_from: apply} - name: Apply NTP ansible.builtin.include_role: {name: ntp, tasks_from: apply} + - name: Step the clock (chrony) + ansible.builtin.include_role: {name: ntp, tasks_from: step} - name: Show the plan ansible.builtin.include_role: {name: common, tasks_from: result} - name: Result line diff --git a/playbooks/durum.yml b/playbooks/durum.yml index 8ea5d8d..c0ebd43 100644 --- a/playbooks/durum.yml +++ b/playbooks/durum.yml @@ -1,5 +1,5 @@ # Salt okunur durum: saat dilimi, LANG, NTP servisi/senkron/ofset/kaynak sayısı. Hiçbir şey değiştirmez, sudo gerektirmez. -# Sonuç satırı: SSHPOCKET_RESULT utility.durum timezone=… locale=… ntp_servis=… ntp_senkron=yes|no offset_ms=… kaynak_sayisi=… +# Sonuç satırı: SSHPOCKET_RESULT utility.durum timezone=… locale=… ntp_servis=… ntp_senkron=yes|no offset_ms=… kaynak_sayisi=… sapma_ms=… secili_kaynak=yes|no - name: utility durum (salt okunur) hosts: utility gather_facts: false @@ -52,9 +52,19 @@ failed_when: false check_mode: false + # sapma: seçili kaynak varsa tracking, yoksa erişilebilir kaynakların en küçük farkı (utility_clock.py; ntp rolünün drift görevi) + - name: Read clock drift + ansible.builtin.include_role: {name: ntp, tasks_from: drift} + # chronyc -c tracking alanları: 1 ref-id, 2 ad, 3 katman, 4 ref zamanı, 5 sistem saati farkı (saniye, işaretli) ... - name: Result line vars: + utility_d_clock: "{{ utility_clock }}" + # chrony çalışıyorsa 'senkron' = timedatectl yes + seçili kaynak + sapma <= 1 sn; çalışmıyorsa (timesyncd) timedatectl'e bakılır + utility_d_synced: >- + {{ (utility_d_sync.stdout | default('') | trim == 'yes') and + (not (utility_d_clock.up | default(false)) or + (utility_d_clock.selected | default(false) and utility_d_clock.drift_s | default(none) is not none and utility_d_clock.drift_s | float <= 1.0)) }} utility_d_offset: >- {{ ((utility_d_tracking.stdout.split(',')[4] | float * 1000) | round(1)) if utility_d_tracking.rc == 0 and utility_d_tracking.stdout.split(',') | length > 4 else 'unknown' }} ansible.builtin.debug: @@ -63,6 +73,8 @@ timezone={{ (utility_d_tz.stdout | default('') | trim or 'unknown') | regex_replace('[^A-Za-z0-9_.:+-]', '_') | truncate(60, True, '') }} locale={{ (utility_d_lang.stdout | default('') | trim or 'unset') | regex_replace('[^A-Za-z0-9_.:+-]', '_') | truncate(60, True, '') }} ntp_servis={{ (utility_d_svc.stdout | default('') | trim or 'unknown') | regex_replace('[^A-Za-z0-9_.:+-]', '_') | truncate(60, True, '') }} - ntp_senkron={{ 'yes' if utility_d_sync.stdout | default('') | trim == 'yes' else 'no' }} + ntp_senkron={{ 'yes' if utility_d_synced | bool else 'no' }} offset_ms={{ utility_d_offset | string | regex_replace('[^A-Za-z0-9_.:+-]', '_') | truncate(60, True, '') }} kaynak_sayisi={{ (utility_d_sources.stdout_lines | default([]) | length) if utility_d_sources.rc == 0 else 0 }} + sapma_ms={{ ((utility_d_clock.drift_s | float * 1000) | round(1)) if utility_d_clock.drift_s | default(none) is not none else 'unknown' }} + secili_kaynak={{ 'yes' if utility_d_clock.selected | default(false) else 'no' }} diff --git a/playbooks/geri_yukle.yml b/playbooks/geri_yukle.yml index fac77e3..7da9eec 100644 --- a/playbooks/geri_yukle.yml +++ b/playbooks/geri_yukle.yml @@ -11,6 +11,13 @@ - name: Restore from backup ansible.builtin.include_role: {name: common, tasks_from: restore} + # systemd-timedated saat dilimini önbelleğe alır: geri yazılan /etc/localtime'ı görmesi için yeniden başlatılır + - name: Refresh systemd-timedated + ansible.builtin.command: systemctl try-restart systemd-timedated + changed_when: false + failed_when: false + when: not ansible_check_mode and ('/etc/localtime' in utility_restore.changed_paths or '/etc/timezone' in utility_restore.changed_paths) + - name: Result line ansible.builtin.debug: msg: >- diff --git a/roles/common/tasks/preflight.yml b/roles/common/tasks/preflight.yml index 3110aa0..e1bdd3d 100644 --- a/roles/common/tasks/preflight.yml +++ b/roles/common/tasks/preflight.yml @@ -38,6 +38,7 @@ - (utility_ntp_servers | default([])) is sequence and (utility_ntp_servers | default([])) is not string - (utility_ntp_servers | default([])) | length <= 8 - utility_backup | bool in [true, false] + - utility_ntp_makestep | default(true) | bool in [true, false] - (utility_timezone | default('')) == '' or (utility_timezone | default('')) is match('^[A-Za-z0-9_+-]+(/[A-Za-z0-9_+-]+){0,2}$') - (utility_locale | default('')) == '' or (utility_locale | default('')) is match('^(C|POSIX|C\.UTF-8|[a-z]{2,3}_[A-Z]{2}\.(UTF-8|ISO-8859-[0-9]{1,2}))$') - >- diff --git a/roles/common/tasks/warnings.yml b/roles/common/tasks/warnings.yml index 3cdb992..d9cac04 100644 --- a/roles/common/tasks/warnings.yml +++ b/roles/common/tasks/warnings.yml @@ -27,3 +27,18 @@ UYARI (utility): ufw giden trafiği varsayılan olarak reddediyor (hardening_ufw_outgoing_deny). NTP kaynakları yalnızca 123/udp çıkışı izinliyse çalışır; yoksa kaynaklara ulaşılamaz ve saat senkronu kaybolur. NTS bu sürümde yok. when: (utility_ntp_servers | default([])) | length > 0 and 'deny (outgoing)' in utility_ufw.stdout | default('') + +- name: Warn about the clock step + ansible.builtin.debug: + msg: >- + UYARI (utility): saat {{ utility_clock.drift_s }} sn sapmalı (ölçüm: {{ utility_clock.source }}); apply chrony ile TEK SEFERLİK adımlayacak (`chronyc makestep`). + Adımlama zaman sıçraması demektir: log zaman damgaları, cron işleri, auditd 'time-change' kaydı, ssh/TOTP/sertifika geçerliliği etkilenebilir. + Sapma kalıcı bir sorunsa (kaynak yok, snapshot'tan açılış) önce kaynakları kontrol edin. Kalıcı 'makestep' ayarı YAZILMAZ. + when: "'saat' in utility_plan and utility_clock is defined" + +- name: Note chrony installation and clock drift + ansible.builtin.debug: + msg: >- + BİLGİ (utility): chrony bu koşuda kurulacak/değişecek; saat sapması önizlemede ölçülemez. apply, chrony başlayınca sapmayı ölçer + ve gerekirse (>{{ utility_ntp_step_threshold_s | default(1.0) }} sn) tek seferlik adımlar. + when: "'paket' in utility_plan and (utility_ntp_makestep | default(true) | bool)" diff --git a/roles/ntp/defaults/main.yml b/roles/ntp/defaults/main.yml index a4d4fdc..8e30466 100644 --- a/roles/ntp/defaults/main.yml +++ b/roles/ntp/defaults/main.yml @@ -1,2 +1,7 @@ # boş = dağıtım varsayılanı; dosya yazılmaz, chrony kurulmaz utility_ntp_servers: [] +# saat sapması: eşikten büyükse apply TEK SEFERLİK `chronyc makestep` yapar (kalıcı makestep ayarı yazılmaz). false = sapmaya dokunma +utility_ntp_makestep: true +# iç sabitler (manifest değişkeni değil) +utility_ntp_step_threshold_s: 1.0 +utility_ntp_step_timeout_s: 60 diff --git a/roles/ntp/files/utility_clock.py b/roles/ntp/files/utility_clock.py new file mode 100755 index 0000000..9cc48cb --- /dev/null +++ b/roles/ntp/files/utility_clock.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +"""utility: chrony saat sapmasını ölçer ve gerekirse TEK SEFERLİK adımlar (hedef sunucuda çalışır; yalnızca standart kütüphane). + +Girdi: tek bir JSON bağımsız değişkeni. Çıktı: stdout'a tek satır JSON. + + {"mode": "status", "threshold": 1.0} + -> {"up": bool, "selected": bool, "drift_s": float|null, "source": "tracking"|"kaynak"|"yok", "sources": n, "reachable": n} + {"mode": "step", "threshold": 1.0, "timeout": 60, "interval": 5} + -> {"ok": bool, "stepped": bool, "initial_drift_s": float|null, "drift_s": float|null, "reason": "", "waited_s": n} + +Sapma: seçili bir kaynak varsa `chronyc tracking` sistem saati farkı; yoksa erişilebilir (reach>0) kaynakların son örneklerinin +EN KÜÇÜK mutlak farkı (tek bir kötü kaynak adımlamaya yol açmasın). Ölçülemiyorsa null. +`step`: sapma eşikten büyükse `chronyc makestep` (kalıcı makestep ayarı YAZILMAZ); kaynaklar erişilebilir olana kadar bekler, +eşik altına inene ya da zaman aşımına kadar yineler. reason: chrony_yok | kaynak_yok (hiç ölçülemedi) | adimlanamadi (ölçüldü ama düzelmedi). +""" +import json +import subprocess +import sys +import time + + +def chronyc(*args): + try: + p = subprocess.run(["chronyc"] + list(args), stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, + universal_newlines=True, timeout=15) + except (OSError, subprocess.TimeoutExpired): + return None + return p.stdout if p.returncode == 0 else None + + +def status(): + out = chronyc("-c", "tracking") + if not out or not out.strip(): + return {"up": False, "selected": False, "drift_s": None, "source": "yok", "sources": 0, "reachable": 0} + f = out.strip().splitlines()[0].split(",") + selected = False + track = None + try: + selected = f[0].strip("0") != "" and int(f[2]) > 0 + track = abs(float(f[4])) + except (IndexError, ValueError): + pass + lines = (chronyc("-c", "sources") or "").strip().splitlines() + offs = [] + for ln in lines: + g = ln.split(",") + try: + if int(g[5]) > 0: + offs.append(abs(float(g[7]))) + except (IndexError, ValueError): + continue + if selected and track is not None: + drift, src = track, "tracking" + elif offs: + drift, src = min(offs), "kaynak" + else: + drift, src = None, "yok" + return {"up": True, "selected": selected, "drift_s": None if drift is None else round(drift, 3), "source": src, + "sources": len(lines), "reachable": len(offs)} + + +def step(req): + thr = float(req.get("threshold", 1.0)) + timeout = float(req.get("timeout", 60)) + interval = float(req.get("interval", 5)) + t0 = time.time() + first = status() + if not first["up"]: + return {"ok": False, "stepped": False, "initial_drift_s": None, "drift_s": None, "reason": "chrony_yok", "waited_s": 0} + stepped, last_step, cur = False, None, first + while True: + d = cur["drift_s"] + if d is not None and d <= thr: + return {"ok": True, "stepped": stepped, "initial_drift_s": first["drift_s"], "drift_s": d, "reason": "", + "waited_s": int(time.time() - t0)} + if time.time() - t0 >= timeout: + return {"ok": False, "stepped": stepped, "initial_drift_s": first["drift_s"], "drift_s": d, + "reason": "kaynak_yok" if d is None and not stepped else "adimlanamadi", "waited_s": int(time.time() - t0)} + if d is not None and (last_step is None or time.time() - last_step >= interval): + chronyc("makestep") + stepped, last_step = True, time.time() + time.sleep(min(interval, 2) if d is not None else interval) + cur = status() + if not cur["up"]: + return {"ok": False, "stepped": stepped, "initial_drift_s": first["drift_s"], "drift_s": None, "reason": "chrony_yok", + "waited_s": int(time.time() - t0)} + + +def main(argv): + if len(argv) != 1: + print(json.dumps({"error": "tek JSON argümanı gerekir"})) + return 2 + try: + req = json.loads(argv[0]) + out = {"status": lambda r: status(), "step": step}[req["mode"]](req) + except Exception as exc: # noqa: BLE001 - tek satır JSON hata + print(json.dumps({"error": "%s: %s" % (type(exc).__name__, exc)})) + return 1 + print(json.dumps(out)) + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/roles/ntp/tasks/drift.yml b/roles/ntp/tasks/drift.yml new file mode 100644 index 0000000..da74c82 --- /dev/null +++ b/roles/ntp/tasks/drift.yml @@ -0,0 +1,14 @@ +# chrony saat sapmasını okur (salt okunur; --check'te de çalışır): utility_clock = {up, selected, drift_s, source, sources, reachable} +- name: Read chrony clock drift + ansible.builtin.script: + cmd: >- + utility_clock.py {{ {'mode': 'status', 'threshold': utility_ntp_step_threshold_s} | to_json | quote }} + executable: python3 + register: utility_clock_raw + changed_when: false + failed_when: false + check_mode: false + +- name: Parse the drift + ansible.builtin.set_fact: + utility_clock: "{{ (utility_clock_raw.stdout if utility_clock_raw.rc == 0 and (utility_clock_raw.stdout | default('')) | length > 0 else '{}') | from_json }}" diff --git a/roles/ntp/tasks/plan.yml b/roles/ntp/tasks/plan.yml index c328d46..680611c 100644 --- a/roles/ntp/tasks/plan.yml +++ b/roles/ntp/tasks/plan.yml @@ -53,3 +53,17 @@ utility_plan: "{{ utility_plan + (['paket'] if utility_ntp_need_pkg | bool else []) + (['ntp'] if (utility_ntp_need_file | bool or utility_ntp_need_service | bool) else []) }}" utility_backup_paths: "{{ utility_backup_paths + (['/etc/chrony/sources.d/utility.sources'] if utility_ntp_need_file | bool else []) }}" when: utility_ntp_need_pkg | bool or utility_ntp_need_file | bool or utility_ntp_need_service | bool + +# SAAT SAPMASI (utility_ntp_servers'tan bağımsız): chrony çalışıyorsa ve sapma eşikten büyükse plana 'saat' girer; apply tek seferlik adımlar. +- name: Plan clock step + when: utility_ntp_makestep | bool + block: + - name: Read the drift + ansible.builtin.include_tasks: drift.yml + + - name: Record the clock step plan + ansible.builtin.set_fact: + utility_plan: "{{ utility_plan + ['saat'] }}" + utility_ntp_need_step: true + when: >- + utility_clock.drift_s | default(none) is not none and utility_clock.drift_s | float > utility_ntp_step_threshold_s | float diff --git a/roles/ntp/tasks/step.yml b/roles/ntp/tasks/step.yml new file mode 100644 index 0000000..e66a23c --- /dev/null +++ b/roles/ntp/tasks/step.yml @@ -0,0 +1,60 @@ +# SAAT ADIMLAMA (yalnızca gerçek koşu): chrony yeni kaynaklarla çalışırken sapma eşikten büyükse TEK SEFERLİK `chronyc makestep`. +# Kaynaklar erişilebilir olana kadar bekler (utility_ntp_step_timeout_s); kalıcı makestep ayarı YAZILMAZ. +# chrony bu koşuda kurulduysa/değiştiyse ('paket' ya da 'ntp' planda) ölçülemese bile beklenir: erişilemeyen kaynak burada görünür. +- name: Restart chrony now if its configuration changed (before measuring) + ansible.builtin.meta: flush_handlers + +- name: Step the clock + when: not ansible_check_mode and utility_ntp_makestep | bool + block: + - name: Measure the drift again + ansible.builtin.script: + cmd: >- + utility_clock.py {{ {'mode': 'status', 'threshold': utility_ntp_step_threshold_s} | to_json | quote }} + executable: python3 + register: utility_clock_now_raw + changed_when: false + failed_when: false + + - name: Decide whether to step + ansible.builtin.set_fact: + utility_clock_now: "{{ (utility_clock_now_raw.stdout if utility_clock_now_raw.rc == 0 and (utility_clock_now_raw.stdout | default('')) | length > 0 else '{}') | from_json }}" + utility_clock_touched: "{{ 'paket' in utility_plan or 'ntp' in utility_plan }}" + + - name: Wait for a usable source and step the clock once + ansible.builtin.script: + cmd: >- + utility_clock.py {{ {'mode': 'step', 'threshold': utility_ntp_step_threshold_s, 'timeout': utility_ntp_step_timeout_s, 'interval': 5} | to_json | quote }} + executable: python3 + register: utility_step_raw + changed_when: (utility_step_raw.stdout | default('{}') | from_json).stepped | default(false) + failed_when: >- + utility_step_raw.rc != 0 or (utility_step_raw.stdout | default('{}') | from_json).error is defined or + ((utility_step_raw.stdout | default('{}') | from_json).ok | default(false) | bool == false and + (utility_step_raw.stdout | default('{}') | from_json).reason | default('') == 'adimlanamadi') + when: >- + utility_clock_now.up | default(false) and + ((utility_clock_now.drift_s | default(none) is not none and utility_clock_now.drift_s | float > utility_ntp_step_threshold_s | float) + or utility_clock_touched | bool) + + - name: Note the clock step in the plan + ansible.builtin.set_fact: + utility_plan: "{{ utility_plan + ['saat'] }}" + when: >- + utility_step_raw is not skipped and (utility_step_raw.stdout | from_json).stepped | default(false) and 'saat' not in utility_plan + + - name: Report the clock step + ansible.builtin.debug: + msg: >- + utility: saat adımlandı (sapma {{ (utility_step_raw.stdout | from_json).initial_drift_s }} sn -> {{ (utility_step_raw.stdout | from_json).drift_s }} sn, + {{ (utility_step_raw.stdout | from_json).waited_s }} sn beklendi). Log, cron ve audit kayıtlarında zaman sıçraması beklenen. + when: utility_step_raw is not skipped and (utility_step_raw.stdout | from_json).stepped | default(false) + + - name: Warn when no source became usable + ansible.builtin.debug: + msg: >- + UYARI (utility): chrony'nin erişilebilir kaynağı yok ({{ utility_step_wait }} sn beklendi); saat sapması ölçülemedi ve düzeltilemedi. + Kaynaklara ağ erişimi (123/udp, ufw giden kuralı, DNS) ve `chronyc sources` çıktısını kontrol edin. + vars: + utility_step_wait: "{{ utility_step_timeout_s | default(utility_ntp_step_timeout_s) }}" + when: utility_step_raw is not skipped and (utility_step_raw.stdout | from_json).reason | default('') == 'kaynak_yok' diff --git a/roles/timezone/tasks/apply.yml b/roles/timezone/tasks/apply.yml index 5e4464d..d775835 100644 --- a/roles/timezone/tasks/apply.yml +++ b/roles/timezone/tasks/apply.yml @@ -19,3 +19,10 @@ group: root mode: "0644" when: utility_tz_file_differs | bool + + # systemd-timedated saat dilimini önbelleğe alır: dosyaları doğrudan yazınca `timedatectl` eski değeri göstermeye devam eder + - name: Refresh systemd-timedated + ansible.builtin.command: systemctl try-restart systemd-timedated + changed_when: false + failed_when: false + when: not ansible_check_mode and (utility_tz_link_differs | bool or utility_tz_file_differs | bool) diff --git a/test/clock.bats b/test/clock.bats new file mode 100644 index 0000000..f26b169 --- /dev/null +++ b/test/clock.bats @@ -0,0 +1,87 @@ +#!/usr/bin/env bats +# roles/ntp/files/utility_clock.py birim testleri: sahte `chronyc` (PATH başına eklenir); sunucu/chrony gerekmez. +PY="${PYTHON:-python3}" +SCRIPT="$BATS_TEST_DIRNAME/../roles/ntp/files/utility_clock.py" + +setup() { + S="$BATS_TEST_TMPDIR/stub"; mkdir -p "$S/bin" + # durum dosyaları: offset (saniye), reach, selected (1/0), makestep_needs_reach_after (sayaç: kaç `sources` çağrısından sonra reach>0) + echo -1152060 > "$S/offset"; echo 377 > "$S/reach"; echo 0 > "$S/selected" + cat > "$S/bin/chronyc" <<'STUB' +#!/usr/bin/env bash +S="$(dirname "$0")/.." +[ -f "$S/down" ] && exit 1 +off=$(cat "$S/offset"); reach=$(cat "$S/reach"); sel=$(cat "$S/selected") +case "$*" in + "-c tracking") if [ "$sel" = 1 ]; then echo "A9FE0101,x,3,0,$off,0,0,0,0,0,0,0,0"; else echo "00000000,,0,0,0,0,0,0,0,0,0,0,0"; fi ;; + "-c sources") if [ "$sel" = 1 ]; then st='*'; else st='?'; fi; echo "^,$st,ntp.example,2,6,$reach,10,$off,0,0" ;; + "makestep") if [ "$reach" != 0 ]; then echo 0.0002 > "$S/offset"; echo 1 > "$S/selected"; echo x >> "$S/steps"; fi ;; +esac +STUB + chmod +x "$S/bin/chronyc" + export PATH="$S/bin:$PATH" +} +req() { "$PY" -I "$SCRIPT" "$1"; } +jget() { "$PY" -I -c 'import json,sys; print(json.loads(sys.argv[1])[sys.argv[2]])' "$1" "$2"; } + +@test "status: seçili kaynak yokken erişilebilir kaynağın farkı sapma sayılır" { + out=$(req '{"mode":"status"}') + [ "$(jget "$out" up)" = True ]; [ "$(jget "$out" selected)" = False ] + [ "$(jget "$out" source)" = kaynak ]; [ "$(jget "$out" drift_s)" = 1152060.0 ] +} + +@test "status: seçili kaynak varsa tracking farkı" { + echo 1 > "$S/selected"; echo 0.25 > "$S/offset" + out=$(req '{"mode":"status"}') + [ "$(jget "$out" source)" = tracking ]; [ "$(jget "$out" drift_s)" = 0.25 ] +} + +@test "status: kaynaklar erişilemezse (reach 0) sapma null" { + echo 0 > "$S/reach" + out=$(req '{"mode":"status"}') + [ "$(jget "$out" drift_s)" = None ]; [ "$(jget "$out" source)" = yok ] +} + +@test "status: chronyc yoksa up=False" { + touch "$S/down" + out=$(req '{"mode":"status"}') + [ "$(jget "$out" up)" = False ] +} + +@test "step: büyük sapmayı chronyc makestep ile TEK kez adımlar" { + out=$(req '{"mode":"step","threshold":1,"timeout":10,"interval":1}') + [ "$(jget "$out" ok)" = True ]; [ "$(jget "$out" stepped)" = True ] + [ "$(jget "$out" initial_drift_s)" = 1152060.0 ] + [ "$(wc -l < "$S/steps" | tr -d ' ')" = 1 ] +} + +@test "step: sapma küçükse hiçbir şey yapmaz (idempotans)" { + echo 1 > "$S/selected"; echo 0.2 > "$S/offset" + out=$(req '{"mode":"step","threshold":1,"timeout":10,"interval":1}') + [ "$(jget "$out" ok)" = True ]; [ "$(jget "$out" stepped)" = False ] + [ ! -e "$S/steps" ] +} + +@test "step: kaynaklar hiç erişilebilir olmazsa kaynak_yok (adımlama denenmez)" { + echo 0 > "$S/reach" + out=$(req '{"mode":"step","threshold":1,"timeout":2,"interval":1}') + [ "$(jget "$out" ok)" = False ]; [ "$(jget "$out" reason)" = kaynak_yok ] + [ ! -e "$S/steps" ] +} + +@test "step: ölçüldü ama makestep düzeltmezse adimlanamadi" { + printf '#!/usr/bin/env bash\nS="$(dirname "$0")/.."\ncase "$*" in "-c tracking") echo "00000000,,0,0,0,0";; "-c sources") echo "^,?,n,2,6,377,1,-5000.0,0,0";; esac\n' > "$S/bin/chronyc" + out=$(req '{"mode":"step","threshold":1,"timeout":3,"interval":1}') + [ "$(jget "$out" ok)" = False ]; [ "$(jget "$out" reason)" = adimlanamadi ] +} + +@test "step: chrony yoksa chrony_yok" { + touch "$S/down" + out=$(req '{"mode":"step","timeout":2,"interval":1}') + [ "$(jget "$out" reason)" = chrony_yok ] +} + +@test "geçersiz girdi JSON hata döner ve çıkış kodu 1" { + run req '{"mode":"x"}' + [ "$status" -eq 1 ] +} diff --git a/test/e2e/lab.bats b/test/e2e/lab.bats index 084a061..cce1fca 100644 --- a/test/e2e/lab.bats +++ b/test/e2e/lab.bats @@ -150,4 +150,69 @@ v2() { cat "$BATS_FILE_TMPDIR/v2path"; } [ "$status" -eq 0 ] } +# --- timedated önbelleği --- +@test "timedated: apply ve geri_yukle sonrası timedatectl dosyayla uyumlu" { + run lab timedatectl show -p Timezone # önbelleği doldurur + [ "$output" = "Timezone=Etc/UTC" ] + run "$RUN" apply -e "@$(v2)" + [ "$status" -eq 0 ] + run lab timedatectl show -p Timezone + [ "$output" = "Timezone=America/New_York" ] + run "$RUN" geri_yukle + [ "$status" -eq 0 ] + run lab timedatectl show -p Timezone + [ "$output" = "Timezone=Etc/UTC" ] +} + +# --- saat sapması (SAHTE chronyc: LAB'da chronyd ve saat adımlama yok; yalnızca utility_clock.py + playbook akışı sınanır) --- +stub_on() { + docker cp "$BATS_TEST_DIRNAME/lab/chronyc-stub.sh" utility-lab:/usr/local/bin/chronyc + lab sh -c 'mkdir -p /run/utility-stub && cd /run/utility-stub && echo -1152060 > offset && echo 377 > reach && echo 0 > selected && rm -f steps' +} + +@test "saat sapması: durum ntp_senkron=no ve sapmayı gösterir" { + stub_on + run "$RUN" durum + [ "$status" -eq 0 ] + [[ "$(result)" == *" ntp_senkron=no "* && "$(result)" == *" sapma_ms=1152060000.0 secili_kaynak=no" ]] +} + +@test "saat sapması: preview 'saat' planlar ve hiçbir şey adımlamaz" { + run "$RUN" preview --check --diff + [ "$status" -eq 0 ] + [ "$(result)" = "SSHPOCKET_RESULT utility.preview degisen=1 plan=saat" ] + [[ "$output" == *"apply chrony ile TEK SEFERLİK adımlayacak"* ]] + run lab sh -c 'test ! -e /run/utility-stub/steps && echo temiz' + [ "$output" = temiz ] +} + +@test "saat sapması: utility_ntp_makestep=false sapmaya dokunmaz" { + run "$RUN" preview --check -e utility_ntp_makestep=false + [ "$(result)" = "SSHPOCKET_RESULT utility.preview degisen=0 plan=yok" ] +} + +@test "saat sapması: apply tek seferlik makestep yapar, ikinci apply changed=0" { + run "$RUN" apply + [ "$status" -eq 0 ] + [ "$(result)" = "SSHPOCKET_RESULT utility.apply degisen=1 plan=saat yedek=hayir" ] + [[ "$output" == *"saat adımlandı"* ]] + run lab sh -c 'wc -l < /run/utility-stub/steps' + [ "$output" = 1 ] + run "$RUN" apply + [ "$(result)" = "SSHPOCKET_RESULT utility.apply degisen=0 plan=yok yedek=hayir" ] + [ "$(recap_changed)" = 0 ] + run lab sh -c 'wc -l < /run/utility-stub/steps' + [ "$output" = 1 ] + run "$RUN" durum + [[ "$(result)" == *" ntp_senkron=yes "* && "$(result)" == *" secili_kaynak=yes" ]] +} + +@test "saat sapması: makestep düzeltmezse apply net hatayla düşer" { + lab sh -c 'echo -1152060 > /run/utility-stub/offset; echo 0 > /run/utility-stub/selected; printf "#!/bin/sh\ncase \"\$*\" in \"-c tracking\") echo 00000000,,0,0,0;; \"-c sources\") echo ^,?,n,2,6,377,1,-5000.0,0,0;; esac\n" > /usr/local/bin/chronyc' + run "$RUN" apply -e utility_ntp_step_timeout_s=4 + [ "$status" -ne 0 ] + [[ "$output" == *"adimlanamadi"* ]] + lab rm -f /usr/local/bin/chronyc +} + teardown_file() { "$BATS_TEST_DIRNAME/lab/down.sh" --clean >&3 || true; } diff --git a/test/e2e/lab/chronyc-stub.sh b/test/e2e/lab/chronyc-stub.sh new file mode 100755 index 0000000..dccbc64 --- /dev/null +++ b/test/e2e/lab/chronyc-stub.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +# LAB için SAHTE chronyc: konteynerde chronyd/saat adımlama yoktur. /run/utility-stub/{offset,reach,selected} durumunu okur; +# `makestep` (reach>0 ise) sapmayı kapatır ve /run/utility-stub/steps'e satır ekler. Yalnızca utility_clock.py mantığını sınar, chrony'yi DEĞİL. +S=/run/utility-stub +off=$(cat "$S/offset"); reach=$(cat "$S/reach"); sel=$(cat "$S/selected") +case "$*" in + "-c tracking") if [ "$sel" = 1 ]; then echo "A9FE0101,x,3,0,$off,0,0,0,0,0,0,0,0"; else echo "00000000,,0,0,0,0,0,0,0,0,0,0,0"; fi ;; + "-c sources") if [ "$sel" = 1 ]; then st='*'; else st='?'; fi; echo "^,$st,ntp.example,2,6,$reach,10,$off,0,0" ;; + "makestep") if [ "$reach" != 0 ]; then echo 0.0002 > "$S/offset"; echo 1 > "$S/selected"; echo x >> "$S/steps"; fi ;; + *) exit 1 ;; +esac diff --git a/test/run-all.sh b/test/run-all.sh index 6c51d66..293b81c 100755 --- a/test/run-all.sh +++ b/test/run-all.sh @@ -3,6 +3,6 @@ cd "$(dirname "$0")/.." || exit 1 rc=0 for t in test/test_*.sh; do echo "=== $t"; bash "$t" || rc=1; done -echo "=== test/backup.bats"; bats test/backup.bats || rc=1 +echo "=== test/backup.bats test/clock.bats"; bats test/backup.bats test/clock.bats || rc=1 echo "=== scripts/check-consistency.py"; python3 -I scripts/check-consistency.py || rc=1 exit $rc From 14a91fec853dbcbc2feea4138af629999651ccf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ziya=20Ye=C5=9Filbah=C3=A7e?= Date: Fri, 6 Nov 2026 22:13:55 +0300 Subject: [PATCH 2/2] =?UTF-8?q?S=C3=BCr=C3=BCm=200.1.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 4 +--- VERSION | 2 +- component.json | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d71a420..0ffdbbf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,7 @@ Biçim [Keep a Changelog](https://keepachangelog.com/) esas alınmıştır. -## [Unreleased] - -Sürüm numarası bekliyor (öneri: 0.2.0: yeni değişken, yeni `durum` alanları, saat adımlama davranışı). +## [0.1.1] - 2026-11-06 ### Eklendi - **Saat sapması düzeltme.** chrony çalışırken saat 1 sn'den fazla sapmışsa `preview` plana `saat` ekler ve nedenini yazar; `apply` kaynaklar erişilebilir olunca (en çok 60 sn bekler) TEK SEFERLİK `chronyc makestep` yapar ve eşik altına inmesini doğrular. Kalıcı `makestep` ayarı YAZILMAZ. Ölçülemezse (erişilebilir kaynak yok) uyarır; ölçüldü ama düzelmediyse hata verir. Kapatmak için `utility_ntp_makestep=false` (varsayılan `true`). Uyarı metinleri adımlamanın etkisini (log/cron/auditd `time-change`, ssh/TOTP) söyler. diff --git a/VERSION b/VERSION index 6e8bf73..17e51c3 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.1.0 +0.1.1 diff --git a/component.json b/component.json index baa183a..f178d66 100644 --- a/component.json +++ b/component.json @@ -1,6 +1,6 @@ { "id": "utility", - "version": "0.1.0", + "version": "0.1.1", "sshpocket_api": 1, "name": "Sunucu araçları: saat dilimi, locale, NTP", "description": "Saat dilimini, sistem dilini (LANG) ve NTP kaynaklarını (chrony drop-in) önizleyerek, yedekleyerek ve onayla yönetir. Ubuntu 26.04 ve Debian 13; Proxmox VE kapsam dışı.",