From e51cdd987c8d88c1ddad12ca6afd23e3a9d1886c Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 7 Sep 2022 13:07:52 +0200 Subject: [PATCH 01/30] helper ynh_get_ram: LANG= isn't enough to get en_US output, gotta use LC_ALL --- helpers/hardware | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/helpers/hardware b/helpers/hardware index 337630fa8..3ccf7ffe8 100644 --- a/helpers/hardware +++ b/helpers/hardware @@ -30,8 +30,8 @@ ynh_get_ram() { ram=0 # Use the total amount of ram elif [ $free -eq 1 ]; then - local free_ram=$(LANG=C vmstat --stats --unit M | grep "free memory" | awk '{print $1}') - local free_swap=$(LANG=C vmstat --stats --unit M | grep "free swap" | awk '{print $1}') + local free_ram=$(LC_ALL=C vmstat --stats --unit M | grep "free memory" | awk '{print $1}') + local free_swap=$(LC_ALL=C vmstat --stats --unit M | grep "free swap" | awk '{print $1}') local free_ram_swap=$((free_ram + free_swap)) # Use the total amount of free ram @@ -44,8 +44,8 @@ ynh_get_ram() { ram=$free_swap fi elif [ $total -eq 1 ]; then - local total_ram=$(LANG=C vmstat --stats --unit M | grep "total memory" | awk '{print $1}') - local total_swap=$(LANG=C vmstat --stats --unit M | grep "total swap" | awk '{print $1}') + local total_ram=$(LC_ALL=C vmstat --stats --unit M | grep "total memory" | awk '{print $1}') + local total_swap=$(LC_ALL=C vmstat --stats --unit M | grep "total swap" | awk '{print $1}') local total_ram_swap=$((total_ram + total_swap)) local ram=$total_ram_swap From 6a914fb2b5713caa4f611acb1d162309b90660e8 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 7 Sep 2022 13:09:35 +0200 Subject: [PATCH 02/30] Update changelog for 11.0.9.14 --- debian/changelog | 8 +++++++- maintenance/make_changelog.sh | 2 -- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/debian/changelog b/debian/changelog index 32c42fb9c..f6fbe6eba 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,10 @@ +yunohost (11.0.9.14) stable; urgency=low + + - [fix] dns: confusion on XMPP CNAME records for nohost.me & co domains (f6057d25) + - [fix] helper ynh_get_ram: LANG= isn't enough to get en_US output, gotta use LC_ALL (e51cdd98) + + -- Alexandre Aubin Wed, 07 Sep 2022 13:08:31 +0200 + yunohost (11.0.9.13) stable; urgency=low - [fix] defaultapp: domain may not exist in app_map dict output (efe0e601) @@ -10,7 +17,6 @@ yunohost (11.0.9.13) stable; urgency=low -- Alexandre Aubin Sat, 03 Sep 2022 23:27:56 +0200 - yunohost (11.0.9.12) stable; urgency=low - [fix] postinstall: check all partitions (not only physical ones) ([#1497](https://github.com/YunoHost/yunohost/pull/1497)) diff --git a/maintenance/make_changelog.sh b/maintenance/make_changelog.sh index 89087eba3..a73b5061b 100644 --- a/maintenance/make_changelog.sh +++ b/maintenance/make_changelog.sh @@ -7,8 +7,6 @@ EMAIL=$(git config --global --get user.email) LAST_RELEASE=$(git tag --list 'debian/11.*' --sort="v:refname" | tail -n 1) -echo $LAST_RELEASE - echo "$REPO ($VERSION) $RELEASE; urgency=low" echo "" From b0411d5da99fcfbac60d4335fcbca0a66096cca6 Mon Sep 17 00:00:00 2001 From: "ljf (zamentur)" Date: Mon, 12 Sep 2022 01:47:03 +0200 Subject: [PATCH 03/30] [fix] Lidswitch if no reboot --- hooks/conf_regen/01-yunohost | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/hooks/conf_regen/01-yunohost b/hooks/conf_regen/01-yunohost index dc0bfc689..14c0da969 100755 --- a/hooks/conf_regen/01-yunohost +++ b/hooks/conf_regen/01-yunohost @@ -221,7 +221,10 @@ do_post_regen() { systemctl restart ntp } [[ ! "$regen_conf_files" =~ "nftables.service.d/ynh-override.conf" ]] || systemctl daemon-reload - [[ ! "$regen_conf_files" =~ "login.conf.d/ynh-override.conf" ]] || systemctl daemon-reload + [[ ! "$regen_conf_files" =~ "login.conf.d/ynh-override.conf" ]] || { + systemctl daemon-reload + systemctl restart systemd-logind + } [[ ! "$regen_conf_files" =~ "yunohost-firewall.service" ]] || systemctl daemon-reload [[ ! "$regen_conf_files" =~ "yunohost-api.service" ]] || systemctl daemon-reload From 7e8aa0a67dd59b7cb991a6e527b044b78fafa9b2 Mon Sep 17 00:00:00 2001 From: ButterflyOfFire Date: Sun, 4 Sep 2022 11:14:45 +0000 Subject: [PATCH 04/30] Translated using Weblate (Arabic) Currently translated at 13.4% (93 of 693 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/ar/ --- locales/ar.json | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/locales/ar.json b/locales/ar.json index 17603ba8f..62ee173ab 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -30,7 +30,7 @@ "backup_method_copy_finished": "إنتهت عملية النسخ الإحتياطي", "backup_nothings_done": "ليس هناك أي شيء للحفظ", "backup_output_directory_required": "يتوجب عليك تحديد مجلد لتلقي النسخ الإحتياطية", - "certmanager_cert_install_success": "تمت عملية تنصيب شهادة Let's Encrypt بنجاح على النطاق {domain}", + "certmanager_cert_install_success": "تمت عملية تنصيب شهادة Let's Encrypt بنجاح على النطاق '{domain}'", "certmanager_cert_install_success_selfsigned": "نجحت عملية تثبيت الشهادة الموقعة ذاتيا الخاصة بالنطاق '{domain}'", "certmanager_cert_renew_success": "نجحت عملية تجديد شهادة Let's Encrypt الخاصة باسم النطاق '{domain}'", "certmanager_cert_signing_failed": "فشل إجراء توقيع الشهادة الجديدة", @@ -63,7 +63,7 @@ "service_disabled": "لن يتم إطلاق خدمة '{service}' أثناء بداية تشغيل النظام.", "service_enabled": "تم تنشيط خدمة '{service}'", "service_removed": "تمت إزالة خدمة '{service}'", - "service_started": "تم إطلاق تشغيل خدمة '{service}'", + "service_started": "تم إطلاق تشغيل خدمة '{service}'", "service_stopped": "تمّ إيقاف خدمة '{service}'", "system_upgraded": "تمت عملية ترقية النظام", "unlimit": "دون تحديد الحصة", @@ -159,5 +159,6 @@ "diagnosis_description_dnsrecords": "تسجيلات خدمة DNS", "diagnosis_description_ip": "الإتصال بالإنترنت", "diagnosis_description_basesystem": "النظام الأساسي", - "field_invalid": "الحقل غير صحيح : '{}'" -} \ No newline at end of file + "field_invalid": "الحقل غير صحيح : '{}'", + "diagnosis_ignored_issues": "(+ {nb_ignored} مشاكل تم تجاهلها)" +} From df2a261d3276dec43e88a3c6646e0f0cb915d1d9 Mon Sep 17 00:00:00 2001 From: Sedat Albayrak Date: Sun, 11 Sep 2022 15:54:13 +0000 Subject: [PATCH 05/30] Translated using Weblate (Turkish) Currently translated at 1.2% (9 of 693 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/tr/ --- locales/tr.json | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/locales/tr.json b/locales/tr.json index 6c881eec7..8eb68a247 100644 --- a/locales/tr.json +++ b/locales/tr.json @@ -1,3 +1,11 @@ { - "password_too_simple_1": "Şifre en az 8 karakter uzunluğunda olmalı" -} \ No newline at end of file + "password_too_simple_1": "Şifre en az 8 karakter uzunluğunda olmalı", + "action_invalid": "Geçersiz işlem '{action}'", + "admin_password": "Yönetici şifresi", + "admin_password_change_failed": "Şifre değiştirme başarısız oldu", + "admin_password_changed": "Yönetici şifresi değişti", + "admin_password_too_long": "Lütfen 127 karakterden kısa bir şifre seçin", + "already_up_to_date": "Yapılacak yeni bir şey yok. Her şey zaten güncel.", + "app_action_broke_system": "Bu işlem bazı hizmetleri bozmuş olabilir: {services}", + "good_practices_about_user_password": "Şimdi yeni bir kullanıcı şifresi tanımlamak üzeresiniz. Parola en az 8 karakter uzunluğunda olmalıdır - ancak daha uzun bir parola (yani bir parola) ve/veya çeşitli karakterler (büyük harf, küçük harf, rakamlar ve özel karakterler) daha iyidir." +} From 9bc82d6b5ff4fb6dd5594cd58bb709cd1132767a Mon Sep 17 00:00:00 2001 From: Sedat Albayrak Date: Mon, 12 Sep 2022 12:01:00 +0000 Subject: [PATCH 06/30] Translated using Weblate (Turkish) Currently translated at 1.4% (10 of 693 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/tr/ --- locales/tr.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/tr.json b/locales/tr.json index 8eb68a247..6dd03c57e 100644 --- a/locales/tr.json +++ b/locales/tr.json @@ -7,5 +7,6 @@ "admin_password_too_long": "Lütfen 127 karakterden kısa bir şifre seçin", "already_up_to_date": "Yapılacak yeni bir şey yok. Her şey zaten güncel.", "app_action_broke_system": "Bu işlem bazı hizmetleri bozmuş olabilir: {services}", - "good_practices_about_user_password": "Şimdi yeni bir kullanıcı şifresi tanımlamak üzeresiniz. Parola en az 8 karakter uzunluğunda olmalıdır - ancak daha uzun bir parola (yani bir parola) ve/veya çeşitli karakterler (büyük harf, küçük harf, rakamlar ve özel karakterler) daha iyidir." + "good_practices_about_user_password": "Şimdi yeni bir kullanıcı şifresi tanımlamak üzeresiniz. Parola en az 8 karakter uzunluğunda olmalıdır - ancak daha uzun bir parola (yani bir parola) ve/veya çeşitli karakterler (büyük harf, küçük harf, rakamlar ve özel karakterler) daha iyidir.", + "aborting": "İptal ediliyor." } From 51f95c8a5cac55229d58611151df604e4fc30e52 Mon Sep 17 00:00:00 2001 From: marty hiatt Date: Tue, 13 Sep 2022 16:57:44 +0200 Subject: [PATCH 07/30] edits to locales/en.json --- locales/en.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/locales/en.json b/locales/en.json index b7c18ca70..a3233453c 100644 --- a/locales/en.json +++ b/locales/en.json @@ -15,7 +15,7 @@ "app_already_up_to_date": "{app} is already up-to-date", "app_argument_choice_invalid": "Pick a valid value for argument '{name}': '{value}' is not among the available choices ({choices})", "app_argument_invalid": "Pick a valid value for the argument '{name}': {error}", - "app_argument_password_no_default": "Error while parsing password argument '{name}': password argument can't have a default value for security reason", + "app_argument_password_no_default": "Error while parsing password argument '{name}': password argument can't have a default value for security reasons", "app_argument_required": "Argument '{name}' is required", "app_change_url_identical_domains": "The old and new domain/url_path are identical ('{domain}{path}'), nothing to do.", "app_change_url_no_script": "The app '{app_name}' doesn't support URL modification yet. Maybe you should upgrade it.", @@ -41,13 +41,13 @@ "app_not_properly_removed": "{app} has not been properly removed", "app_not_upgraded": "The app '{failed_app}' failed to upgrade, and as a consequence the following apps' upgrades have been cancelled: {apps}", "app_packaging_format_not_supported": "This app cannot be installed because its packaging format is not supported by your YunoHost version. You should probably consider upgrading your system.", - "app_remove_after_failed_install": "Removing the app following the installation failure...", + "app_remove_after_failed_install": "Removing the app after installation failure...", "app_removed": "{app} uninstalled", "app_requirements_checking": "Checking required packages for {app}...", "app_requirements_unmeet": "Requirements are not met for {app}, the package {pkgname} ({version}) must be {spec}", "app_restore_failed": "Could not restore {app}: {error}", "app_restore_script_failed": "An error occured inside the app restore script", - "app_sources_fetch_failed": "Could not fetch sources files, is the URL correct?", + "app_sources_fetch_failed": "Could not fetch source files, is the URL correct?", "app_start_backup": "Collecting files to be backed up for {app}...", "app_start_install": "Installing {app}...", "app_start_remove": "Removing {app}...", @@ -82,7 +82,7 @@ "backup_applying_method_tar": "Creating the backup TAR archive...", "backup_archive_app_not_found": "Could not find {app} in the backup archive", "backup_archive_broken_link": "Could not access the backup archive (broken link to {path})", - "backup_archive_cant_retrieve_info_json": "Could not load infos for archive '{archive}'... The info.json cannot be retrieved (or is not a valid json).", + "backup_archive_cant_retrieve_info_json": "Could not load info for archive '{archive}'... The info.json file cannot be retrieved (or is not a valid json).", "backup_archive_corrupted": "It looks like the backup archive '{archive}' is corrupted : {error}", "backup_archive_name_exists": "A backup archive with this name already exists.", "backup_archive_name_unknown": "Unknown local backup archive named '{name}'", @@ -120,7 +120,7 @@ "backup_unable_to_organize_files": "Could not use the quick method to organize files in the archive", "backup_with_no_backup_script_for_app": "The app '{app}' has no backup script. Ignoring.", "backup_with_no_restore_script_for_app": "{app} has no restoration script, you will not be able to automatically restore the backup of this app.", - "certmanager_acme_not_configured_for_domain": "The ACME challenge cannot be ran for {domain} right now because its nginx conf lacks the corresponding code snippet... Please make sure that your nginx configuration is up to date using `yunohost tools regen-conf nginx --dry-run --with-diff`.", + "certmanager_acme_not_configured_for_domain": "The ACME challenge cannot be run for {domain} right now because its nginx conf lacks the corresponding code snippet... Please make sure that your nginx configuration is up to date using `yunohost tools regen-conf nginx --dry-run --with-diff`.", "certmanager_attempt_to_renew_nonLE_cert": "The certificate for the domain '{domain}' is not issued by Let's Encrypt. Cannot renew it automatically!", "certmanager_attempt_to_renew_valid_cert": "The certificate for the domain '{domain}' is not about to expire! (You may use --force if you know what you're doing)", "certmanager_attempt_to_replace_valid_cert": "You are attempting to overwrite a good and valid certificate for domain {domain}! (Use --force to bypass)", @@ -131,7 +131,7 @@ "certmanager_cert_signing_failed": "Could not sign the new certificate", "certmanager_certificate_fetching_or_enabling_failed": "Trying to use the new certificate for {domain} did not work...", "certmanager_domain_cert_not_selfsigned": "The certificate for domain {domain} is not self-signed. Are you sure you want to replace it? (Use '--force' to do so.)", - "certmanager_domain_dns_ip_differs_from_public_ip": "The DNS records for domain '{domain}' is different from this server's IP. Please check the 'DNS records' (basic) category in the diagnosis for more info. If you recently modified your A record, please wait for it to propagate (some DNS propagation checkers are available online). (If you know what you are doing, use '--no-checks' to turn off those checks.)", + "certmanager_domain_dns_ip_differs_from_public_ip": "The DNS records for domain '{domain}' are different to this server's IP. Please check the 'DNS records' (basic) category in the diagnosis for more info. If you recently modified your A record, please wait for it to propagate (some DNS propagation checkers are available online). (If you know what you are doing, use '--no-checks' to turn off those checks.)", "certmanager_domain_http_not_working": "Domain {domain} does not seem to be accessible through HTTP. Please check the 'Web' category in the diagnosis for more info. (If you know what you are doing, use '--no-checks' to turn off those checks.)", "certmanager_domain_not_diagnosed_yet": "There is no diagnosis result for domain {domain} yet. Please re-run a diagnosis for categories 'DNS records' and 'Web' in the diagnosis section to check if the domain is ready for Let's Encrypt. (Or if you know what you are doing, use '--no-checks' to turn off those checks.)", "certmanager_hit_rate_limit": "Too many certificates already issued for this exact set of domains {domain} recently. Please try again later. See https://letsencrypt.org/docs/rate-limits/ for more details", @@ -527,8 +527,8 @@ "migrations_need_to_accept_disclaimer": "To run the migration {id}, your must accept the following disclaimer:\n---\n{disclaimer}\n---\nIf you accept to run the migration, please re-run the command with the option '--accept-disclaimer'.", "migrations_no_migrations_to_run": "No migrations to run", "migrations_no_such_migration": "There is no migration called '{id}'", - "migrations_not_pending_cant_skip": "Those migrations are not pending, so cannot be skipped: {ids}", - "migrations_pending_cant_rerun": "Those migrations are still pending, so cannot be run again: {ids}", + "migrations_not_pending_cant_skip": "These migrations are not pending, so cannot be skipped: {ids}", + "migrations_pending_cant_rerun": "These migrations are still pending, so cannot be run again: {ids}", "migrations_running_forward": "Running migration {id}...", "migrations_skip_migration": "Skipping migration {id}...", "migrations_success_forward": "Migration {id} completed", From 5af946dbfec35ffdbd9263347a36575947160e57 Mon Sep 17 00:00:00 2001 From: marty hiatt Date: Tue, 20 Sep 2022 11:51:34 +0200 Subject: [PATCH 08/30] edits to locales/en.json -- up to the end of diagnosis --- locales/en.json | 60 ++++++++++++++++++++++++------------------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/locales/en.json b/locales/en.json index a3233453c..735cf4c15 100644 --- a/locales/en.json +++ b/locales/en.json @@ -131,9 +131,9 @@ "certmanager_cert_signing_failed": "Could not sign the new certificate", "certmanager_certificate_fetching_or_enabling_failed": "Trying to use the new certificate for {domain} did not work...", "certmanager_domain_cert_not_selfsigned": "The certificate for domain {domain} is not self-signed. Are you sure you want to replace it? (Use '--force' to do so.)", - "certmanager_domain_dns_ip_differs_from_public_ip": "The DNS records for domain '{domain}' are different to this server's IP. Please check the 'DNS records' (basic) category in the diagnosis for more info. If you recently modified your A record, please wait for it to propagate (some DNS propagation checkers are available online). (If you know what you are doing, use '--no-checks' to turn off those checks.)", - "certmanager_domain_http_not_working": "Domain {domain} does not seem to be accessible through HTTP. Please check the 'Web' category in the diagnosis for more info. (If you know what you are doing, use '--no-checks' to turn off those checks.)", - "certmanager_domain_not_diagnosed_yet": "There is no diagnosis result for domain {domain} yet. Please re-run a diagnosis for categories 'DNS records' and 'Web' in the diagnosis section to check if the domain is ready for Let's Encrypt. (Or if you know what you are doing, use '--no-checks' to turn off those checks.)", + "certmanager_domain_dns_ip_differs_from_public_ip": "The DNS records for domain '{domain}' are different to this server's IP. Please check the 'DNS records' (basic) category in the diagnosis for more info. If you recently modified your A record, please wait for it to propagate (some DNS propagation checkers are available online). (If you know what you are doing, use '--no-checks' to turn off these checks.)", + "certmanager_domain_http_not_working": "Domain {domain} does not seem to be accessible through HTTP. Please check the 'Web' category in the diagnosis for more info. (If you know what you are doing, use '--no-checks' to turn off these checks.)", + "certmanager_domain_not_diagnosed_yet": "There is no diagnosis result for domain {domain} yet. Please re-run a diagnosis for categories 'DNS records' and 'Web' in the diagnosis section to check if the domain is ready for Let's Encrypt. (Or if you know what you are doing, use '--no-checks' to turn off these checks.)", "certmanager_hit_rate_limit": "Too many certificates already issued for this exact set of domains {domain} recently. Please try again later. See https://letsencrypt.org/docs/rate-limits/ for more details", "certmanager_no_cert_file": "Could not read the certificate file for the domain {domain} (file: {file})", "certmanager_self_ca_conf_file_not_found": "Could not find configuration file for self-signing authority (file: {file})", @@ -152,17 +152,17 @@ "config_version_not_supported": "Config panel versions '{version}' are not supported.", "confirm_app_install_danger": "DANGER! This app is known to be still experimental (if not explicitly not working)! You should probably NOT install it unless you know what you are doing. NO SUPPORT will be provided if this app doesn't work or breaks your system... If you are willing to take that risk anyway, type '{answers}'", "confirm_app_install_thirdparty": "DANGER! This app is not part of YunoHost's app catalog. Installing third-party apps may compromise the integrity and security of your system. You should probably NOT install it unless you know what you are doing. NO SUPPORT will be provided if this app doesn't work or breaks your system... If you are willing to take that risk anyway, type '{answers}'", - "confirm_app_install_warning": "Warning: This app may work, but is not well-integrated in YunoHost. Some features such as single sign-on and backup/restore might not be available. Install anyway? [{answers}] ", + "confirm_app_install_warning": "Warning: This app may work, but is not well-integrated into YunoHost. Some features such as single sign-on and backup/restore might not be available. Install anyway? [{answers}] ", "custom_app_url_required": "You must provide a URL to upgrade your custom app {app}", "danger": "Danger:", "diagnosis_apps_allgood": "All installed apps respect basic packaging practices", "diagnosis_apps_bad_quality": "This application is currently flagged as broken on YunoHost's application catalog. This may be a temporary issue while the maintainers attempt to fix the issue. In the meantime, upgrading this app is disabled.", "diagnosis_apps_broken": "This application is currently flagged as broken on YunoHost's application catalog. This may be a temporary issue while the maintainers attempt to fix the issue. In the meantime, upgrading this app is disabled.", - "diagnosis_apps_deprecated_practices": "This app's installed version still uses some super-old deprecated packaging practices. You should really consider upgrading it.", + "diagnosis_apps_deprecated_practices": "This app's installed version still uses some very old, deprecated packaging practices. You should really consider upgrading it.", "diagnosis_apps_issue": "An issue was found for app {app}", - "diagnosis_apps_not_in_app_catalog": "This application is not in YunoHost's application catalog. If it was in the past and got removed, you should consider uninstalling this app as it won't receive upgrade, and may compromise the integrity and security of your system.", + "diagnosis_apps_not_in_app_catalog": "This application is not in YunoHost's application catalog. If it was in the past and was removed, you should consider uninstalling this app as it won't receive upgrades and may compromise the integrity and security of your system.", "diagnosis_apps_outdated_ynh_requirement": "This app's installed version only requires yunohost >= 2.x or 3.x, which tends to indicate that it's not up to date with recommended packaging practices and helpers. You should really consider upgrading it.", - "diagnosis_backports_in_sources_list": "It looks like apt (the package manager) is configured to use the backports repository. Unless you really know what you are doing, we strongly discourage from installing packages from backports, because it's likely to create unstabilities or conflicts on your system.", + "diagnosis_backports_in_sources_list": "It looks like apt (the package manager) is configured to use the backports repository. Unless you really know what you are doing, we strongly discourage installing packages from backports, because it's likely to create unstabilities or conflicts on your system.", "diagnosis_basesystem_hardware": "Server hardware architecture is {virt} {arch}", "diagnosis_basesystem_hardware_model": "Server model is {model}", "diagnosis_basesystem_host": "Server is running Debian {debian_version}", @@ -190,7 +190,7 @@ "diagnosis_dns_discrepancy": "The following DNS record does not seem to follow the recommended configuration:
Type: {type}
Name: {name}
Current value: {current}
Expected value: {value}", "diagnosis_dns_good_conf": "DNS records are correctly configured for domain {domain} (category {category})", "diagnosis_dns_missing_record": "According to the recommended DNS configuration, you should add a DNS record with the following info.
Type: {type}
Name: {name}
Value: {value}", - "diagnosis_dns_point_to_doc": "Please check the documentation at https://yunohost.org/dns_config if you need help about configuring DNS records.", + "diagnosis_dns_point_to_doc": "Please check the documentation at https://yunohost.org/dns_config if you need help configuring DNS records.", "diagnosis_dns_specialusedomain": "Domain {domain} is based on a special-use top-level domain (TLD) such as .local or .test and is therefore not expected to have actual DNS records.", "diagnosis_dns_try_dyndns_update_force": "This domain's DNS configuration should automatically be managed by YunoHost. If that's not the case, you can try to force an update using yunohost dyndns update --force.", "diagnosis_domain_expiration_error": "Some domains will expire VERY SOON!", @@ -214,14 +214,14 @@ "diagnosis_http_hairpinning_issue": "Your local network does not seem to have hairpinning enabled.", "diagnosis_http_hairpinning_issue_details": "This is probably because of your ISP box / router. As a result, people from outside your local network will be able to access your server as expected, but not people from inside the local network (like you, probably?) when using the domain name or global IP. You may be able to improve the situation by having a look at https://yunohost.org/dns_local_network", "diagnosis_http_nginx_conf_not_up_to_date": "This domain's nginx configuration appears to have been modified manually, and prevents YunoHost from diagnosing if it's reachable on HTTP.", - "diagnosis_http_nginx_conf_not_up_to_date_details": "To fix the situation, inspect the difference with the command line using yunohost tools regen-conf nginx --dry-run --with-diff and if you're ok, apply the changes with yunohost tools regen-conf nginx --force.", + "diagnosis_http_nginx_conf_not_up_to_date_details": "To fix the situation, inspect the difference from the command line using yunohost tools regen-conf nginx --dry-run --with-diff and if you're ok with it, apply the changes with yunohost tools regen-conf nginx --force.", "diagnosis_http_ok": "Domain {domain} is reachable through HTTP from outside the local network.", "diagnosis_http_partially_unreachable": "Domain {domain} appears unreachable through HTTP from outside the local network in IPv{failed}, though it works in IPv{passed}.", "diagnosis_http_special_use_tld": "Domain {domain} is based on a special-use top-level domain (TLD) such as .local or .test and is therefore not expected to be exposed outside the local network.", - "diagnosis_http_timeout": "Timed-out while trying to contact your server from outside. It appears to be unreachable.
1. The most common cause for this issue is that port 80 (and 443) are not correctly forwarded to your server.
2. You should also make sure that the service nginx is running
3. On more complex setups: make sure that no firewall or reverse-proxy is interfering.", + "diagnosis_http_timeout": "Timed-out while trying to contact your server from the outside. It appears to be unreachable.
1. The most common cause for this issue is that port 80 (and 443) are not correctly forwarded to your server.
2. You should also make sure that the service nginx is running
3. On more complex setups: make sure that no firewall or reverse-proxy is interfering.", "diagnosis_http_unreachable": "Domain {domain} appears unreachable through HTTP from outside the local network.", "diagnosis_ignored_issues": "(+ {nb_ignored} ignored issue(s))", - "diagnosis_ip_broken_dnsresolution": "Domain name resolution seems to be broken for some reason... Is a firewall blocking DNS requests ?", + "diagnosis_ip_broken_dnsresolution": "Domain name resolution seems to be broken for some reason... Is a firewall blocking DNS requests?", "diagnosis_ip_broken_resolvconf": "Domain name resolution seems to be broken on your server, which seems related to /etc/resolv.conf not pointing to 127.0.0.1.", "diagnosis_ip_connected_ipv4": "The server is connected to the Internet through IPv4!", "diagnosis_ip_connected_ipv6": "The server is connected to the Internet through IPv6!", @@ -237,26 +237,26 @@ "diagnosis_mail_blacklist_listed_by": "Your IP or domain {item} is blacklisted on {blacklist_name}", "diagnosis_mail_blacklist_ok": "The IPs and domains used by this server do not appear to be blacklisted", "diagnosis_mail_blacklist_reason": "The blacklist reason is: {reason}", - "diagnosis_mail_blacklist_website": "After identifying why you are listed and fixed it, feel free to ask for your IP or domaine to be removed on {blacklist_website}", + "diagnosis_mail_blacklist_website": "After identifying why you are listed and fixing it, feel free to ask for your IP or domain to be removed on {blacklist_website}", "diagnosis_mail_ehlo_bad_answer": "A non-SMTP service answered on port 25 on IPv{ipversion}", - "diagnosis_mail_ehlo_bad_answer_details": "It could be due to an other machine answering instead of your server.", - "diagnosis_mail_ehlo_could_not_diagnose": "Could not diagnose if postfix mail server is reachable from outside in IPv{ipversion}.", + "diagnosis_mail_ehlo_bad_answer_details": "It could be due to an another machine answering instead of your server.", + "diagnosis_mail_ehlo_could_not_diagnose": "Could not diagnose if postfix mail server is reachable from the outside in IPv{ipversion}.", "diagnosis_mail_ehlo_could_not_diagnose_details": "Error: {error}", "diagnosis_mail_ehlo_ok": "The SMTP mail server is reachable from the outside and therefore is able to receive emails!", "diagnosis_mail_ehlo_unreachable": "The SMTP mail server is unreachable from the outside on IPv{ipversion}. It won't be able to receive emails.", "diagnosis_mail_ehlo_unreachable_details": "Could not open a connection on port 25 to your server in IPv{ipversion}. It appears to be unreachable.
1. The most common cause for this issue is that port 25 is not correctly forwarded to your server.
2. You should also make sure that service postfix is running.
3. On more complex setups: make sure that no firewall or reverse-proxy is interfering.", "diagnosis_mail_ehlo_wrong": "A different SMTP mail server answers on IPv{ipversion}. Your server will probably not be able to receive emails.", "diagnosis_mail_ehlo_wrong_details": "The EHLO received by the remote diagnoser in IPv{ipversion} is different from your server's domain.
Received EHLO: {wrong_ehlo}
Expected: {right_ehlo}
The most common cause for this issue is that port 25 is not correctly forwarded to your server. Alternatively, make sure that no firewall or reverse-proxy is interfering.", - "diagnosis_mail_fcrdns_different_from_ehlo_domain": "The reverse DNS is not correctly configured in IPv{ipversion}. Some emails may fail to get delivered or may get flagged as spam.", + "diagnosis_mail_fcrdns_different_from_ehlo_domain": "Reverse DNS is not correctly configured for IPv{ipversion}. Some emails may fail to get delivered or be flagged as spam.", "diagnosis_mail_fcrdns_different_from_ehlo_domain_details": "Current reverse DNS: {rdns_domain}
Expected value: {ehlo_domain}", - "diagnosis_mail_fcrdns_dns_missing": "No reverse DNS is defined in IPv{ipversion}. Some emails may fail to get delivered or may get flagged as spam.", - "diagnosis_mail_fcrdns_nok_alternatives_4": "Some providers won't let you configure your reverse DNS (or their feature might be broken...). If you are experiencing issues because of this, consider the following solutions:
- Some ISP provide the alternative of using a mail server relay though it implies that the relay will be able to spy on your email traffic.
- A privacy-friendly alternative is to use a VPN *with a dedicated public IP* to bypass this kind of limits. See https://yunohost.org/#/vpn_advantage
- Or it's possible to switch to a different provider", + "diagnosis_mail_fcrdns_dns_missing": "No reverse DNS is defined in IPv{ipversion}. Some emails may fail to get delivered or be flagged as spam.", + "diagnosis_mail_fcrdns_nok_alternatives_4": "Some providers won't let you configure your reverse DNS (or their feature might be broken...). If you are experiencing issues because of this, consider the following solutions:
- Some ISP provide the alternative of using a mail server relay though it implies that the relay will be able to spy on your email traffic.
- A privacy-friendly alternative is to use a VPN *with a dedicated public IP* to bypass these kinds of limits. See https://yunohost.org/#/vpn_advantage
- Or it's possible to switch to a different provider", "diagnosis_mail_fcrdns_nok_alternatives_6": "Some providers won't let you configure your reverse DNS (or their feature might be broken...). If your reverse DNS is correctly configured for IPv4, you can try disabling the use of IPv6 when sending emails by running yunohost settings set smtp.allow_ipv6 -v off. Note: this last solution means that you won't be able to send or receive emails from the few IPv6-only servers out there.", - "diagnosis_mail_fcrdns_nok_details": "You should first try to configure the reverse DNS with {ehlo_domain} in your internet router interface or your hosting provider interface. (Some hosting provider may require you to send them a support ticket for this).", + "diagnosis_mail_fcrdns_nok_details": "You should first try to configure reverse DNS with {ehlo_domain} in your internet router interface or your hosting provider interface. (Some hosting providers may require you to send them a support ticket for this).", "diagnosis_mail_fcrdns_ok": "Your reverse DNS is correctly configured!", "diagnosis_mail_outgoing_port_25_blocked": "The SMTP mail server cannot send emails to other servers because outgoing port 25 is blocked in IPv{ipversion}.", - "diagnosis_mail_outgoing_port_25_blocked_details": "You should first try to unblock outgoing port 25 in your internet router interface or your hosting provider interface. (Some hosting provider may require you to send them a support ticket for this).", - "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Some providers won't let you unblock outgoing port 25 because they don't care about Net Neutrality.
- Some of them provide the alternative of using a mail server relay though it implies that the relay will be able to spy on your email traffic.
- A privacy-friendly alternative is to use a VPN *with a dedicated public IP* to bypass this kind of limits. See https://yunohost.org/#/vpn_advantage
- You can also consider switching to a more net neutrality-friendly provider", + "diagnosis_mail_outgoing_port_25_blocked_details": "You should first try to unblock outgoing port 25 in your internet router interface or your hosting provider interface. (Some hosting providers may require you to send them a support ticket for this).", + "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Some providers won't let you unblock outgoing port 25 because they don't care about Net Neutrality.
- Some of them provide the alternative of using a mail server relay though it implies that the relay will be able to spy on your email traffic.
- A privacy-friendly alternative is to use a VPN *with a dedicated public IP* to bypass these kinds of limits. See https://yunohost.org/#/vpn_advantage
- You can also consider switching to a more net neutrality-friendly provider", "diagnosis_mail_outgoing_port_25_ok": "The SMTP mail server is able to send emails (outgoing port 25 is not blocked).", "diagnosis_mail_queue_ok": "{nb_pending} pending emails in the mail queues", "diagnosis_mail_queue_too_big": "Too many pending emails in mail queue ({nb_pending} emails)", @@ -270,20 +270,20 @@ "diagnosis_ports_could_not_diagnose_details": "Error: {error}", "diagnosis_ports_forwarding_tip": "To fix this issue, you most probably need to configure port forwarding on your internet router as described in https://yunohost.org/isp_box_config", "diagnosis_ports_needed_by": "Exposing this port is needed for {category} features (service {service})", - "diagnosis_ports_ok": "Port {port} is reachable from outside.", - "diagnosis_ports_partially_unreachable": "Port {port} is not reachable from outside in IPv{failed}.", - "diagnosis_ports_unreachable": "Port {port} is not reachable from outside.", - "diagnosis_processes_killed_by_oom_reaper": "Some processes were recently killed by the system because it ran out of memory. This is typically symptomatic of a lack of memory on the system or of a process that ate up to much memory. Summary of the processes killed:\n{kills_summary}", + "diagnosis_ports_ok": "Port {port} is reachable from the outside.", + "diagnosis_ports_partially_unreachable": "Port {port} is not reachable from the outside in IPv{failed}.", + "diagnosis_ports_unreachable": "Port {port} is not reachable from the outside.", + "diagnosis_processes_killed_by_oom_reaper": "Some processes were recently killed by the system because it ran out of memory. This is typically symptomatic of a lack of memory on the system or of a process consuming too much memory. Summary of the processes killed:\n{kills_summary}", "diagnosis_ram_low": "The system has {available} ({available_percent}%) RAM available (out of {total}). Be careful.", "diagnosis_ram_ok": "The system still has {available} ({available_percent}%) RAM available out of {total}.", "diagnosis_ram_verylow": "The system has only {available} ({available_percent}%) RAM available! (out of {total})", - "diagnosis_regenconf_allgood": "All configurations files are in line with the recommended configuration!", + "diagnosis_regenconf_allgood": "All configuration files are in line with the recommended configuration!", "diagnosis_regenconf_manually_modified": "Configuration file {file} appears to have been manually modified.", "diagnosis_regenconf_manually_modified_details": "This is probably OK if you know what you're doing! YunoHost will stop updating this file automatically... But beware that YunoHost upgrades could contain important recommended changes. If you want to, you can inspect the differences with yunohost tools regen-conf {category} --dry-run --with-diff and force the reset to the recommended configuration with yunohost tools regen-conf {category} --force", "diagnosis_rootfstotalspace_critical": "The root filesystem only has a total of {space} which is quite worrisome! You will likely run out of disk space very quickly! It's recommended to have at least 16 GB for the root filesystem.", "diagnosis_rootfstotalspace_warning": "The root filesystem only has a total of {space}. This may be okay, but be careful because ultimately you may run out of disk space quickly... It's recommended to have at least 16 GB for the root filesystem.", - "diagnosis_security_vulnerable_to_meltdown": "You appear vulnerable to the Meltdown criticial security vulnerability", - "diagnosis_security_vulnerable_to_meltdown_details": "To fix this, you should upgrade your system and reboot to load the new linux kernel (or contact your server provider if this doesn't work). See https://meltdownattack.com/ for more infos.", + "diagnosis_security_vulnerable_to_meltdown": "You appear vulnerable to the Meltdown critical security vulnerability", + "diagnosis_security_vulnerable_to_meltdown_details": "To fix this, you should upgrade your system and reboot to load the new linux kernel (or contact your server provider if this doesn't work). See https://meltdownattack.com/ for more info.", "diagnosis_services_bad_status": "Service {service} is {status} :(", "diagnosis_services_bad_status_tip": "You can try to restart the service, and if it doesn't work, have a look at the service logs in the webadmin (from the command line, you can do this with yunohost service restart {service} and yunohost service log {service}).", "diagnosis_services_conf_broken": "Configuration is broken for service {service}!", @@ -294,11 +294,11 @@ "diagnosis_swap_none": "The system has no swap at all. You should consider adding at least {recommended} of swap to avoid situations where the system runs out of memory.", "diagnosis_swap_notsomuch": "The system has only {total} swap. You should consider having at least {recommended} to avoid situations where the system runs out of memory.", "diagnosis_swap_ok": "The system has {total} of swap!", - "diagnosis_swap_tip": "Please be careful and aware that if the server is hosting swap on an SD card or SSD storage, it may drastically reduce the life expectancy of the device`.", + "diagnosis_swap_tip": "Please be careful and aware that if the server is hosting swap on an SD card or SSD storage, it may drastically reduce the life expectancy of the device.", "diagnosis_unknown_categories": "The following categories are unknown: {categories}", "disk_space_not_sufficient_install": "There is not enough disk space left to install this application", "disk_space_not_sufficient_update": "There is not enough disk space left to update this application", - "domain_cannot_add_xmpp_upload": "You cannot add domains starting with 'xmpp-upload.'. This kind of name is reserved for the XMPP upload feature integrated in YunoHost.", + "domain_cannot_add_xmpp_upload": "You cannot add domains starting with 'xmpp-upload.'. This kind of name is reserved for the XMPP upload feature integrated into YunoHost.", "domain_cannot_remove_main": "You cannot remove '{domain}' since it's the main domain, you first need to set another domain as the main domain using 'yunohost domain main-domain -n '; here is the list of candidate domains: {other_domains}", "domain_cannot_remove_main_add_new_one": "You cannot remove '{domain}' since it's the main domain and your only domain, you need to first add another domain using 'yunohost domain add ', then set is as the main domain using 'yunohost domain main-domain -n ' and then you can remove the domain '{domain}' using 'yunohost domain remove {domain}'.'", "domain_cert_gen_failed": "Could not generate certificate", @@ -692,4 +692,4 @@ "yunohost_installing": "Installing YunoHost...", "yunohost_not_installed": "YunoHost is not correctly installed. Please run 'yunohost tools postinstall'", "yunohost_postinstall_end_tip": "The post-install completed! To finalize your setup, please consider:\n - adding a first user through the 'Users' section of the webadmin (or 'yunohost user create ' in command-line);\n - diagnose potential issues through the 'Diagnosis' section of the webadmin (or 'yunohost diagnosis run' in command-line);\n - reading the 'Finalizing your setup' and 'Getting to know YunoHost' parts in the admin documentation: https://yunohost.org/admindoc." -} \ No newline at end of file +} From f8b9563ac450d7d89beec9ffe2fbd9700f39bb01 Mon Sep 17 00:00:00 2001 From: Alice Kile Date: Sun, 25 Sep 2022 09:05:18 +0000 Subject: [PATCH 09/30] Translated using Weblate (Telugu) Currently translated at 6.4% (45 of 693 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/te/ --- locales/te.json | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/locales/te.json b/locales/te.json index ca871c2ae..63d7feb25 100644 --- a/locales/te.json +++ b/locales/te.json @@ -14,5 +14,34 @@ "app_action_broke_system": "ఈ చర్య ఈ ముఖ్యమైన సేవలను విచ్ఛిన్నం చేసినట్లుగా కనిపిస్తోంది: {services}", "app_action_cannot_be_ran_because_required_services_down": "ఈ చర్యను అమలు చేయడానికి ఈ అవసరమైన సేవలు అమలు చేయబడాలి: {services}. కొనసాగడం కొరకు వాటిని పునఃప్రారంభించడానికి ప్రయత్నించండి (మరియు అవి ఎందుకు పనిచేయడం లేదో పరిశోధించవచ్చు).", "app_argument_choice_invalid": "ఆర్గ్యుమెంట్ '{name}' కొరకు చెల్లుబాటు అయ్యే వైల్యూ ఎంచుకోండి: '{value}' అనేది లభ్యం అవుతున్న ఎంపికల్లో ({choices}) లేదు", - "app_argument_password_no_default": "పాస్వర్డ్ ఆర్గ్యుమెంట్ '{name}'ని పార్సింగ్ చేసేటప్పుడు దోషం: భద్రతా కారణం కొరకు పాస్వర్డ్ ఆర్గ్యుమెంట్ డిఫాల్ట్ విలువను కలిగి ఉండరాదు" -} \ No newline at end of file + "app_argument_password_no_default": "పాస్వర్డ్ ఆర్గ్యుమెంట్ '{name}'ని పార్సింగ్ చేసేటప్పుడు దోషం: భద్రతా కారణం కొరకు పాస్వర్డ్ ఆర్గ్యుమెంట్ డిఫాల్ట్ విలువను కలిగి ఉండరాదు", + "app_extraction_failed": "ఇన్‌స్టాలేషన్ ఫైల్‌లను సంగ్రహించడం సాధ్యపడలేదు", + "app_id_invalid": "చెల్లని యాప్ ID", + "app_install_failed": "{app}ని ఇన్‌స్టాల్ చేయడం సాధ్యపడలేదు: {error}", + "app_install_script_failed": "యాప్ ఇన్‌స్టాలేషన్ స్క్రిప్ట్‌లో లోపం సంభవించింది", + "app_manifest_install_ask_domain": "ఈ యాప్‌ను ఇన్‌స్టాల్ చేయాల్సిన డొమైన్‌ను ఎంచుకోండి", + "app_manifest_install_ask_password": "ఈ యాప్‌కు అడ్మినిస్ట్రేషన్ పాస్‌వర్డ్‌ను ఎంచుకోండి", + "app_not_installed": "ఇన్‌స్టాల్ చేసిన యాప్‌ల జాబితాలో {app}ని కనుగొనడం సాధ్యపడలేదు: {all apps}", + "app_removed": "{app} అన్‌ఇన్‌స్టాల్ చేయబడింది", + "app_restore_failed": "{app}: {error}ని పునరుద్ధరించడం సాధ్యపడలేదు", + "app_start_backup": "{app} కోసం బ్యాకప్ చేయాల్సిన ఫైల్‌లను సేకరిస్తోంది...", + "app_start_install": "{app}ని ఇన్‌స్టాల్ చేస్తోంది...", + "app_start_restore": "{app}ని పునరుద్ధరిస్తోంది...", + "app_unknown": "తెలియని యాప్", + "app_upgrade_failed": "అప్‌గ్రేడ్ చేయడం సాధ్యపడలేదు {app}: {error}", + "app_manifest_install_ask_admin": "ఈ యాప్ కోసం నిర్వాహక వినియోగదారుని ఎంచుకోండి", + "app_argument_required": "ఆర్గ్యుమెంట్ '{name}' అవసరం", + "app_change_url_success": "{app} URL ఇప్పుడు {domain}{path}", + "app_config_unable_to_apply": "config ప్యానెల్ values దరఖాస్తు చేయడంలో విఫలమయ్యాము.", + "app_install_files_invalid": "ఈ ఫైల్‌లను ఇన్‌స్టాల్ చేయడం సాధ్యం కాదు", + "app_manifest_install_ask_is_public": "అనామక సందర్శకులకు ఈ యాప్ బహిర్గతం కావాలా?", + "app_not_correctly_installed": "{app} తప్పుగా ఇన్‌స్టాల్ చేయబడినట్లుగా ఉంది", + "app_not_properly_removed": "{app} సరిగ్గా తీసివేయబడలేదు", + "app_remove_after_failed_install": "ఇన్‌స్టాలేషన్ విఫలమైనందున యాప్‌ని తీసివేస్తోంది...", + "app_requirements_checking": "{app} కోసం అవసరమైన ప్యాకేజీలను తనిఖీ చేస్తోంది...", + "app_restore_script_failed": "యాప్ పునరుద్ధరణ స్క్రిప్ట్‌లో లోపం సంభవించింది", + "app_sources_fetch_failed": "మూలాధార ఫైల్‌లను పొందడం సాధ్యపడలేదు, URL సరైనదేనా?", + "app_start_remove": "{app}ని తీసివేస్తోంది...", + "app_upgrade_app_name": "ఇప్పుడు {app}ని అప్‌గ్రేడ్ చేస్తోంది...", + "app_config_unable_to_read": "కాన్ఫిగరేషన్ ప్యానెల్ విలువలను చదవడంలో విఫలమైంది." +} From a289e4809d08d7047ee621a4e86c6e5b17f8df0d Mon Sep 17 00:00:00 2001 From: Jose Riha Date: Sun, 25 Sep 2022 20:39:33 +0000 Subject: [PATCH 10/30] Translated using Weblate (Slovak) Currently translated at 34.3% (238 of 693 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/sk/ --- locales/sk.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/locales/sk.json b/locales/sk.json index 18a4bf8bf..939f28836 100644 --- a/locales/sk.json +++ b/locales/sk.json @@ -233,5 +233,8 @@ "diagnosis_ip_no_ipv4": "Na serveri nefunguje spojenie cez protokol IPv4.", "diagnosis_ip_no_ipv6": "Na serveri nefunguje spojenie cez protokol IPv6.", "diagnosis_ip_not_connected_at_all": "Zdá sa, že tento server nie je vôbec pripojený k internetu!?", - "diagnosis_ip_weird_resolvconf": "Zdá sa, že preklad názvov domén funguje, ale podľa všetkého používate vlastný súbor /etc/resolv.conf." -} \ No newline at end of file + "diagnosis_ip_weird_resolvconf": "Zdá sa, že preklad názvov domén funguje, ale podľa všetkého používate vlastný súbor /etc/resolv.conf.", + "root_password_desynchronized": "Heslo pre správu bolo zmenené, ale YunoHost nedokázal túto zmenu premietnuť do hesla používateľa root!", + "main_domain_changed": "Hlavná doména bola zmenená", + "user_updated": "Informácie o používateľovi boli zmenené" +} From b7bea608f6dc268cc2eb6e2aeeca12e6f458ae88 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 29 Sep 2022 00:02:17 +0200 Subject: [PATCH 11/30] Fix edge case where var would get undefined.. --- src/tools.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/tools.py b/src/tools.py index e739c4504..1460dac33 100644 --- a/src/tools.py +++ b/src/tools.py @@ -238,6 +238,9 @@ def tools_postinstall( # If this is a nohost.me/noho.st, actually check for availability if not ignore_dyndns and is_yunohost_dyndns_domain(domain): + + available = None + # Check if the domain is available... try: available = _dyndns_available(domain) From 90e3f3235cc8ac63b18571d230c294e089b9305f Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 7 Feb 2022 18:58:10 +0100 Subject: [PATCH 12/30] configpanels: Quick and dirty POC for config panel actions --- helpers/config | 14 +++++ src/app.py | 144 ++++---------------------------------------- src/utils/config.py | 131 ++++++++++++++++++++++++++++++++++++---- 3 files changed, 146 insertions(+), 143 deletions(-) diff --git a/helpers/config b/helpers/config index c1f8bca32..fce215a30 100644 --- a/helpers/config +++ b/helpers/config @@ -285,6 +285,18 @@ ynh_app_config_apply() { _ynh_app_config_apply } +ynh_app_action_run() { + local runner="run__$1" + # Get value from getter if exists + if type -t "$runner" 2>/dev/null | grep -q '^function$' 2>/dev/null; then + $runner + #ynh_return "result:" + #ynh_return "$(echo "${result}" | sed 's/^/ /g')" + else + ynh_die "No handler defined in app's script for action $1. If you are the maintainer of this app, you should define '$runner'" + fi +} + ynh_app_config_run() { declare -Ag old=() declare -Ag changed=() @@ -309,5 +321,7 @@ ynh_app_config_run() { ynh_app_config_apply ynh_script_progression --message="Configuration of $app completed" --last ;; + *) + ynh_app_action_run $1 esac } diff --git a/src/app.py b/src/app.py index fd70e883e..81557978b 100644 --- a/src/app.py +++ b/src/app.py @@ -1429,90 +1429,16 @@ def app_change_label(app, new_label): def app_action_list(app): - logger.warning(m18n.n("experimental_feature")) - # this will take care of checking if the app is installed - app_info_dict = app_info(app) - - return { - "app": app, - "app_name": app_info_dict["name"], - "actions": _get_app_actions(app), - } + return AppConfigPanel(app).list_actions() @is_unit_operation() -def app_action_run(operation_logger, app, action, args=None): - logger.warning(m18n.n("experimental_feature")) +def app_action_run( + operation_logger, app, action, args=None, args_file=None +): - from yunohost.hook import hook_exec - - # will raise if action doesn't exist - actions = app_action_list(app)["actions"] - actions = {x["id"]: x for x in actions} - - if action not in actions: - available_actions = (", ".join(actions.keys()),) - raise YunohostValidationError( - f"action '{action}' not available for app '{app}', available actions are: {available_actions}", - raw_msg=True, - ) - - operation_logger.start() - - action_declaration = actions[action] - - # Retrieve arguments list for install script - raw_questions = actions[action].get("arguments", {}) - questions = ask_questions_and_parse_answers(raw_questions, prefilled_answers=args) - args = { - question.name: question.value - for question in questions - if question.value is not None - } - - tmp_workdir_for_app = _make_tmp_workdir_for_app(app=app) - - env_dict = _make_environment_for_app_script( - app, args=args, args_prefix="ACTION_", workdir=tmp_workdir_for_app - ) - env_dict["YNH_ACTION"] = action - - _, action_script = tempfile.mkstemp(dir=tmp_workdir_for_app) - - with open(action_script, "w") as script: - script.write(action_declaration["command"]) - - if action_declaration.get("cwd"): - cwd = action_declaration["cwd"].replace("$app", app) - else: - cwd = tmp_workdir_for_app - - try: - retcode = hook_exec( - action_script, - env=env_dict, - chdir=cwd, - user=action_declaration.get("user", "root"), - )[0] - # Calling hook_exec could fail miserably, or get - # manually interrupted (by mistake or because script was stuck) - # In that case we still want to delete the tmp work dir - except (KeyboardInterrupt, EOFError, Exception): - retcode = -1 - import traceback - - logger.error(m18n.n("unexpected_error", error="\n" + traceback.format_exc())) - finally: - shutil.rmtree(tmp_workdir_for_app) - - if retcode not in action_declaration.get("accepted_return_codes", [0]): - msg = f"Error while executing action '{action}' of app '{app}': return code {retcode}" - operation_logger.error(msg) - raise YunohostError(msg, raw_msg=True) - - operation_logger.success() - return logger.success("Action successed!") + return AppConfigPanel(app).run_action(action, args=args, args_file=args_file, operation_logger=operation_logger) def app_config_get(app, key="", full=False, export=False): @@ -1556,6 +1482,10 @@ class AppConfigPanel(ConfigPanel): def _load_current_values(self): self.values = self._call_config_script("show") + def _run_action(self, action): + env = {key: str(value) for key, value in self.new_values.items()} + self._call_config_script(action, env=env) + def _apply(self): env = {key: str(value) for key, value in self.new_values.items()} return_content = self._call_config_script("apply", env=env) @@ -1609,8 +1539,10 @@ ynh_app_config_run $1 if ret != 0: if action == "show": raise YunohostError("app_config_unable_to_read") - else: + elif action == "show": raise YunohostError("app_config_unable_to_apply") + else: + raise YunohostError("app_action_failed", action=action) return values @@ -1619,58 +1551,6 @@ def _get_app_actions(app_id): actions_toml_path = os.path.join(APPS_SETTING_PATH, app_id, "actions.toml") actions_json_path = os.path.join(APPS_SETTING_PATH, app_id, "actions.json") - # sample data to get an idea of what is going on - # this toml extract: - # - - # [restart_service] - # name = "Restart service" - # command = "echo pouet $YNH_ACTION_SERVICE" - # user = "root" # optional - # cwd = "/" # optional - # accepted_return_codes = [0, 1, 2, 3] # optional - # description.en = "a dummy stupid exemple or restarting a service" - # - # [restart_service.arguments.service] - # type = "string", - # ask.en = "service to restart" - # example = "nginx" - # - # will be parsed into this: - # - # OrderedDict([(u'restart_service', - # OrderedDict([(u'name', u'Restart service'), - # (u'command', u'echo pouet $YNH_ACTION_SERVICE'), - # (u'user', u'root'), - # (u'cwd', u'/'), - # (u'accepted_return_codes', [0, 1, 2, 3]), - # (u'description', - # OrderedDict([(u'en', - # u'a dummy stupid exemple or restarting a service')])), - # (u'arguments', - # OrderedDict([(u'service', - # OrderedDict([(u'type', u'string'), - # (u'ask', - # OrderedDict([(u'en', - # u'service to restart')])), - # (u'example', - # u'nginx')]))]))])), - # - # - # and needs to be converted into this: - # - # [{u'accepted_return_codes': [0, 1, 2, 3], - # u'arguments': [{u'ask': {u'en': u'service to restart'}, - # u'example': u'nginx', - # u'name': u'service', - # u'type': u'string'}], - # u'command': u'echo pouet $YNH_ACTION_SERVICE', - # u'cwd': u'/', - # u'description': {u'en': u'a dummy stupid exemple or restarting a service'}, - # u'id': u'restart_service', - # u'name': u'Restart service', - # u'user': u'root'}] - if os.path.exists(actions_toml_path): toml_actions = toml.load(open(actions_toml_path, "r"), _dict=OrderedDict) diff --git a/src/utils/config.py b/src/utils/config.py index ec7faa719..ac317d83c 100644 --- a/src/utils/config.py +++ b/src/utils/config.py @@ -273,6 +273,10 @@ class ConfigPanel: logger.debug(f"Formating result in '{mode}' mode") result = {} for panel, section, option in self._iterate(): + + if section["is_action_section"] and mode != "full": + continue + key = f"{panel['id']}.{section['id']}.{option['id']}" if mode == "export": result[option["id"]] = option.get("current_value") @@ -311,6 +315,82 @@ class ConfigPanel: else: return result + def list_actions(self): + + actions = {} + + # FIXME : meh, loading the entire config panel is again going to cause + # stupid issues for domain (e.g loading registrar stuff when willing to just list available actions ...) + self.filter_key = "" + self._get_config_panel() + for panel, section, option in self._iterate(): + if option["type"] == "button": + key = f"{panel['id']}.{section['id']}.{option['id']}" + actions[key] = _value_for_locale(option["ask"]) + + return actions + + def run_action( + self, action=None, args=None, args_file=None, operation_logger=None + ): + # + # FIXME : this stuff looks a lot like set() ... + # + + self.filter_key = ".".join(action.split(".")[:2]) + action_id = action.split(".")[2] + + # Read config panel toml + self._get_config_panel() + + # FIXME: should also check that there's indeed a key called action + if not self.config: + raise YunohostValidationError("config_no_such_action", action=action) + + # Import and parse pre-answered options + logger.debug("Import and parse pre-answered options") + self._parse_pre_answered(args, None, args_file) + + # Read or get values and hydrate the config + self._load_current_values() + self._hydrate() + Question.operation_logger = operation_logger + self._ask(for_action=True) + + # FIXME: here, we could want to check constrains on + # the action's visibility / requirements wrt to the answer to questions ... + + if operation_logger: + operation_logger.start() + + try: + self._run_action(action_id) + except YunohostError: + raise + # Script got manually interrupted ... + # N.B. : KeyboardInterrupt does not inherit from Exception + except (KeyboardInterrupt, EOFError): + error = m18n.n("operation_interrupted") + logger.error(m18n.n("config_action_failed", action=action, error=error)) + raise + # Something wrong happened in Yunohost's code (most probably hook_exec) + except Exception: + import traceback + + error = m18n.n("unexpected_error", error="\n" + traceback.format_exc()) + logger.error(m18n.n("config_action_failed", action=action, error=error)) + raise + finally: + # Delete files uploaded from API + # FIXME : this is currently done in the context of config panels, + # but could also happen in the context of app install ... (or anywhere else + # where we may parse args etc...) + FileQuestion.clean_upload_dirs() + + # FIXME: i18n + logger.success(f"Action {action_id} successful") + operation_logger.success() + def set( self, key=None, value=None, args=None, args_file=None, operation_logger=None ): @@ -417,6 +497,7 @@ class ConfigPanel: "name": "", "services": [], "optional": True, + "is_action_section": False, }, }, "options": { @@ -485,6 +566,9 @@ class ConfigPanel: elif level == "sections": subnode["name"] = key # legacy subnode.setdefault("optional", raw_infos.get("optional", True)) + # If this section contains at least one button, it becomes an "action" section + if subnode["type"] == "button": + out["is_action_section"] = True out.setdefault(sublevel, []).append(subnode) # Key/value are a property else: @@ -533,10 +617,14 @@ class ConfigPanel: def _hydrate(self): # Hydrating config panel with current value - for _, _, option in self._iterate(): + for _, section, option in self._iterate(): if option["id"] not in self.values: - allowed_empty_types = ["alert", "display_text", "markdown", "file"] - if ( + + allowed_empty_types = ["alert", "display_text", "markdown", "file", "button"] + + if section["is_action_section"] and option.get("default") is not None: + self.values[option["id"]] = option["default"] + elif ( option["type"] in allowed_empty_types or option.get("bind") == "null" ): @@ -554,7 +642,7 @@ class ConfigPanel: return self.values - def _ask(self): + def _ask(self, for_action=False): logger.debug("Ask unanswered question and prevalidate data") if "i18n" in self.config: @@ -568,13 +656,22 @@ class ConfigPanel: Moulinette.display(colorize(message, "purple")) for panel, section, obj in self._iterate(["panel", "section"]): + + # Ugly hack to skip action section ... except when when explicitly running actions + if not for_action: + if section and section["is_action_section"]: + continue + + if panel == obj: + name = _value_for_locale(panel["name"]) + display_header(f"\n{'='*40}\n>>>> {name}\n{'='*40}") + else: + name = _value_for_locale(section["name"]) + if name: + display_header(f"\n# {name}") + if panel == obj: - name = _value_for_locale(panel["name"]) - display_header(f"\n{'='*40}\n>>>> {name}\n{'='*40}") continue - name = _value_for_locale(section["name"]) - if name: - display_header(f"\n# {name}") # Check and ask unanswered questions prefilled_answers = self.args.copy() @@ -594,8 +691,6 @@ class ConfigPanel: } ) - self.errors = None - def _get_default_values(self): return { option["id"]: option["default"] @@ -1334,6 +1429,17 @@ class FileQuestion(Question): return self.value +class ButtonQuestion(Question): + argument_type = "button" + + #def __init__( + # self, question, context: Mapping[str, Any] = {}, hooks: Dict[str, Callable] = {} + #): + # super().__init__(question, context, hooks) + + + + ARGUMENTS_TYPE_PARSERS = { "string": StringQuestion, "text": StringQuestion, @@ -1356,6 +1462,7 @@ ARGUMENTS_TYPE_PARSERS = { "markdown": DisplayTextQuestion, "file": FileQuestion, "app": AppQuestion, + "button": ButtonQuestion, } @@ -1395,6 +1502,8 @@ def ask_questions_and_parse_answers( for raw_question in raw_questions: question_class = ARGUMENTS_TYPE_PARSERS[raw_question.get("type", "string")] + if question_class.argument_type == "button": + continue raw_question["value"] = answers.get(raw_question["name"]) question = question_class(raw_question, context=context, hooks=hooks) new_values = question.ask_if_needed() From 47543b19b764bf085b91ccdd2d721f6abffd986c Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 8 Feb 2022 01:39:29 +0100 Subject: [PATCH 13/30] configpanels: Iterating on action POC to create a certificat section in domain config panels --- conf/nginx/server.tpl.conf | 8 ++-- share/config_domain.toml | 68 ++++++++++++++++++++++++++++++--- src/certificate.py | 77 +++++++++++--------------------------- src/domain.py | 28 ++++++++++++++ 4 files changed, 116 insertions(+), 65 deletions(-) diff --git a/conf/nginx/server.tpl.conf b/conf/nginx/server.tpl.conf index 379b597a7..4ee20a720 100644 --- a/conf/nginx/server.tpl.conf +++ b/conf/nginx/server.tpl.conf @@ -44,10 +44,10 @@ server { ssl_certificate /etc/yunohost/certs/{{ domain }}/crt.pem; ssl_certificate_key /etc/yunohost/certs/{{ domain }}/key.pem; - {% if domain_cert_ca != "Self-signed" %} + {% if domain_cert_ca != "selfsigned" %} more_set_headers "Strict-Transport-Security : max-age=63072000; includeSubDomains; preload"; {% endif %} - {% if domain_cert_ca == "Let's Encrypt" %} + {% if domain_cert_ca == "letsencrypt" %} # OCSP settings ssl_stapling on; ssl_stapling_verify on; @@ -99,10 +99,10 @@ server { ssl_certificate /etc/yunohost/certs/{{ domain }}/crt.pem; ssl_certificate_key /etc/yunohost/certs/{{ domain }}/key.pem; - {% if domain_cert_ca != "Self-signed" %} + {% if domain_cert_ca != "selfsigned" %} more_set_headers "Strict-Transport-Security : max-age=63072000; includeSubDomains; preload"; {% endif %} - {% if domain_cert_ca == "Let's Encrypt" %} + {% if domain_cert_ca == "letsencrypt" %} # OCSP settings ssl_stapling on; ssl_stapling_verify on; diff --git a/share/config_domain.toml b/share/config_domain.toml index 65e755365..e7f43f84d 100644 --- a/share/config_domain.toml +++ b/share/config_domain.toml @@ -16,7 +16,7 @@ i18n = "domain_config" type = "app" filter = "is_webapp" default = "_none" - + [feature.mail] #services = ['postfix', 'dovecot'] @@ -28,17 +28,17 @@ i18n = "domain_config" [feature.mail.mail_out] type = "boolean" default = 1 - + [feature.mail.mail_in] type = "boolean" default = 1 - + #[feature.mail.backup_mx] #type = "tags" #default = [] #pattern.regexp = '^([^\W_A-Z]+([-]*[^\W_A-Z]+)*\.)+((xn--)?[^\W_]{2,})$' #pattern.error = "pattern_error" - + [feature.xmpp] [feature.xmpp.xmpp] @@ -46,7 +46,7 @@ i18n = "domain_config" default = 0 [dns] - + [dns.registrar] optional = true @@ -58,3 +58,61 @@ i18n = "domain_config" # type = "number" # min = 0 # default = 3600 + + +[cert] + + [cert.status] + + [cert.status.cert_summary] + type = "alert" + # Automatically filled by DomainConfigPanel + + [cert.status.cert_validity] + type = "number" + readonly = true + # Automatically filled by DomainConfigPanel + + [cert.cert] + + [cert.cert.cert_issuer] + type = "string" + readonly = true + visible = "false" + # Automatically filled by DomainConfigPanel + + [cert.cert.acme_eligible] + type = "boolean" + readonly = true + visible = "false" + # Automatically filled by DomainConfigPanel + + [cert.cert.acme_eligible_explain] + type = "alert" + visible = "acme_eligible == false" + # FIXME: improve messaging ... + ask = "Uhoh, domain isnt ready for ACME challenge according to the diagnosis" + + [cert.cert.cert_no_checks] + ask = "Ignore diagnosis checks" + type = "boolean" + default = false + visible = "acme_eligible == false" + + [cert.cert.cert_install] + ask = "Install Let's Encrypt certificate" + type = "button" + icon = "star" + style = "success" + visible = "issuer != 'letsencrypt'" + enabled = "acme_eligible or cert_no_checks" + # ??? api = "PUT /domains/{domain}/cert?force&" + + [cert.cert.cert_renew] + ask = "Renew Let's Encrypt certificate" + help = "The certificate should be automatically renewed by YunoHost a few days before it expires." + type = "button" + icon = "refresh" + style = "warning" + visible = "issuer == 'letsencrypt'" + enabled = "acme_eligible or cert_no_checks" diff --git a/src/certificate.py b/src/certificate.py index 2a9fb4ce9..137a0aba0 100644 --- a/src/certificate.py +++ b/src/certificate.py @@ -95,8 +95,6 @@ def certificate_status(domains, full=False): if not full: del status["subject"] del status["CA_name"] - status["CA_type"] = status["CA_type"]["verbose"] - status["summary"] = status["summary"]["verbose"] if full: try: @@ -154,7 +152,7 @@ def _certificate_install_selfsigned(domain_list, force=False): if not force and os.path.isfile(current_cert_file): status = _get_status(domain) - if status["summary"]["code"] in ("good", "great"): + if status["summary"] == "success": raise YunohostValidationError( "certmanager_attempt_to_replace_valid_cert", domain=domain ) @@ -216,7 +214,7 @@ def _certificate_install_selfsigned(domain_list, force=False): if ( status - and status["CA_type"]["code"] == "self-signed" + and status["CA_type"] == "selfsigned" and status["validity"] > 3648 ): logger.success( @@ -241,7 +239,7 @@ def _certificate_install_letsencrypt(domains, force=False, no_checks=False): for domain in domain_list()["domains"]: status = _get_status(domain) - if status["CA_type"]["code"] != "self-signed": + if status["CA_type"] != "selfsigned": continue domains.append(domain) @@ -253,7 +251,7 @@ def _certificate_install_letsencrypt(domains, force=False, no_checks=False): # Is it self-signed? status = _get_status(domain) - if not force and status["CA_type"]["code"] != "self-signed": + if not force and status["CA_type"] != "selfsigned": raise YunohostValidationError( "certmanager_domain_cert_not_selfsigned", domain=domain ) @@ -314,7 +312,7 @@ def certificate_renew(domains, force=False, no_checks=False, email=False): # Does it have a Let's Encrypt cert? status = _get_status(domain) - if status["CA_type"]["code"] != "lets-encrypt": + if status["CA_type"] != "letsencrypt": continue # Does it expire soon? @@ -349,7 +347,7 @@ def certificate_renew(domains, force=False, no_checks=False, email=False): ) # Does it have a Let's Encrypt cert? - if status["CA_type"]["code"] != "lets-encrypt": + if status["CA_type"] != "letsencrypt": raise YunohostValidationError( "certmanager_attempt_to_renew_nonLE_cert", domain=domain ) @@ -539,7 +537,7 @@ def _fetch_and_enable_new_certificate(domain, no_checks=False): # Check the status of the certificate is now good status_summary = _get_status(domain)["summary"] - if status_summary["code"] != "great": + if status_summary != "success": raise YunohostError( "certmanager_certificate_fetching_or_enabling_failed", domain=domain ) @@ -634,58 +632,25 @@ def _get_status(domain): days_remaining = (valid_up_to - datetime.utcnow()).days if cert_issuer in ["yunohost.org"] + yunohost.domain.domain_list()["domains"]: - CA_type = { - "code": "self-signed", - "verbose": "Self-signed", - } - + CA_type = "selfsigned" elif organization_name == "Let's Encrypt": - CA_type = { - "code": "lets-encrypt", - "verbose": "Let's Encrypt", - } - + CA_type = "letsencrypt" else: - CA_type = { - "code": "other-unknown", - "verbose": "Other / Unknown", - } + CA_type = "other" if days_remaining <= 0: - status_summary = { - "code": "critical", - "verbose": "CRITICAL", - } - - elif CA_type["code"] in ("self-signed", "fake-lets-encrypt"): - status_summary = { - "code": "warning", - "verbose": "WARNING", - } - + summary = "danger" + elif CA_type == "selfsigned": + summary = "warning" elif days_remaining < VALIDITY_LIMIT: - status_summary = { - "code": "attention", - "verbose": "About to expire", - } - - elif CA_type["code"] == "other-unknown": - status_summary = { - "code": "good", - "verbose": "Good", - } - - elif CA_type["code"] == "lets-encrypt": - status_summary = { - "code": "great", - "verbose": "Great!", - } - + summary = "warning" + elif CA_type == "other": + summary = "success" + elif CA_type == "letsencrypt": + summary = "success" else: - status_summary = { - "code": "unknown", - "verbose": "Unknown?", - } + # shouldnt happen, because CA_type can be only selfsigned, letsencrypt, or other + summary = "" return { "domain": domain, @@ -693,7 +658,7 @@ def _get_status(domain): "CA_name": cert_issuer, "CA_type": CA_type, "validity": days_remaining, - "summary": status_summary, + "summary": summary, } diff --git a/src/domain.py b/src/domain.py index 29040ced8..4456d972c 100644 --- a/src/domain.py +++ b/src/domain.py @@ -499,6 +499,19 @@ class DomainConfigPanel(ConfigPanel): self.registar_id = toml["dns"]["registrar"]["registrar"]["value"] del toml["dns"]["registrar"]["registrar"]["value"] + # Cert stuff + if not filter_key or filter_key[0] == "cert": + + from yunohost.certificate import certificate_status + status = certificate_status([self.entity], full=True)["certificates"][self.entity] + + toml["cert"]["status"]["cert_summary"]["style"] = status["summary"] + # FIXME: improve message + toml["cert"]["status"]["cert_summary"]["ask"] = f"Status is {status['summary']} ! (FIXME: improve message depending on summary / issuer / validity ..." + + # FIXME: Ugly hack to save the cert status and reinject it in _load_current_values ... + self.cert_status = status + return toml def _load_current_values(self): @@ -511,6 +524,21 @@ class DomainConfigPanel(ConfigPanel): if not filter_key or filter_key[0] == "dns": self.values["registrar"] = self.registar_id + # FIXME: Ugly hack to save the cert status and reinject it in _load_current_values ... + if not filter_key or filter_key[0] == "cert": + self.values["cert_validity"] = self.cert_status["validity"] + self.values["cert_issuer"] = self.cert_status["CA_type"] + self.values["acme_eligible"] = self.cert_status["ACME_eligible"] + + def _run_action(self, action): + + if action == "cert_install": + from yunohost.certificate import certificate_install as action_func + elif action == "cert_renew": + from yunohost.certificate import certificate_renew as action_func + + action_func([self.entity], force=True, no_checks=self.new_values["cert_no_checks"]) + def _get_domain_settings(domain: str) -> dict: From c39f0ae3bc5d7d68b3f944dae947e61885be21cd Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 8 Feb 2022 14:45:36 +0100 Subject: [PATCH 14/30] actionsmap: hide a bunch of technical commands from --help --- share/actionsmap.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/share/actionsmap.yml b/share/actionsmap.yml index 89c6e914d..86ef3848a 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -552,6 +552,7 @@ domain: ### domain_url_available() url-available: + hide_in_help: True action_help: Check availability of a web path api: GET /domain//urlavailable arguments: @@ -868,6 +869,7 @@ app: ### app_register_url() register-url: + hide_in_help: True action_help: Book/register a web path for a given app arguments: app: @@ -880,6 +882,7 @@ app: ### app_makedefault() makedefault: + hide_in_help: True action_help: Redirect domain root to an app api: PUT /apps//default arguments: @@ -1065,6 +1068,7 @@ backup: ### backup_download() download: + hide_in_help: True action_help: (API only) Request to download the file api: GET /backups//download arguments: @@ -1651,6 +1655,7 @@ hook: ### hook_info() info: + hide_in_help: True action_help: Get information about a given hook arguments: action: @@ -1680,6 +1685,7 @@ hook: ### hook_callback() callback: + hide_in_help: True action_help: Execute all scripts binded to an action arguments: action: @@ -1702,6 +1708,7 @@ hook: ### hook_exec() exec: + hide_in_help: True action_help: Execute hook from a file with arguments arguments: path: From 40ad8ce25e5459a0b7ebc28a901692918d25fec7 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 8 Feb 2022 16:07:35 +0100 Subject: [PATCH 15/30] configpanel: Implement 'hidden' domain_action_run route --- share/actionsmap.yml | 14 ++++++++++++++ share/config_domain.toml | 1 - src/domain.py | 21 +++++++++++++++------ 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/share/actionsmap.yml b/share/actionsmap.yml index 86ef3848a..969a2e07c 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -563,6 +563,20 @@ domain: path: help: The path to check (e.g. /coffee) + ### domain_action_run() + action-run: + hide_in_help: True + action_help: Run domain action + api: PUT /domain//actions/ + arguments: + domain: + help: Domain name + action: + help: action id + -a: + full: --args + help: Serialized arguments for action (i.e. "foo=bar&lorem=ipsum") + subcategories: config: diff --git a/share/config_domain.toml b/share/config_domain.toml index e7f43f84d..14bb72a67 100644 --- a/share/config_domain.toml +++ b/share/config_domain.toml @@ -106,7 +106,6 @@ i18n = "domain_config" style = "success" visible = "issuer != 'letsencrypt'" enabled = "acme_eligible or cert_no_checks" - # ??? api = "PUT /domains/{domain}/cert?force&" [cert.cert.cert_renew] ask = "Renew Let's Encrypt certificate" diff --git a/src/domain.py b/src/domain.py index 4456d972c..45a516f7d 100644 --- a/src/domain.py +++ b/src/domain.py @@ -530,14 +530,23 @@ class DomainConfigPanel(ConfigPanel): self.values["cert_issuer"] = self.cert_status["CA_type"] self.values["acme_eligible"] = self.cert_status["ACME_eligible"] - def _run_action(self, action): - if action == "cert_install": - from yunohost.certificate import certificate_install as action_func - elif action == "cert_renew": - from yunohost.certificate import certificate_renew as action_func +@is_unit_operation() +def domain_action_run( + operation_logger, domain, action, args=None +): - action_func([self.entity], force=True, no_checks=self.new_values["cert_no_checks"]) + import urllib.parse + + if action == "cert.cert.cert_install": + from yunohost.certificate import certificate_install as action_func + elif action == "cert.cert.cert_renew": + from yunohost.certificate import certificate_renew as action_func + + args = dict(urllib.parse.parse_qsl(args or "", keep_blank_values=True)) + no_checks = bool(args["cert_no_checks"]) + + action_func([domain], force=True, no_checks=no_checks) def _get_domain_settings(domain: str) -> dict: From 0dc08c8f8c1f0c6ed8cb6251810a0979eaf188f3 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 9 Feb 2022 20:08:39 +0100 Subject: [PATCH 16/30] Apply suggestions from code review Co-authored-by: Axolotle --- share/config_domain.toml | 2 ++ src/domain.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/share/config_domain.toml b/share/config_domain.toml index 14bb72a67..84201b845 100644 --- a/share/config_domain.toml +++ b/share/config_domain.toml @@ -71,6 +71,7 @@ i18n = "domain_config" [cert.status.cert_validity] type = "number" readonly = true + visible = "false" # Automatically filled by DomainConfigPanel [cert.cert] @@ -89,6 +90,7 @@ i18n = "domain_config" [cert.cert.acme_eligible_explain] type = "alert" + style = "warning" visible = "acme_eligible == false" # FIXME: improve messaging ... ask = "Uhoh, domain isnt ready for ACME challenge according to the diagnosis" diff --git a/src/domain.py b/src/domain.py index 45a516f7d..50a6451bf 100644 --- a/src/domain.py +++ b/src/domain.py @@ -544,7 +544,7 @@ def domain_action_run( from yunohost.certificate import certificate_renew as action_func args = dict(urllib.parse.parse_qsl(args or "", keep_blank_values=True)) - no_checks = bool(args["cert_no_checks"]) + no_checks = args["cert_no_checks"] in ("y", "yes", "on", "1") action_func([domain], force=True, no_checks=no_checks) From 0838d443a1d49826c34496d11d3cfe9a410ba222 Mon Sep 17 00:00:00 2001 From: axolotle Date: Wed, 2 Mar 2022 20:43:26 +0100 Subject: [PATCH 17/30] normalize actionmap config API routes --- share/actionsmap.yml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/share/actionsmap.yml b/share/actionsmap.yml index 969a2e07c..9d5f8866e 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -586,7 +586,9 @@ domain: ### domain_config_get() get: action_help: Display a domain configuration - api: GET /domains//config + api: + - GET /domains//config + - GET /domains//config/ arguments: domain: help: Domain name @@ -605,7 +607,7 @@ domain: ### domain_config_set() set: action_help: Apply a new configuration - api: PUT /domains//config + api: PUT /domains//config/ arguments: domain: help: Domain name @@ -958,7 +960,9 @@ app: ### app_config_get() get: action_help: Display an app configuration - api: GET /apps//config-panel + api: + - GET /apps//config + - GET /apps//config/ arguments: app: help: App name @@ -977,7 +981,7 @@ app: ### app_config_set() set: action_help: Apply a new configuration - api: PUT /apps//config + api: PUT /apps//config/ arguments: app: help: App name From 3644937fffbd2a8ff73f7141329e04403d0eabd7 Mon Sep 17 00:00:00 2001 From: axolotle Date: Wed, 2 Mar 2022 20:46:51 +0100 Subject: [PATCH 18/30] fix config domain syntax --- share/config_domain.toml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/share/config_domain.toml b/share/config_domain.toml index 84201b845..7189003d3 100644 --- a/share/config_domain.toml +++ b/share/config_domain.toml @@ -63,6 +63,7 @@ i18n = "domain_config" [cert] [cert.status] + name = "Status" [cert.status.cert_summary] type = "alert" @@ -71,21 +72,19 @@ i18n = "domain_config" [cert.status.cert_validity] type = "number" readonly = true - visible = "false" # Automatically filled by DomainConfigPanel [cert.cert] + name = "Manage" [cert.cert.cert_issuer] type = "string" - readonly = true - visible = "false" + visible = false # Automatically filled by DomainConfigPanel [cert.cert.acme_eligible] type = "boolean" - readonly = true - visible = "false" + visible = false # Automatically filled by DomainConfigPanel [cert.cert.acme_eligible_explain] @@ -107,7 +106,8 @@ i18n = "domain_config" icon = "star" style = "success" visible = "issuer != 'letsencrypt'" - enabled = "acme_eligible or cert_no_checks" + enabled = "acme_eligible || cert_no_checks" + args = ["cert_no_checks"] [cert.cert.cert_renew] ask = "Renew Let's Encrypt certificate" @@ -116,4 +116,4 @@ i18n = "domain_config" icon = "refresh" style = "warning" visible = "issuer == 'letsencrypt'" - enabled = "acme_eligible or cert_no_checks" + enabled = "acme_eligible || cert_no_checks" From 35bac35bb08f933b88b0dc112411f0a727afa8f4 Mon Sep 17 00:00:00 2001 From: axolotle Date: Sat, 1 Oct 2022 20:18:32 +0200 Subject: [PATCH 19/30] add readonly prop for config panel arg to display a value --- locales/en.json | 1 + src/utils/config.py | 35 +++++++++++++++++++++++++++++++---- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/locales/en.json b/locales/en.json index 735cf4c15..86a7ae770 100644 --- a/locales/en.json +++ b/locales/en.json @@ -142,6 +142,7 @@ "config_apply_failed": "Applying the new configuration failed: {error}", "config_cant_set_value_on_section": "You can't set a single value on an entire config section.", "config_forbidden_keyword": "The keyword '{keyword}' is reserved, you can't create or use a config panel with a question with this id.", + "config_forbidden_readonly_type": "The type '{type}' can't be set as readonly, use another type to render this value (relevant arg id: '{id}').", "config_no_panel": "No config panel found.", "config_unknown_filter_key": "The filter key '{filter_key}' is incorrect.", "config_validate_color": "Should be a valid RGB hexadecimal color", diff --git a/src/utils/config.py b/src/utils/config.py index ac317d83c..c03d6cfa8 100644 --- a/src/utils/config.py +++ b/src/utils/config.py @@ -524,6 +524,7 @@ class ConfigPanel: "accept", "redact", "filter", + "readonly", ], "defaults": {}, }, @@ -609,10 +610,27 @@ class ConfigPanel: "max_progression", ] forbidden_keywords += format_description["sections"] + forbidden_readonly_types = [ + "password", + "app", + "domain", + "user", + "file" + ] for _, _, option in self._iterate(): if option["id"] in forbidden_keywords: raise YunohostError("config_forbidden_keyword", keyword=option["id"]) + if ( + option.get("readonly", False) and + option.get("type", "string") in forbidden_readonly_types + ): + raise YunohostError( + "config_forbidden_readonly_type", + type=option["type"], + id=option["id"] + ) + return self.config def _hydrate(self): @@ -797,6 +815,7 @@ class Question: self.default = question.get("default", None) self.optional = question.get("optional", False) self.visible = question.get("visible", None) + self.readonly = question.get("readonly", False) # Don't restrict choices if there's none specified self.choices = question.get("choices", None) self.pattern = question.get("pattern", self.pattern) @@ -857,8 +876,9 @@ class Question: # Display question if no value filled or if it's a readonly message if Moulinette.interface.type == "cli" and os.isatty(1): text_for_user_input_in_cli = self._format_text_for_user_input_in_cli() - if getattr(self, "readonly", False): + if self.readonly: Moulinette.display(text_for_user_input_in_cli) + return {} elif self.value is None: self._prompt(text_for_user_input_in_cli) @@ -918,7 +938,12 @@ class Question: text_for_user_input_in_cli = _value_for_locale(self.ask) - if self.choices: + if self.readonly: + text_for_user_input_in_cli = colorize(text_for_user_input_in_cli, "purple") + if self.choices: + return text_for_user_input_in_cli + f" {self.choices[self.current_value]}" + return text_for_user_input_in_cli + f" {self.humanize(self.current_value)}" + elif self.choices: # Prevent displaying a shitload of choices # (e.g. 100+ available users when choosing an app admin...) @@ -1018,6 +1043,7 @@ class ColorQuestion(StringQuestion): class TagsQuestion(Question): argument_type = "tags" + default_value = "" @staticmethod def humanize(value, option={}): @@ -1189,7 +1215,8 @@ class BooleanQuestion(Question): def _format_text_for_user_input_in_cli(self): text_for_user_input_in_cli = super()._format_text_for_user_input_in_cli() - text_for_user_input_in_cli += " [yes | no]" + if not self.readonly: + text_for_user_input_in_cli += " [yes | no]" return text_for_user_input_in_cli @@ -1342,7 +1369,6 @@ class NumberQuestion(Question): class DisplayTextQuestion(Question): argument_type = "display_text" - readonly = True def __init__( self, question, context: Mapping[str, Any] = {}, hooks: Dict[str, Callable] = {} @@ -1350,6 +1376,7 @@ class DisplayTextQuestion(Question): super().__init__(question, context, hooks) self.optional = True + self.readonly = True self.style = question.get( "style", "info" if question["type"] == "alert" else "" ) From 9a3d65c3132d03f9e558b3ea722d88aefb3a2dd2 Mon Sep 17 00:00:00 2001 From: axolotle Date: Sat, 1 Oct 2022 20:19:51 +0200 Subject: [PATCH 20/30] update arg 'time' validation regex to allow 24 hours format --- src/utils/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/config.py b/src/utils/config.py index c03d6cfa8..0ab5fc2ba 100644 --- a/src/utils/config.py +++ b/src/utils/config.py @@ -1029,7 +1029,7 @@ class DateQuestion(StringQuestion): class TimeQuestion(StringQuestion): pattern = { - "regexp": r"^(1[12]|0?\d):[0-5]\d$", + "regexp": r"^(?:\d|[01]\d|2[0-3]):[0-5]\d$", "error": "config_validate_time", # i18n: config_validate_time } From 3d4909bbf51e8c4efbb1b14e801c6c99dc338f40 Mon Sep 17 00:00:00 2001 From: axolotle Date: Sun, 2 Oct 2022 17:10:05 +0200 Subject: [PATCH 21/30] configpanel: misc fix + add section visible evaluation --- share/config_domain.toml | 1 - src/app.py | 2 +- src/utils/config.py | 16 ++++++++-------- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/share/config_domain.toml b/share/config_domain.toml index 7189003d3..fd12d4506 100644 --- a/share/config_domain.toml +++ b/share/config_domain.toml @@ -107,7 +107,6 @@ i18n = "domain_config" style = "success" visible = "issuer != 'letsencrypt'" enabled = "acme_eligible || cert_no_checks" - args = ["cert_no_checks"] [cert.cert.cert_renew] ask = "Renew Let's Encrypt certificate" diff --git a/src/app.py b/src/app.py index 81557978b..c57cf038e 100644 --- a/src/app.py +++ b/src/app.py @@ -1539,7 +1539,7 @@ ynh_app_config_run $1 if ret != 0: if action == "show": raise YunohostError("app_config_unable_to_read") - elif action == "show": + elif action == "apply": raise YunohostError("app_config_unable_to_apply") else: raise YunohostError("app_action_failed", action=action) diff --git a/src/utils/config.py b/src/utils/config.py index 0ab5fc2ba..4291e133e 100644 --- a/src/utils/config.py +++ b/src/utils/config.py @@ -49,6 +49,7 @@ from yunohost.log import OperationLogger logger = getActionLogger("yunohost.config") CONFIG_PANEL_VERSION_SUPPORTED = 1.0 + # Those js-like evaluate functions are used to eval safely visible attributes # The goal is to evaluate in the same way than js simple-evaluate # https://github.com/shepherdwind/simple-evaluate @@ -675,6 +676,11 @@ class ConfigPanel: for panel, section, obj in self._iterate(["panel", "section"]): + if section and section.get("visible") and not evaluate_simple_js_expression( + section["visible"], context=self.new_values + ): + continue + # Ugly hack to skip action section ... except when when explicitly running actions if not for_action: if section and section["is_action_section"]: @@ -878,7 +884,8 @@ class Question: text_for_user_input_in_cli = self._format_text_for_user_input_in_cli() if self.readonly: Moulinette.display(text_for_user_input_in_cli) - return {} + self.value = self.values[self.name] = self.current_value + return self.values elif self.value is None: self._prompt(text_for_user_input_in_cli) @@ -1459,13 +1466,6 @@ class FileQuestion(Question): class ButtonQuestion(Question): argument_type = "button" - #def __init__( - # self, question, context: Mapping[str, Any] = {}, hooks: Dict[str, Callable] = {} - #): - # super().__init__(question, context, hooks) - - - ARGUMENTS_TYPE_PARSERS = { "string": StringQuestion, From f1003939a9b25045bf176201cc62638a877b778c Mon Sep 17 00:00:00 2001 From: axolotle Date: Mon, 3 Oct 2022 16:13:30 +0200 Subject: [PATCH 22/30] configpanel: add 'enabled' prop evaluation for button --- locales/en.json | 1 + src/utils/config.py | 35 ++++++++++++++++++++++++++++++----- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/locales/en.json b/locales/en.json index 86a7ae770..0a1203b61 100644 --- a/locales/en.json +++ b/locales/en.json @@ -139,6 +139,7 @@ "certmanager_self_ca_conf_file_not_found": "Could not find configuration file for self-signing authority (file: {file})", "certmanager_unable_to_parse_self_CA_name": "Could not parse name of self-signing authority (file: {file})", "certmanager_warning_subdomain_dns_record": "Subdomain '{subdomain}' does not resolve to the same IP address as '{domain}'. Some features will not be available until you fix this and regenerate the certificate.", + "config_action_disabled": "Could not run action '{action}' since it is disabled, make sure to meet its constraints. help: {help}", "config_apply_failed": "Applying the new configuration failed: {error}", "config_cant_set_value_on_section": "You can't set a single value on an entire config section.", "config_forbidden_keyword": "The keyword '{keyword}' is reserved, you can't create or use a config panel with a question with this id.", diff --git a/src/utils/config.py b/src/utils/config.py index 4291e133e..7411b79de 100644 --- a/src/utils/config.py +++ b/src/utils/config.py @@ -356,7 +356,7 @@ class ConfigPanel: self._load_current_values() self._hydrate() Question.operation_logger = operation_logger - self._ask(for_action=True) + self._ask(action=action_id) # FIXME: here, we could want to check constrains on # the action's visibility / requirements wrt to the answer to questions ... @@ -526,6 +526,7 @@ class ConfigPanel: "redact", "filter", "readonly", + "enabled", ], "defaults": {}, }, @@ -661,7 +662,7 @@ class ConfigPanel: return self.values - def _ask(self, for_action=False): + def _ask(self, action=None): logger.debug("Ask unanswered question and prevalidate data") if "i18n" in self.config: @@ -682,7 +683,7 @@ class ConfigPanel: continue # Ugly hack to skip action section ... except when when explicitly running actions - if not for_action: + if not action: if section and section["is_action_section"]: continue @@ -693,6 +694,12 @@ class ConfigPanel: name = _value_for_locale(section["name"]) if name: display_header(f"\n# {name}") + elif section: + # filter action section options in case of multiple buttons + section["options"] = [ + option for option in section["options"] + if option.get("type", "string") != "button" or option["id"] == action + ] if panel == obj: continue @@ -1465,6 +1472,13 @@ class FileQuestion(Question): class ButtonQuestion(Question): argument_type = "button" + enabled = None + + def __init__( + self, question, context: Mapping[str, Any] = {}, hooks: Dict[str, Callable] = {} + ): + super().__init__(question, context, hooks) + self.enabled = question.get("enabled", None) ARGUMENTS_TYPE_PARSERS = { @@ -1529,10 +1543,21 @@ def ask_questions_and_parse_answers( for raw_question in raw_questions: question_class = ARGUMENTS_TYPE_PARSERS[raw_question.get("type", "string")] - if question_class.argument_type == "button": - continue raw_question["value"] = answers.get(raw_question["name"]) question = question_class(raw_question, context=context, hooks=hooks) + if question.type == "button": + if ( + not question.enabled + or evaluate_simple_js_expression(question.enabled, context=context) + ): + continue + else: + raise YunohostValidationError( + "config_action_disabled", + action=question.name, + help=_value_for_locale(question.help) + ) + new_values = question.ask_if_needed() answers.update(new_values) context.update(new_values) From aad576fdd0ed5dd1f18478c08b9c9c47dc836115 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 3 Oct 2022 17:12:53 +0200 Subject: [PATCH 23/30] =?UTF-8?q?mypy=20won't=20guess=20that=20'question'?= =?UTF-8?q?=20does=20have=20an=20'enabled'=20attr=20in=20that=20context=20?= =?UTF-8?q?=C3=A9=5F=C3=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/config.py b/src/utils/config.py index 7411b79de..36f7d986d 100644 --- a/src/utils/config.py +++ b/src/utils/config.py @@ -1547,8 +1547,8 @@ def ask_questions_and_parse_answers( question = question_class(raw_question, context=context, hooks=hooks) if question.type == "button": if ( - not question.enabled - or evaluate_simple_js_expression(question.enabled, context=context) + not question.enabled # type: ignore + or evaluate_simple_js_expression(question.enabled, context=context) # type: ignore ): continue else: From 85b6d8554d2ce3fc545a20e1d109f0e061b59149 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 4 Oct 2022 13:04:30 +0200 Subject: [PATCH 24/30] Fix i18n issues / also we don't need operation logger for domain_action_run, already handled in subcalls --- locales/en.json | 2 +- src/domain.py | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/locales/en.json b/locales/en.json index 0a1203b61..81be8da6c 100644 --- a/locales/en.json +++ b/locales/en.json @@ -139,6 +139,7 @@ "certmanager_self_ca_conf_file_not_found": "Could not find configuration file for self-signing authority (file: {file})", "certmanager_unable_to_parse_self_CA_name": "Could not parse name of self-signing authority (file: {file})", "certmanager_warning_subdomain_dns_record": "Subdomain '{subdomain}' does not resolve to the same IP address as '{domain}'. Some features will not be available until you fix this and regenerate the certificate.", + "config_action_failed": "Failed to run action '{action}': {error}", "config_action_disabled": "Could not run action '{action}' since it is disabled, make sure to meet its constraints. help: {help}", "config_apply_failed": "Applying the new configuration failed: {error}", "config_cant_set_value_on_section": "You can't set a single value on an entire config section.", @@ -362,7 +363,6 @@ "dyndns_registered": "DynDNS domain registered", "dyndns_registration_failed": "Could not register DynDNS domain: {error}", "dyndns_unavailable": "The domain '{domain}' is unavailable.", - "experimental_feature": "Warning: This feature is experimental and not considered stable, you should not use it unless you know what you are doing.", "extracting": "Extracting...", "field_invalid": "Invalid field '{}'", "file_does_not_exist": "The file {path} does not exist.", diff --git a/src/domain.py b/src/domain.py index 50a6451bf..51c9fb7fb 100644 --- a/src/domain.py +++ b/src/domain.py @@ -531,10 +531,7 @@ class DomainConfigPanel(ConfigPanel): self.values["acme_eligible"] = self.cert_status["ACME_eligible"] -@is_unit_operation() -def domain_action_run( - operation_logger, domain, action, args=None -): +def domain_action_run(domain, action, args=None): import urllib.parse From e4df838d9d72d0bb22ecc9b5c9bb5d307d250b81 Mon Sep 17 00:00:00 2001 From: axolotle Date: Tue, 4 Oct 2022 18:12:10 +0200 Subject: [PATCH 25/30] cert: raise errors for cert install/renew --- locales/en.json | 3 +++ src/certificate.py | 24 ++++++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/locales/en.json b/locales/en.json index 81be8da6c..0f2ef7be8 100644 --- a/locales/en.json +++ b/locales/en.json @@ -125,8 +125,11 @@ "certmanager_attempt_to_renew_valid_cert": "The certificate for the domain '{domain}' is not about to expire! (You may use --force if you know what you're doing)", "certmanager_attempt_to_replace_valid_cert": "You are attempting to overwrite a good and valid certificate for domain {domain}! (Use --force to bypass)", "certmanager_cannot_read_cert": "Something wrong happened when trying to open current certificate for domain {domain} (file: {file}), reason: {reason}", + "certmanager_cert_install_failed": "Let's Encrypt certificate install failed for {domains}", + "certmanager_cert_install_failed_selfsigned": "Self-signed certificate install failed for {domains}", "certmanager_cert_install_success": "Let's Encrypt certificate now installed for the domain '{domain}'", "certmanager_cert_install_success_selfsigned": "Self-signed certificate now installed for the domain '{domain}'", + "certmanager_cert_renew_failed": "Let's Encrypt certificate renew failed for {domains}", "certmanager_cert_renew_success": "Let's Encrypt certificate renewed for the domain '{domain}'", "certmanager_cert_signing_failed": "Could not sign the new certificate", "certmanager_certificate_fetching_or_enabling_failed": "Trying to use the new certificate for {domain} did not work...", diff --git a/src/certificate.py b/src/certificate.py index 137a0aba0..3be821b0e 100644 --- a/src/certificate.py +++ b/src/certificate.py @@ -129,6 +129,7 @@ def certificate_install(domain_list, force=False, no_checks=False, self_signed=F def _certificate_install_selfsigned(domain_list, force=False): + failed_cert_install = [] for domain in domain_list: operation_logger = OperationLogger( @@ -223,9 +224,16 @@ def _certificate_install_selfsigned(domain_list, force=False): operation_logger.success() else: msg = f"Installation of self-signed certificate installation for {domain} failed !" + failed_cert_install.append(domain) logger.error(msg) operation_logger.error(msg) + if failed_cert_install: + raise YunohostError( + "certmanager_cert_install_failed_selfsigned", + domains=",".join(failed_cert_install) + ) + def _certificate_install_letsencrypt(domains, force=False, no_checks=False): from yunohost.domain import domain_list, _assert_domain_exists @@ -257,6 +265,7 @@ def _certificate_install_letsencrypt(domains, force=False, no_checks=False): ) # Actual install steps + failed_cert_install = [] for domain in domains: if not no_checks: @@ -285,11 +294,18 @@ def _certificate_install_letsencrypt(domains, force=False, no_checks=False): logger.error( f"Please consider checking the 'DNS records' (basic) and 'Web' categories of the diagnosis to check for possible issues that may prevent installing a Let's Encrypt certificate on domain {domain}." ) + failed_cert_install.append(domain) else: logger.success(m18n.n("certmanager_cert_install_success", domain=domain)) operation_logger.success() + if failed_cert_install: + raise YunohostError( + "certmanager_cert_install_failed", + domains=",".join(failed_cert_install) + ) + def certificate_renew(domains, force=False, no_checks=False, email=False): """ @@ -359,6 +375,7 @@ def certificate_renew(domains, force=False, no_checks=False, email=False): ) # Actual renew steps + failed_cert_install = [] for domain in domains: if not no_checks: @@ -400,6 +417,8 @@ def certificate_renew(domains, force=False, no_checks=False, email=False): logger.error(stack.getvalue()) logger.error(str(e)) + failed_cert_install.append(domain) + if email: logger.error("Sending email with details to root ...") _email_renewing_failed(domain, msg + "\n" + str(e), stack.getvalue()) @@ -407,6 +426,11 @@ def certificate_renew(domains, force=False, no_checks=False, email=False): logger.success(m18n.n("certmanager_cert_renew_success", domain=domain)) operation_logger.success() + if failed_cert_install: + raise YunohostError( + "certmanager_cert_renew_failed", + domains=",".join(failed_cert_install) + ) # # Back-end stuff # From 6295374fdb36206a01d357700be43bba25bcbfaf Mon Sep 17 00:00:00 2001 From: axolotle Date: Tue, 4 Oct 2022 19:27:04 +0200 Subject: [PATCH 26/30] configpanels: auto add i18n help for an arg if present in locales --- src/utils/config.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/utils/config.py b/src/utils/config.py index 36f7d986d..869b2792d 100644 --- a/src/utils/config.py +++ b/src/utils/config.py @@ -527,6 +527,7 @@ class ConfigPanel: "filter", "readonly", "enabled", + # "confirm", # TODO: to ask confirmation before running an action ], "defaults": {}, }, @@ -669,6 +670,9 @@ class ConfigPanel: for panel, section, option in self._iterate(): if "ask" not in option: option["ask"] = m18n.n(self.config["i18n"] + "_" + option["id"]) + # auto add i18n help text if present in locales + if m18n.key_exists(self.config["i18n"] + "_" + option["id"] + '_help'): + option["help"] = m18n.n(self.config["i18n"] + "_" + option["id"] + '_help') def display_header(message): """CLI panel/section header display""" From 1c06fd50179c53228f50a473c8e12633f3a3b073 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 4 Oct 2022 17:26:38 +0200 Subject: [PATCH 27/30] configpanels: i18n for domain cert config panel --- locales/en.json | 14 ++++++++++++++ share/config_domain.toml | 5 ----- src/certificate.py | 25 ++++++++++++++++--------- src/domain.py | 12 +++++++++--- src/utils/config.py | 2 +- 5 files changed, 40 insertions(+), 18 deletions(-) diff --git a/locales/en.json b/locales/en.json index 0f2ef7be8..ccbba1609 100644 --- a/locales/en.json +++ b/locales/en.json @@ -321,6 +321,20 @@ "domain_config_mail_in": "Incoming emails", "domain_config_mail_out": "Outgoing emails", "domain_config_xmpp": "Instant messaging (XMPP)", + "domain_config_acme_eligible": "ACME eligibility", + "domain_config_acme_eligible_explain": "This domain doesn't seem ready for a Let's Encrypt certificate. Please check your DNS configuration and HTTP server reachability. The 'DNS records' and 'Web' section in the diagnosis page can help you understand what is misconfigured.", + "domain_config_cert_install": "Install Let's Encrypt certificate", + "domain_config_cert_issuer": "Certification authority", + "domain_config_cert_no_checks": "Ignore diagnosis checks", + "domain_config_cert_renew": "Renew Let's Encrypt certificate", + "domain_config_cert_renew_help":"Certificate will be automatically renewed during the last 15 days of validity. You can manually renew it if you want to. (Not recommended).", + "domain_config_cert_summary": "Certificate status", + "domain_config_cert_summary_expired": "CRITICAL: Current certificate is not valid! HTTPS won't work at all!", + "domain_config_cert_summary_selfsigned": "WARNING: Current certificate is self-signed. Browsers will display a spooky warning to new visitors!", + "domain_config_cert_summary_abouttoexpire": "Current certificate is about to expire. It should soon be renewed automatically.", + "domain_config_cert_summary_ok": "Okay, current certificate looks good!", + "domain_config_cert_summary_letsencrypt": "Great! You're using a valid Let's Encrypt certificate!", + "domain_config_cert_validity": "Validity", "domain_created": "Domain created", "domain_creation_failed": "Unable to create domain {domain}: {error}", "domain_deleted": "Domain deleted", diff --git a/share/config_domain.toml b/share/config_domain.toml index fd12d4506..28c394cf1 100644 --- a/share/config_domain.toml +++ b/share/config_domain.toml @@ -91,8 +91,6 @@ i18n = "domain_config" type = "alert" style = "warning" visible = "acme_eligible == false" - # FIXME: improve messaging ... - ask = "Uhoh, domain isnt ready for ACME challenge according to the diagnosis" [cert.cert.cert_no_checks] ask = "Ignore diagnosis checks" @@ -101,7 +99,6 @@ i18n = "domain_config" visible = "acme_eligible == false" [cert.cert.cert_install] - ask = "Install Let's Encrypt certificate" type = "button" icon = "star" style = "success" @@ -109,8 +106,6 @@ i18n = "domain_config" enabled = "acme_eligible || cert_no_checks" [cert.cert.cert_renew] - ask = "Renew Let's Encrypt certificate" - help = "The certificate should be automatically renewed by YunoHost a few days before it expires." type = "button" icon = "refresh" style = "warning" diff --git a/src/certificate.py b/src/certificate.py index 3be821b0e..7ef7f1d54 100644 --- a/src/certificate.py +++ b/src/certificate.py @@ -153,7 +153,7 @@ def _certificate_install_selfsigned(domain_list, force=False): if not force and os.path.isfile(current_cert_file): status = _get_status(domain) - if status["summary"] == "success": + if status["style"] == "success": raise YunohostValidationError( "certmanager_attempt_to_replace_valid_cert", domain=domain ) @@ -559,9 +559,9 @@ def _fetch_and_enable_new_certificate(domain, no_checks=False): _enable_certificate(domain, new_cert_folder) # Check the status of the certificate is now good - status_summary = _get_status(domain)["summary"] + status_style = _get_status(domain)["style"] - if status_summary != "success": + if status_style != "success": raise YunohostError( "certmanager_certificate_fetching_or_enabling_failed", domain=domain ) @@ -663,18 +663,24 @@ def _get_status(domain): CA_type = "other" if days_remaining <= 0: - summary = "danger" + style = "danger" + summary = "expired" elif CA_type == "selfsigned": - summary = "warning" + style = "warning" + summary = "selfsigned" elif days_remaining < VALIDITY_LIMIT: - summary = "warning" + style = "warning" + summary = "abouttoexpire" elif CA_type == "other": - summary = "success" + style = "success" + summary = "ok" elif CA_type == "letsencrypt": - summary = "success" + style = "success" + summary = "letsencrypt" else: # shouldnt happen, because CA_type can be only selfsigned, letsencrypt, or other - summary = "" + style = "" + summary = "wat" return { "domain": domain, @@ -682,6 +688,7 @@ def _get_status(domain): "CA_name": cert_issuer, "CA_type": CA_type, "validity": days_remaining, + "style": style, "summary": summary, } diff --git a/src/domain.py b/src/domain.py index 51c9fb7fb..f5f58b3cf 100644 --- a/src/domain.py +++ b/src/domain.py @@ -505,9 +505,14 @@ class DomainConfigPanel(ConfigPanel): from yunohost.certificate import certificate_status status = certificate_status([self.entity], full=True)["certificates"][self.entity] - toml["cert"]["status"]["cert_summary"]["style"] = status["summary"] - # FIXME: improve message - toml["cert"]["status"]["cert_summary"]["ask"] = f"Status is {status['summary']} ! (FIXME: improve message depending on summary / issuer / validity ..." + toml["cert"]["status"]["cert_summary"]["style"] = status["style"] + + # i18n: domain_config_cert_summary_expired + # i18n: domain_config_cert_summary_selfsigned + # i18n: domain_config_cert_summary_abouttoexpire + # i18n: domain_config_cert_summary_ok + # i18n: domain_config_cert_summary_letsencrypt + toml["cert"]["status"]["cert_summary"]["ask"] = m18n.n(f"domain_config_cert_summary_{status['summary']}") # FIXME: Ugly hack to save the cert status and reinject it in _load_current_values ... self.cert_status = status @@ -529,6 +534,7 @@ class DomainConfigPanel(ConfigPanel): self.values["cert_validity"] = self.cert_status["validity"] self.values["cert_issuer"] = self.cert_status["CA_type"] self.values["acme_eligible"] = self.cert_status["ACME_eligible"] + self.values["summary"] = self.cert_status["summary"] def domain_action_run(domain, action, args=None): diff --git a/src/utils/config.py b/src/utils/config.py index 869b2792d..9b35d7d3b 100644 --- a/src/utils/config.py +++ b/src/utils/config.py @@ -346,7 +346,7 @@ class ConfigPanel: # FIXME: should also check that there's indeed a key called action if not self.config: - raise YunohostValidationError("config_no_such_action", action=action) + raise YunohostValidationError(f"No action named {action}", raw_msg=True) # Import and parse pre-answered options logger.debug("Import and parse pre-answered options") From 702156554a9fedf571dfea65ccd9a0ddcbbc56b5 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 4 Oct 2022 21:24:51 +0200 Subject: [PATCH 28/30] Add more info during selsigned cert generation error to debug CI --- src/certificate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/certificate.py b/src/certificate.py index 7ef7f1d54..34b16fff3 100644 --- a/src/certificate.py +++ b/src/certificate.py @@ -226,6 +226,7 @@ def _certificate_install_selfsigned(domain_list, force=False): msg = f"Installation of self-signed certificate installation for {domain} failed !" failed_cert_install.append(domain) logger.error(msg) + logger.error(status) operation_logger.error(msg) if failed_cert_install: From 463d76f867ff4253882a0709a44c1a5deac01153 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 4 Oct 2022 22:21:23 +0200 Subject: [PATCH 29/30] domain/certs: fix bug where a self-signed cert would not get identified as a self-signed cert --- src/certificate.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/certificate.py b/src/certificate.py index 34b16fff3..b0f563c32 100644 --- a/src/certificate.py +++ b/src/certificate.py @@ -35,6 +35,7 @@ from datetime import datetime from moulinette import m18n from moulinette.utils.log import getActionLogger from moulinette.utils.filesystem import read_file +from moulinette.utils.process import check_output from yunohost.vendor.acme_tiny.acme_tiny import get_crt as sign_certificate from yunohost.utils.error import YunohostError, YunohostValidationError @@ -656,7 +657,17 @@ def _get_status(domain): ) days_remaining = (valid_up_to - datetime.utcnow()).days - if cert_issuer in ["yunohost.org"] + yunohost.domain.domain_list()["domains"]: + self_signed_issuers = ["yunohost.org"] + yunohost.domain.domain_list()["domains"] + + # FIXME: is the .ca.cnf one actually used anywhere ? x_x + conf = os.path.join(SSL_DIR, "openssl.ca.cnf") + if os.path.exists(conf): + self_signed_issuers.append(check_output(f"grep commonName_default {conf}").split()[-1]) + conf = os.path.join(SSL_DIR, "openssl.cnf") + if os.path.exists(conf): + self_signed_issuers.append(check_output(f"grep commonName_default {conf}").split()[-1]) + + if cert_issuer in self_signed_issuers: CA_type = "selfsigned" elif organization_name == "Let's Encrypt": CA_type = "letsencrypt" @@ -905,6 +916,4 @@ def _name_self_CA(): def _tail(n, file_path): - from moulinette.utils.process import check_output - return check_output(f"tail -n {n} '{file_path}'") From fe4f8b4d5e00414a2ed7d24fe35ac7c65dfdf33c Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 4 Oct 2022 22:27:23 +0200 Subject: [PATCH 30/30] not foo -> foo is None --- src/utils/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/config.py b/src/utils/config.py index 9b35d7d3b..b55478007 100644 --- a/src/utils/config.py +++ b/src/utils/config.py @@ -1551,7 +1551,7 @@ def ask_questions_and_parse_answers( question = question_class(raw_question, context=context, hooks=hooks) if question.type == "button": if ( - not question.enabled # type: ignore + question.enabled is None # type: ignore or evaluate_simple_js_expression(question.enabled, context=context) # type: ignore ): continue