diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..25fe0e5fc --- /dev/null +++ b/.travis.yml @@ -0,0 +1,5 @@ +language: python +install: "pip install pytest pyyaml" +python: + - "2.7" +script: "py.test tests" diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md new file mode 100644 index 000000000..375c1cec6 --- /dev/null +++ b/CONTRIBUTORS.md @@ -0,0 +1,88 @@ +YunoHost core contributors +========================== + +YunoHost is built and maintained by the YunoHost project community. +Everyone is encouraged to submit issues and changes, and to contribute in other ways -- see https://yunohost.org/contribute to find out how. + +-- + +Initial YunoHost core was built by Kload & beudbeud, for YunoHost v2. + +Most of code was written by Kload and jerome, with help of numerous contributors. + +Translation is made by a bunch of lovely people all over the world. + +We would like to thank anyone who ever helped the YunoHost project <3 + + +YunoHost core Contributors +-------------------------- + +- Jérôme Lebleu +- Kload +- Laurent 'Bram' Peuch +- Julien 'ju' Malik +- opi +- Aleks +- Adrien 'beudbeud' Beudin +- M5oul +- Valentin 'zamentur' / 'ljf' Grimaud +- Jocelyn Delalande +- infertux +- Taziden +- ZeHiro +- Josue-T +- nahoj +- a1ex +- JimboJoe +- vetetix +- jellium +- Sebastien 'sebian' Badia +- lmangani +- Julien Vaubourg + + +YunoHost core Translators +------------------------- + +If you want to help translation, please visit https://translate.yunohost.org/projects/yunohost/yunohost/ + + +### Dutch + +- DUBWiSE +- marut + +### English + +- Bugsbane + +### French + +- aoz roon +- Genma +- Jean-Baptiste Holcroft +- Jérôme Lebleu + +### German + +- david.bartke +- Felix Bartels +- Philip Gatzka + +### Hindi + +- Anmol + +### Italian + +- Thomas Bille + +### Portuguese + +- Deleted User + +### Spanish + +- Juanu + diff --git a/README.md b/README.md index 6e928e9ce..9aed880ac 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,42 @@ -Please report issues here (no registration needed): -https://dev.yunohost.org/projects/yunohost/issues +# YunoHost core + +- [YunoHost project website](https://yunohost.org) + +This repository is the core of YunoHost code. + + +Translation status + + +## Issues +- [Please report issues on YunoHost bugtracker](https://dev.yunohost.org/projects/yunohost/issues) (no registration needed). + +## Contribute +- You can develop on this repository using [ynh-dev tool](https://github.com/YunoHost/ynh-dev) with `use-git` sub-command. +- On this repository we are [following this workflow](https://yunohost.org/#/build_system_en): `stable <— testing <— branch`. +- Note: if you modify python scripts, you will have to modifiy the actions map. + +## Repository content +- [YunoHost core Python 2.7 scripts](https://github.com/YunoHost/yunohost/tree/stable/src/yunohost). +- [An actionsmap](https://github.com/YunoHost/yunohost/blob/stable/data/actionsmap/yunohost.yml) used by moulinette. +- [Services configuration templates](https://github.com/YunoHost/yunohost/tree/stable/data/templates). +- [Hooks](https://github.com/YunoHost/yunohost/tree/stable/data/hooks). +- [Locales](https://github.com/YunoHost/yunohost/tree/stable/locales) for translations of `yunohost` command. +- [Shell helpers](https://github.com/YunoHost/yunohost/tree/stable/data/helpers.d) for [application packaging](https://yunohost.org/#/packaging_apps_helpers_en). +- [Modules for the XMPP server Metronome](https://github.com/YunoHost/yunohost/tree/stable/lib/metronome/modules). +- [Debian files](https://github.com/YunoHost/yunohost/tree/stable/debian) for package creation. + +## How does it works? +- Python core scripts are accessible through two interfaces thanks to the [moulinette framework](https://github.com/YunoHost/moulinette): + - [CLI](https://en.wikipedia.org/wiki/Command-line_interface) for `yunohost` command. + - [API](https://en.wikipedia.org/wiki/Application_programming_interface) for [web administration module](https://github.com/YunoHost/yunohost-admin) (other modules could be implemented). +- You can find more details about how YunoHost works on this [documentation (in french)](https://yunohost.org/#/package_list_fr). + +## Dependencies +- [Python 2.7](https://www.python.org/download/releases/2.7) +- [Moulinette](https://github.com/YunoHost/moulinette) +- [Bash](https://www.gnu.org/software/bash/bash.html) +- [Debian Jessie](https://www.debian.org/releases/jessie) + +## License +As [other components of YunoHost core code](https://yunohost.org/#/faq_en), this repository is under GNU AGPL v.3 license. diff --git a/bin/yunohost b/bin/yunohost index b2a0e4b1b..0bf2c004c 100755 --- a/bin/yunohost +++ b/bin/yunohost @@ -66,6 +66,10 @@ def _parse_cli_args(): action='store_true', default=False, help="Don't produce any output", ) + parser.add_argument('--timeout', + type=int, default=None, + help="Number of seconds before this command will timeout because it can't acquire the lock (meaning that another command is currently running), by default there is no timeout and the command will wait until it can get the lock", + ) parser.add_argument('--admin-password', default=None, dest='password', metavar='PASSWORD', help="The admin password to use to authenticate", @@ -209,6 +213,7 @@ if __name__ == '__main__': ret = moulinette.cli( _retrieve_namespaces(), args, use_cache=opts.use_cache, output_as=opts.output_as, - password=opts.password, parser_kwargs={'top_parser': parser} + password=opts.password, parser_kwargs={'top_parser': parser}, + timeout=opts.timeout, ) sys.exit(ret) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 5a1465258..25eb6cf6d 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -305,8 +305,71 @@ domain: - !!str ^[0-9]+$ - "pattern_positive_number" + ### certificate_status() + cert-status: + action_help: List status of current certificates (all by default). + api: GET /domains/cert-status/ + configuration: + authenticate: all + authenticator: ldap-anonymous + arguments: + domain_list: + help: Domains to check + nargs: "*" + --full: + help: Show more details + action: store_true - ### domain_info() + ### certificate_install() + cert-install: + action_help: Install Let's Encrypt certificates for given domains (all by default). + api: POST /domains/cert-install/ + configuration: + authenticate: all + authenticator: ldap-anonymous + arguments: + domain_list: + help: Domains for which to install the certificates + nargs: "*" + --force: + help: Install even if current certificate is not self-signed + action: store_true + --no-checks: + help: Does not perform any check that your domain seems correctly configured (DNS, reachability) before attempting to install. (Not recommended) + action: store_true + --self-signed: + help: Install self-signed certificate instead of Let's Encrypt + action: store_true + --staging: + help: Use the fake/staging Let's Encrypt certification authority. The new certificate won't actually be enabled - it is only intended to test the main steps of the procedure. + action: store_true + + ### certificate_renew() + cert-renew: + action_help: Renew the Let's Encrypt certificates for given domains (all by default). + api: POST /domains/cert-renew/ + configuration: + authenticate: all + authenticator: ldap-anonymous + arguments: + domain_list: + help: Domains for which to renew the certificates + nargs: "*" + --force: + help: Ignore the validity threshold (30 days) + action: store_true + --email: + help: Send an email to root with logs if some renewing fails + action: store_true + --no-checks: + help: Does not perform any check that your domain seems correctly configured (DNS, reachability) before attempting to renew. (Not recommended) + action: store_true + --staging: + help: Use the fake/staging Let's Encrypt certification authority. The new certificate won't actually be enabled - it is only intended to test the main steps of the procedure. + action: store_true + + + ### domain_info() # info: # action_help: Get domain informations # api: GET /domains/ @@ -436,6 +499,10 @@ app: -a: full: --args help: Serialized arguments for app script (i.e. "domain=domain.tld&path=/path") + -n: + full: --no-remove-on-failure + help: Debug option to avoid removing the app on a failed installation + action: store_true ### app_remove() TODO: Write help remove: @@ -1200,7 +1267,7 @@ tools: ### tools_maindomain() maindomain: - action_help: Main domain change tool + action_help: Check the current main domain, or change it api: - GET /domains/main - PUT /domains/main @@ -1208,12 +1275,9 @@ tools: authenticate: all lock: false arguments: - -o: - full: --old-domain - extra: - pattern: *pattern_domain -n: full: --new-domain + help: Change the current main domain extra: pattern: *pattern_domain diff --git a/data/helpers.d/filesystem b/data/helpers.d/filesystem index 3fc1b0eb8..27a016e63 100644 --- a/data/helpers.d/filesystem +++ b/data/helpers.d/filesystem @@ -17,7 +17,7 @@ ynh_backup() { # validate arguments [[ -e "${SRCPATH}" ]] || { - echo "Source path '${DESTPATH}' does not exist" >&2 + echo "Source path '${SRCPATH}' does not exist" >&2 return 1 } diff --git a/data/helpers.d/string b/data/helpers.d/string index a2bf0d463..1a848d239 100644 --- a/data/helpers.d/string +++ b/data/helpers.d/string @@ -6,6 +6,6 @@ # | arg: length - the string length to generate (default: 24) ynh_string_random() { dd if=/dev/urandom bs=1 count=200 2> /dev/null \ - | tr -c -d '[A-Za-z0-9]' \ + | tr -c -d 'A-Za-z0-9' \ | sed -n 's/\(.\{'"${1:-24}"'\}\).*/\1/p' } diff --git a/data/hooks/conf_regen/06-slapd b/data/hooks/conf_regen/06-slapd index b3353962e..aef47c347 100755 --- a/data/hooks/conf_regen/06-slapd +++ b/data/hooks/conf_regen/06-slapd @@ -102,6 +102,23 @@ do_post_regen() { fi sudo service slapd force-reload + + # on slow hardware/vm this regen conf would exit before the admin user that + # is stored in ldap is available because ldap seems to slow to restart + # so we'll wait either until we are able to log as admin or until a timeout + # is reached + # we need to do this because the next hooks executed after this one during + # postinstall requires to run as admin thus breaking postinstall on slow + # hardware which mean yunohost can't be correctly installed on those hardware + # and this sucks + # wait a maximum time of 5 minutes + # yes, force-reload behave like a restart + number_of_wait=0 + while ! sudo su admin -c '' && ((number_of_wait < 60)) + do + sleep 5 + ((number_of_wait += 1)) + done } FORCE=${2:-0} diff --git a/data/hooks/conf_regen/25-dovecot b/data/hooks/conf_regen/25-dovecot index 5d82470a5..4c5ae24c1 100755 --- a/data/hooks/conf_regen/25-dovecot +++ b/data/hooks/conf_regen/25-dovecot @@ -26,11 +26,18 @@ do_pre_regen() { 's/^\(listen =\).*/\1 */' \ "${dovecot_dir}/dovecot.conf" fi + + mkdir -p "${dovecot_dir}/yunohost.d" + cp pre-ext.conf "${dovecot_dir}/yunohost.d" + cp post-ext.conf "${dovecot_dir}/yunohost.d" } do_post_regen() { regen_conf_files=$1 + sudo mkdir -p "/etc/dovecot/yunohost.d/pre-ext.d" + sudo mkdir -p "/etc/dovecot/yunohost.d/post-ext.d" + # create vmail user id vmail > /dev/null 2>&1 \ || sudo adduser --system --ingroup mail --uid 500 vmail diff --git a/data/hooks/conf_regen/28-rmilter b/data/hooks/conf_regen/28-rmilter index 05f921e09..011856cd6 100755 --- a/data/hooks/conf_regen/28-rmilter +++ b/data/hooks/conf_regen/28-rmilter @@ -9,7 +9,9 @@ do_pre_regen() { install -D -m 644 rmilter.conf \ "${pending_dir}/etc/rmilter.conf" - install -D -m 644 rmilter.socket \ + # Remove old socket file (we stopped using it, since rspamd 1.3.1) + # Regen-conf system need an empty file to delete it + install -D -m 644 /dev/null \ "${pending_dir}/etc/systemd/system/rmilter.socket" } @@ -37,17 +39,19 @@ do_post_regen() { sudo chown _rmilter /etc/dkim/*.mail.key sudo chmod 400 /etc/dkim/*.mail.key + # fix rmilter socket permission (postfix is chrooted in /var/spool/postfix ) + sudo mkdir -p /var/spool/postfix/run/rmilter + sudo chown -R postfix:_rmilter /var/spool/postfix/run/rmilter + sudo chmod g+w /var/spool/postfix/run/rmilter + [ -z "$regen_conf_files" ] && exit 0 # reload systemd daemon - [[ "$regen_conf_files" =~ rmilter\.socket ]] && { - sudo systemctl -q daemon-reload - } + sudo systemctl -q daemon-reload - # ensure that the socket is listening and stop the service - it will be - # started again by the socket as needed - sudo systemctl -q start rmilter.socket - sudo systemctl -q stop rmilter.service 2>&1 || true + # Restart rmilter due to the rspamd update + # https://rspamd.com/announce/2016/08/01/rspamd-1.3.1.html + sudo systemctl -q restart rmilter.service } FORCE=${2:-0} diff --git a/data/hooks/conf_regen/31-rspamd b/data/hooks/conf_regen/31-rspamd index 327bedef1..afdfc1bf1 100755 --- a/data/hooks/conf_regen/31-rspamd +++ b/data/hooks/conf_regen/31-rspamd @@ -25,10 +25,9 @@ do_post_regen() { sudo systemctl restart dovecot } - # ensure that the socket is listening and stop the service - it will be - # started again by the socket as needed - sudo systemctl -q start rspamd.socket - sudo systemctl -q stop rspamd.service 2>&1 || true + # Restart rspamd due to the upgrade + # https://rspamd.com/announce/2016/08/01/rspamd-1.3.1.html + sudo systemctl -q restart rspamd.service } FORCE=${2:-0} diff --git a/data/templates/dovecot/dovecot.conf b/data/templates/dovecot/dovecot.conf index 3daa670bf..5ea10ea79 100644 --- a/data/templates/dovecot/dovecot.conf +++ b/data/templates/dovecot/dovecot.conf @@ -1,18 +1,48 @@ -# 2.1.7: /etc/dovecot/dovecot.conf -# OS: Linux 3.2.0-3-686-pae i686 Debian wheezy/sid ext4 +!include yunohost.d/pre-ext.conf + listen = *, :: auth_mechanisms = plain login -login_greeting = Dovecot ready!! + mail_gid = 8 mail_home = /var/mail/%n mail_location = maildir:/var/mail/%n mail_uid = 500 + +protocols = imap sieve + +mail_plugins = $mail_plugins quota + + +ssl = yes +ssl_cert = [\w\-.^_]+) # Values: TEXT # -failregex = access.lua:[1-9]+: authenticate\(\): Connection failed for: .*, client: +failregex = helpers.lua:[1-9]+: authenticate\(\): Connection failed for: .*, client: ^ -.*\"POST /yunohost/api/login HTTP/1.1\" 401 22 # Option: ignoreregex diff --git a/data/templates/postfix/main.cf b/data/templates/postfix/main.cf index f3597e136..b0b2688d9 100644 --- a/data/templates/postfix/main.cf +++ b/data/templates/postfix/main.cf @@ -141,7 +141,7 @@ smtp_reply_filter = pcre:/etc/postfix/smtp_reply_filter # Rmilter milter_mail_macros = i {mail_addr} {client_addr} {client_name} {auth_authen} milter_protocol = 6 -smtpd_milters = inet:localhost:11000 +smtpd_milters = unix:/run/rmilter/rmilter.sock # Skip email without checking if milter has died milter_default_action = accept diff --git a/data/templates/rmilter/rmilter.conf b/data/templates/rmilter/rmilter.conf index d585b9217..829d76418 100644 --- a/data/templates/rmilter/rmilter.conf +++ b/data/templates/rmilter/rmilter.conf @@ -5,8 +5,7 @@ # pidfile - path to pid file pidfile = /run/rmilter/rmilter.pid; -# rmilter is socket-activated under systemd -bind_socket = fd:3; +bind_socket = unix:/var/spool/postfix/run/rmilter/rmilter.sock; # DKIM signing dkim { diff --git a/data/templates/rmilter/rmilter.socket b/data/templates/rmilter/rmilter.socket deleted file mode 100644 index dc3ae7a2a..000000000 --- a/data/templates/rmilter/rmilter.socket +++ /dev/null @@ -1,5 +0,0 @@ -.include /lib/systemd/system/rmilter.socket - -[Socket] -ListenStream= -ListenStream=127.0.0.1:11000 diff --git a/debian/changelog b/debian/changelog index 94ddd58ec..2a719fc83 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,141 @@ +yunohost (2.5.3.1) testing; urgency=low + + * super quickfix release for a typo that break LE certificates + + -- Laurent Peuch Tue, 10 Jan 2017 02:58:56 +0100 + +yunohost (2.5.3) testing; urgency=low + + Love: + * [enh][love] Add CONTRIBUTORS.md + + LE: + * Check acme challenge conf exists in nginx when renewing cert + * Fix bad validity check.. + + Fix a situation where to domain for the LE cert can't be locally resolved: + * Adding check that domain is resolved locally for cert management + * Changing the way to check domain is locally resolved + + Fix a situation where a cert could end up with bad perms for metronome: + * Attempt to fix missing perm for metronome in weird cases + + Rspamd cannot be activate on socket anymore: + * [fix] new rspamd version replace rspamd.socket with rspamd.service + * [fix] Remove residual rmilter socket file + * [fix] Postfix can't access rmilter socket due to chroot + + Various: + * fix fail2ban rules to take into account failed loggin on ssowat + * [fix] Ignore dyndns option is not needed with small domain + * [enh] add yaml syntax check in travis.yml + * [mod] autopep8 on all files that aren't concerned by a PR + * [fix] add timeout to fetchlist's wget + + Thanks to all contributors: Aleks, Bram, ju, ljf, opi, zimo2001 and to the + people who are participating to the beta and giving us feedback <3 + + + -- Laurent Peuch Mon, 09 Jan 2017 18:38:30 +0100 + +yunohost (2.5.2) testing; urgency=low + + LDAP admin user: + * [fix] wait for admin user to be available after a slapd regen-conf, this fix install on slow hardware/vps + + Dovecot/emails: + * [enh] reorder dovecot main configuration so that it is easier to read and extend + * [enh] Allow for dovecot configuration extensions + * [fix] Can't get mailbos used space if dovecot is down + + Backup: + * [fix] Need to create archives_path even for custom output directory + * Keep track of backups with custom directory using symlinks + + Security: + * [fix] Improve dnssec key generation on low entropy devices + * [enh] Add haveged as dependency + + Random broken app installed on slow hardware: + * [enh] List available domains when installing an app by CLI. + + Translation: + * French by Jibec and Genma + * German by Philip Gatzka + * Hindi by Anmol + * Spanish by Juanu + + Other fixes and improvements: + * [enh] remove timeout from cli interface + * [fix] [#662](https://dev.yunohost.org/issues/662): missing 'python-openssl' dependency for Let's Encrypt integration. + * [fix] --no-remove-on-failure for app install should behave as a flag. + * [fix] don't remove trailing char if it's not a slash + + Thanks to all contributors: Aleks, alex, Anmol, Bram, Genma, jibec, ju, + Juanu, ljf, Moul, opi, Philip Gatzka and to the people who are participating + to the beta and giving us feedback <3 + + -- Laurent Peuch Fri, 16 Dec 2016 00:49:08 +0100 + +yunohost (2.5.1) testing; urgency=low + + * [fix] Raise error on malformed SSOwat persistent conf. + * [enh] Catch SSOwat persistent configuration write error. + * [fix] Write SSOwat configuration file only if needed. + * [enh] Display full exception error message. + * [enh] cli option to avoid removing an application on installation failure + * [mod] give instructions on how to solve the conf.json.persistant parsing error + * [fix] avoid random bug on post-install due to nscd cache + * [enh] Adding check that user is actually created + minor refactor of ldap/auth init + * [fix] Fix the way name of self-CA is determined + * [fix] Add missing dependency to nscd package #656 + * [fix] Refactoring tools_maindomain and disabling removal of main domain to avoid breaking things + * [fix] Bracket in passwd from ynh_string_random + + Thanks to all contributors: Aleks, Bram, ju, jibec, ljf, M5oul, opi + + -- Laurent Peuch Sun, 11 Dec 2016 15:26:21 +0100 + +yunohost (2.5.0) testing; urgency=low + + * Certificate management integration (e.g. Let's Encrypt certificate install) + * [fix] Support git ynh app with submodules #533 (#174) + * [enh] display file path on file_not_exist error + * [mod] move a part of os.system calls to native shutil/os + * [fix] Can't restore app on a root domain + + Miscellaneous + + * Update backup.py + * [mod] autopep8 + * [mod] trailing spaces + * [mod] pep8 + * [mod] remove useless imports + * [mod] more pythonic and explicit tests with more verbose errors + * [fix] correctly handle all cases + * [mod] simplier condition + * [fix] uses https + * [mod] uses logger string concatenation api + * [mod] small opti, getting domain list can be slow + * [mod] pylint + * [mod] os.path.join + * [mod] remove useless assign + * [enh] include tracebak into error email + * [mod] remove the summary code concept and switch to code/verbose duet instead + * [mod] I only need to reload nginx, not restart it + * [mod] top level constants should be upper case (pep8) + * Check that the DNS A record matches the global IP now using dnspython and FDN's DNS + * Refactored the self-signed cert generation, some steps were overly complicated for no reason + * Using a single generic skipped regex for acme challenge in ssowat conf + * Adding an option to use the staging Let's Encrypt CA, sort of a dry-run + * [enh] Complete readme (#183) + * [fix] avoid reverse order log display on web admin + + Thanks to all contributors: Aleks, Bram, JimboJoe, ljf, M5oul + Kudos to Aleks for leading the Let's Encrypt integration to YunoHost core \o/ + + -- opi Thu, 01 Dec 2016 21:22:19 +0100 + yunohost (2.4.2) stable; urgency=low [ Laurent Peuch ] diff --git a/debian/control b/debian/control index 80f562e76..47f0e61b2 100644 --- a/debian/control +++ b/debian/control @@ -11,13 +11,13 @@ Package: yunohost Architecture: all Depends: ${python:Depends}, ${misc:Depends} , moulinette (>= 2.3.5.1) - , python-psutil, python-requests, python-dnspython + , python-psutil, python-requests, python-dnspython, python-openssl , python-apt, python-miniupnpc , glances , dnsutils, bind9utils, unzip, git, curl, cron , ca-certificates, netcat-openbsd, iproute , mariadb-server | mysql-server, php5-mysql | php5-mysqlnd - , slapd, ldap-utils, sudo-ldap, libnss-ldapd + , slapd, ldap-utils, sudo-ldap, libnss-ldapd, nscd , postfix-ldap, postfix-policyd-spf-perl, postfix-pcre, procmail , dovecot-ldap, dovecot-lmtpd, dovecot-managesieved , dovecot-antispam, fail2ban @@ -25,6 +25,7 @@ Depends: ${python:Depends}, ${misc:Depends} , dnsmasq, openssl, avahi-daemon , ssowat, metronome , rspamd (>= 1.2.0), rmilter (>=1.7.0), redis-server, opendkim-tools + , haveged Recommends: yunohost-admin , openssh-server, ntp, inetutils-ping | iputils-ping , bash-completion, rsyslog, etckeeper diff --git a/locales/br.json b/locales/br.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/locales/br.json @@ -0,0 +1 @@ +{} diff --git a/locales/de.json b/locales/de.json index 1331c56b4..0fff3e0b4 100644 --- a/locales/de.json +++ b/locales/de.json @@ -209,5 +209,14 @@ "yunohost_ca_creation_failed": "Zertifikatsstelle konnte nicht erstellt werden", "yunohost_configured": "YunoHost wurde erfolgreich konfiguriert", "yunohost_installing": "YunoHost wird installiert...", - "yunohost_not_installed": "Die YunoHost ist unvollständig. Bitte 'yunohost tools postinstall' ausführen." + "yunohost_not_installed": "Die YunoHost ist unvollständig. Bitte 'yunohost tools postinstall' ausführen.", + "app_not_properly_removed": "{app:s} wurde nicht ordnungsgemäß entfernt", + "service_regenconf_failed": "Konnte die Konfiguration für folgende Dienste nicht neu erzeugen: {services}", + "not_enough_disk_space": "Zu wenig Speicherplatz unter '{path:s}' verfügbar", + "backup_creation_failed": "Erzeugung des Backups fehlgeschlagen", + "service_conf_up_to_date": "Die Konfiguration für den Dienst '{service}' ist bereits aktuell", + "package_not_installed": "Paket '{pkgname}' ist nicht installiert", + "pattern_positive_number": "Muss eine positive Zahl sein", + "diagnostic_kernel_version_error": "Kann Kernelversion nicht abrufen: {error}", + "package_unexpected_error": "Ein unerwarteter Fehler trat bei der Verarbeitung des Pakets '{pkgname}' auf" } diff --git a/locales/en.json b/locales/en.json index e939b26fa..5e9d02e06 100644 --- a/locales/en.json +++ b/locales/en.json @@ -43,6 +43,7 @@ "backup_action_required": "You must specify something to save", "backup_app_failed": "Unable to back up the app '{app:s}'", "backup_archive_app_not_found": "App '{app:s}' not found in the backup archive", + "backup_archive_broken_link": "Unable to access backup archive (broken link to {path:s})", "backup_archive_hook_not_exec": "Hook '{hook:s}' not executed in this backup", "backup_archive_name_exists": "The backup's archive name already exists", "backup_archive_name_unknown": "Unknown local backup archive named '{name:s}'", @@ -57,7 +58,7 @@ "backup_hook_unknown": "Backup hook '{hook:s}' unknown", "backup_invalid_archive": "Invalid backup archive", "backup_nothings_done": "There is nothing to save", - "backup_output_directory_forbidden": "Forbidden output directory. Backups can't be created in /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var or /home/yunohost.backup/archives sub-folders.", + "backup_output_directory_forbidden": "Forbidden output directory. Backups can't be created in /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var or /home/yunohost.backup/archives sub-folders", "backup_output_directory_not_empty": "The output directory is not empty", "backup_output_directory_required": "You must provide an output directory for the backup", "backup_running_app_script": "Running backup script of app '{app:s}'...", @@ -71,7 +72,6 @@ "diagnostic_monitor_system_error": "Can't monitor system: {error}", "diagnostic_no_apps": "No installed application", "dnsmasq_isnt_installed": "dnsmasq does not seem to be installed, please run 'apt-get remove bind9 && apt-get install dnsmasq'", - "domain_cert_gen_failed": "Unable to generate certificate", "domain_created": "The domain has been created", "domain_creation_failed": "Unable to create domain", "domain_deleted": "The domain has been deleted", @@ -80,11 +80,12 @@ "domain_dyndns_invalid": "Invalid domain to use with DynDNS", "domain_dyndns_root_unknown": "Unknown DynDNS root domain", "domain_exists": "Domain already exists", - "domain_uninstall_app_first": "One or more apps are installed on this domain. Please uninstall them before proceeding to domain removal.", + "domain_uninstall_app_first": "One or more apps are installed on this domain. Please uninstall them before proceeding to domain removal", "domain_unknown": "Unknown domain", "domain_zone_exists": "DNS zone file already exists", "domain_zone_not_found": "DNS zone file not found for domain {:s}", - "done": "Done.", + "done": "Done", + "domains_available": "Available domains:", "downloading": "Downloading...", "dyndns_cron_installed": "The DynDNS cron job has been installed", "dyndns_cron_remove_failed": "Unable to remove the DynDNS cron job", @@ -103,7 +104,7 @@ "field_invalid": "Invalid field '{:s}'", "firewall_reload_failed": "Unable to reload the firewall", "firewall_reloaded": "The firewall has been reloaded", - "firewall_rules_cmd_failed": "Some firewall rules commands have failed. For more information, see the log.", + "firewall_rules_cmd_failed": "Some firewall rules commands have failed. For more information, see the log", "format_datetime_short": "%m/%d/%Y %I:%M %p", "hook_exec_failed": "Script execution failed: {path:s}", "hook_exec_not_terminated": "Script execution hasn’t terminated: {path:s}", @@ -111,13 +112,15 @@ "hook_name_unknown": "Unknown hook name '{name:s}'", "installation_complete": "Installation complete", "installation_failed": "Installation failed", - "ip6tables_unavailable": "You cannot play with ip6tables here. You are either in a container or your kernel does not support it.", - "iptables_unavailable": "You cannot play with iptables here. You are either in a container or your kernel does not support it.", + "ip6tables_unavailable": "You cannot play with ip6tables here. You are either in a container or your kernel does not support it", + "iptables_unavailable": "You cannot play with iptables here. You are either in a container or your kernel does not support it", "ldap_initialized": "LDAP has been initialized", + "ldap_init_failed_to_create_admin": "LDAP initialization failed to create admin user", "license_undefined": "undefined", "mail_alias_remove_failed": "Unable to remove mail alias '{mail:s}'", "mail_domain_unknown": "Unknown mail address domain '{domain:s}'", "mail_forward_remove_failed": "Unable to remove mail forward '{mail:s}'", + "mailbox_used_space_dovecot_down": "Dovecot mailbox service need to be up, if you want to get mailbox used space", "maindomain_change_failed": "Unable to change the main domain", "maindomain_changed": "The main domain has been changed", "monitor_disabled": "The server monitoring has been disabled", @@ -209,6 +212,8 @@ "service_unknown": "Unknown service '{service:s}'", "ssowat_conf_generated": "The SSOwat configuration has been generated", "ssowat_conf_updated": "The SSOwat configuration has been updated", + "ssowat_persistent_conf_read_error": "Error while reading SSOwat persistent configuration: {error:s}. Edit /etc/ssowat/conf.json.persistent file to fix the JSON syntax", + "ssowat_persistent_conf_write_error": "Error while saving SSOwat persistent configuration: {error:s}. Edit /etc/ssowat/conf.json.persistent file to fix the JSON syntax", "system_upgraded": "The system has been upgraded", "system_username_exists": "Username already exists in the system users", "unbackup_app": "App '{app:s}' will not be saved", @@ -237,5 +242,31 @@ "yunohost_ca_creation_failed": "Unable to create certificate authority", "yunohost_configured": "YunoHost has been configured", "yunohost_installing": "Installing YunoHost...", - "yunohost_not_installed": "YunoHost is not or not correctly installed. Please execute 'yunohost tools postinstall'." + "yunohost_not_installed": "YunoHost is not or not correctly installed. Please execute 'yunohost tools postinstall'", + "domain_cert_gen_failed": "Unable to generate certificate", + "certmanager_attempt_to_replace_valid_cert": "You are attempting to overwrite a good and valid certificate for domain {domain:s}! (Use --force to bypass)", + "certmanager_domain_unknown": "Unknown domain {domain:s}", + "certmanager_domain_cert_not_selfsigned": "The certificate for domain {domain:s} is not self-signed. Are you sure you want to replace it? (Use --force)", + "certmanager_certificate_fetching_or_enabling_failed": "Sounds like enabling the new certificate for {domain:s} failed somehow...", + "certmanager_attempt_to_renew_nonLE_cert": "The certificate for domain {domain:s} is not issued by Let's Encrypt. Cannot renew it automatically!", + "certmanager_attempt_to_renew_valid_cert": "The certificate for domain {domain:s} is not about to expire! Use --force to bypass", + "certmanager_domain_http_not_working": "It seems that the domain {domain:s} cannot be accessed through HTTP. Please check your DNS and nginx configuration is okay", + "certmanager_error_no_A_record": "No DNS 'A' record found for {domain:s}. You need to make your domain name point to your machine to be able to install a Let's Encrypt certificate! (If you know what you are doing, use --no-checks to disable those checks.)", + "certmanager_domain_dns_ip_differs_from_public_ip": "The DNS 'A' record for domain {domain:s} is different from this server IP. 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 disable those checks.)", + "certmanager_domain_not_resolved_locally": "The domain {domain:s} cannot be resolved from inside your Yunohost server. This might happen if you recently modified your DNS record. If so, please wait a few hours for it to propagate. If the issue persists, consider adding {domain:s} to /etc/hosts. (If you know what you are doing, use --no-checks to disable those checks.)", + "certmanager_cannot_read_cert": "Something wrong happened when trying to open current certificate for domain {domain:s} (file: {file:s}), reason: {reason:s}", + "certmanager_cert_install_success_selfsigned": "Successfully installed a self-signed certificate for domain {domain:s}!", + "certmanager_cert_install_success": "Successfully installed Let's Encrypt certificate for domain {domain:s}!", + "certmanager_cert_renew_success": "Successfully renewed Let's Encrypt certificate for domain {domain:s}!", + "certmanager_old_letsencrypt_app_detected": "\nYunohost detected that the 'letsencrypt' app is installed, which conflits with the new built-in certificate management features in Yunohost. If you wish to use the new built-in features, please run the following commands to migrate your installation:\n\n yunohost app remove letsencrypt\n yunohost domain cert-install\n\nN.B.: this will attempt to re-install certificates for all domains with a Let's Encrypt certificate or self-signed certificate", + "certmanager_hit_rate_limit":"Too many certificates already issued for exact set of domains {domain:s} recently. Please try again later. See https://letsencrypt.org/docs/rate-limits/ for more details", + "certmanager_cert_signing_failed": "Signing the new certificate failed", + "certmanager_no_cert_file": "Unable to read certificate file for domain {domain:s} (file: {file:s})", + "certmanager_conflicting_nginx_file": "Unable to prepare domain for ACME challenge: the nginx configuration file {filepath:s} is conflicting and should be removed first", + "domain_cannot_remove_main": "Cannot remove main domain. Set a new main domain first", + "certmanager_self_ca_conf_file_not_found": "Configuration file not found for self-signing authority (file: {file:s})", + "certmanager_acme_not_configured_for_domain": "Certificate for domain {domain:s} does not appear to be correctly installed. Please run cert-install for this domain first.", + "certmanager_http_check_timeout" : "Timed out when server tried to contact itself through HTTP using public IP address (domain {domain:s} with ip {ip:s}). You may be experiencing hairpinning or the firewall/router ahead of your server is misconfigured.", + "certmanager_couldnt_fetch_intermediate_cert" : "Timed out when trying to fetch intermediate certificate from Let's Encrypt. Certificate installation/renewal aborted - please try again later.", + "certmanager_unable_to_parse_self_CA_name": "Unable to parse name of self-signing authority (file: {file:s})" } diff --git a/locales/eo.json b/locales/eo.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/locales/eo.json @@ -0,0 +1 @@ +{} diff --git a/locales/es.json b/locales/es.json index 549cbe29a..c5ead2908 100644 --- a/locales/es.json +++ b/locales/es.json @@ -1,12 +1,12 @@ { - "action_invalid": "Acción no válida '{action:s}'", + "action_invalid": "Acción no válida '{action:s} 1'", "admin_password": "Contraseña administrativa", - "admin_password_change_failed": "No se pudo cambiar la contraseña", + "admin_password_change_failed": "No se puede cambiar la contraseña", "admin_password_changed": "La contraseña administrativa ha sido cambiada", - "app_already_installed": "{app:s} ya está instalada", - "app_argument_choice_invalid": "Opción no válida para el argumento '{name:s}', deber una de {choices:s}", - "app_argument_invalid": "Valor no válido para el argumento '{name:s}': {error:s}", - "app_argument_required": "Se requiere el argumento '{name:s}'", + "app_already_installed": "{app:s} 2 ya está instalada", + "app_argument_choice_invalid": "Opción no válida para el argumento '{name:s} 3', deber una de {choices:s} 4", + "app_argument_invalid": "Valor no válido para el argumento '{name:s} 5': {error:s} 6", + "app_argument_required": "Se requiere el argumento '{name:s} 7'", "app_extraction_failed": "No se pudieron extraer los archivos de instalación", "app_id_invalid": "Id de la aplicación no válida", "app_incompatible": "La aplicación no es compatible con su versión de YunoHost", @@ -15,9 +15,9 @@ "app_location_install_failed": "No se puede instalar la aplicación en esta localización", "app_manifest_invalid": "El manifiesto de la aplicación no es válido", "app_no_upgrade": "No hay aplicaciones para actualizar", - "app_not_correctly_installed": "La aplicación {app:s} parece estar incorrectamente instalada", - "app_not_installed": "{app:s} no está instalada", - "app_not_properly_removed": "La {app:s} no ha sido desinstalada correctamente", + "app_not_correctly_installed": "La aplicación {app:s} 8 parece estar incorrectamente instalada", + "app_not_installed": "{app:s} 9 no está instalada", + "app_not_properly_removed": "La {app:s} 0 no ha sido desinstalada correctamente", "app_package_need_update": "Es necesario actualizar el paquete de la aplicación debido a los cambios en YunoHost", "app_recent_version_required": "{:s} requiere una versión más reciente de moulinette ", "app_removed": "{app:s} ha sido eliminada", @@ -58,7 +58,7 @@ "backup_hook_unknown": "Hook de copia de seguridad desconocido '{hook:s}'", "backup_invalid_archive": "La copia de seguridad no es válida", "backup_nothings_done": "No hay nada que guardar", - "backup_output_directory_forbidden": "Directorio de salida no permitido. Las copias de seguridad no pueden ser creadas en /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var o en los subdirectorios /home/yunohost.backup.", + "backup_output_directory_forbidden": "Directorio de salida no permitido. Las copias de seguridad no pueden ser creadas en /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var o en los subdirectorios de /home/yunohost.backup/archives", "backup_output_directory_not_empty": "El directorio de salida no está vacío", "backup_output_directory_required": "Debe proporcionar un directorio de salida para la copia de seguridad", "backup_running_app_script": "Ejecutando la script de copia de seguridad de la aplicación '{app:s}'...", @@ -71,7 +71,7 @@ "diagnostic_monitor_network_error": "No se puede monitorizar la red: {error}", "diagnostic_monitor_system_error": "No se puede monitorizar el sistema: {error}", "diagnostic_no_apps": "Aplicación no instalada", - "dnsmasq_isnt_installed": "Parece que dnsmasq no está instalado, ejecuta 'apt-get remove bind9 && apt-get install dnsmasq'", + "dnsmasq_isnt_installed": "Parece que dnsmasq no está instalado, ejecute 'apt-get remove bind9 && apt-get install dnsmasq'", "domain_cert_gen_failed": "No se pudo crear el certificado", "domain_created": "El dominio ha sido creado", "domain_creation_failed": "No se pudo crear el dominio", @@ -84,11 +84,11 @@ "domain_uninstall_app_first": "Una o más aplicaciones están instaladas en este dominio. Debe desinstalarlas antes de eliminarlo.", "domain_unknown": "Dominio desconocido", "domain_zone_exists": "El archivo de zona del DNS ya existe", - "domain_zone_not_found": "No se ha encontrado el archivo de zona DNS para el dominio [:s]", + "domain_zone_not_found": "No se ha encontrado el archivo de zona del DNS para el dominio [:s]", "done": "Hecho.", "downloading": "Descargando...", "dyndns_cron_installed": "La tarea cron para DynDNS ha sido instalada", - "dyndns_cron_remove_failed": "No se pudo eliminar la tarea del cron DynDNS", + "dyndns_cron_remove_failed": "No se pudo eliminar la tarea cron DynDNS", "dyndns_cron_removed": "La tarea cron DynDNS ha sido eliminada", "dyndns_ip_update_failed": "No se pudo actualizar la dirección IP en el DynDNS", "dyndns_ip_updated": "Su dirección IP ha sido actualizada en el DynDNS", @@ -113,16 +113,16 @@ "hook_list_by_invalid": "Propiedad no válida para listar por hook", "hook_name_unknown": "Nombre de hook desconocido '{name:s}'", "installation_complete": "Instalación finalizada", - "installation_failed": "No pudo realizar la instalación", - "ip6tables_unavailable": "No puede modificar ip6tables aquí. O bien está en un 'container' o su kernel no soporta esta opción.", - "iptables_unavailable": "No puede modificar iptables aquí. O bien está en un 'container' o su kernel no soporta esta opción.", - "ldap_initialized": "LDAP iniciado", + "installation_failed": "No se pudo realizar la instalación", + "ip6tables_unavailable": "No puede modificar ip6tables aquí. O bien está en un 'container' o su kernel no soporta esta opción", + "iptables_unavailable": "No puede modificar iptables aquí. O bien está en un 'container' o su kernel no soporta esta opción", + "ldap_initialized": "Se ha inicializado LDAP", "license_undefined": "indefinido", "mail_alias_remove_failed": "No se pudo eliminar el alias de correo '{mail:s}'", "mail_domain_unknown": "El dominio de correo '{domain:s}' es desconocido", "mail_forward_remove_failed": "No se pudo eliminar el reenvío de correo '{mail:s}'", "maindomain_change_failed": "No se pudo cambiar el dominio principal", - "maindomain_changed": "El dominio principal ha sido cambiado", + "maindomain_changed": "Se ha cambiado el dominio principal", "monitor_disabled": "La monitorización del sistema ha sido deshabilitada", "monitor_enabled": "La monitorización del sistema ha sido habilitada", "monitor_glances_con_failed": "No se pudo conectar al servidor Glances", @@ -130,13 +130,13 @@ "monitor_period_invalid": "Período de tiempo no válido", "monitor_stats_file_not_found": "No se pudo encontrar el archivo de estadísticas", "monitor_stats_no_update": "No hay estadísticas de monitorización para actualizar", - "monitor_stats_period_unavailable": "No hay estadísticas para ese período", + "monitor_stats_period_unavailable": "No hay estadísticas para el período", "mountpoint_unknown": "Punto de montaje desconocido", "mysql_db_creation_failed": "No se pudo crear la base de datos MySQL", "mysql_db_init_failed": "No se pudo iniciar la base de datos MySQL", - "mysql_db_initialized": "La base de datos MySQL ha sido iniciada", + "mysql_db_initialized": "La base de datos MySQL ha sido inicializada", "network_check_mx_ko": "El registro DNS MX no está configurado", - "network_check_smtp_ko": "El puerto 25 (SMTP) para el correo saliente parece estar bloqueado en su red", + "network_check_smtp_ko": "El puerto 25 (SMTP) para el correo saliente parece estar bloqueado por su red", "network_check_smtp_ok": "El puerto de salida del correo electrónico (25, SMTP) no está bloqueado", "new_domain_required": "Debe proporcionar el nuevo dominio principal", "no_appslist_found": "No se ha encontrado ninguna lista de aplicaciones", @@ -145,13 +145,13 @@ "no_restore_script": "No se ha encontrado un script de restauración para la aplicación '{app:s}'", "not_enough_disk_space": "No hay suficiente espacio en '{path:s}'", "package_not_installed": "El paquete '{pkgname}' no está instalado", - "package_unexpected_error": "Un error inesperado procesando el paquete '{pkgname}'", + "package_unexpected_error": "Ha ocurrido un error inesperado procesando el paquete '{pkgname}'", "package_unknown": "Paquete desconocido '{pkgname}'", - "packages_no_upgrade": "No hay paquetes que actualizar", + "packages_no_upgrade": "No hay paquetes para actualizar", "packages_upgrade_critical_later": "Los paquetes críticos ({packages:s}) serán actualizados más tarde", "packages_upgrade_failed": "No se pudieron actualizar todos los paquetes", - "path_removal_failed": "No se pudo borrar la ruta {:s}", - "pattern_backup_archive_name": "Debe que ser un nombre de archivo válido, solo se admiten caracteres alfanuméricos y los guiones -_", + "path_removal_failed": "No se pudo eliminar la ruta {:s}", + "pattern_backup_archive_name": "Debe ser un nombre de archivo válido, solo se admiten caracteres alfanuméricos y los guiones -_", "pattern_domain": "El nombre de dominio debe ser válido (por ejemplo mi-dominio.org)", "pattern_email": "Debe ser una dirección de correo electrónico válida (por ejemplo, alguien@dominio.org)", "pattern_firstname": "Debe ser un nombre válido", @@ -180,7 +180,7 @@ "restore_running_hooks": "Ejecutando los hooks de restauración...", "service_add_failed": "No se pudo añadir el servicio '{service:s}'", "service_added": "Servicio '{service:s}' ha sido añadido", - "service_already_started": "El servicio '{service:s}' ya ha sido iniciado", + "service_already_started": "El servicio '{service:s}' ya ha sido inicializado", "service_already_stopped": "El servicio '{service:s}' ya ha sido detenido", "service_cmd_exec_failed": "No se pudo ejecutar el comando '{command:s}'", "service_conf_file_backed_up": "Se ha realizado una copia de seguridad del archivo de configuración '{conf}' en '{backup}'", @@ -193,15 +193,15 @@ "service_conf_file_updated": "El archivo de configuración '{conf}' ha sido actualizado", "service_conf_up_to_date": "La configuración del servicio '{service}' ya está actualizada", "service_conf_updated": "La configuración ha sido actualizada para el servicio '{service}'", - "service_conf_would_be_updated": "La configuración podría haber sido actualizada para el servicio '{service}'", + "service_conf_would_be_updated": "La configuración podría haber sido actualizada para el servicio '{service} 1'", "service_disable_failed": "No se pudo deshabilitar el servicio '{service:s}'", "service_disabled": "El servicio '{service:s}' ha sido deshabilitado", "service_enable_failed": "No se pudo habilitar el servicio '{service:s}'", "service_enabled": "El servicio '{service:s}' ha sido habilitado", "service_no_log": "No hay ningún registro para el servicio '{service:s}'", - "service_regenconf_dry_pending_applying": "Comprobando configuración que podría haber sido aplicada al servicio '{service}'...", + "service_regenconf_dry_pending_applying": "Comprobando configuración pendiente que podría haber sido aplicada al servicio '{service}'...", "service_regenconf_failed": "No se puede regenerar la configuración para el servicio(s): {services}", - "service_regenconf_pending_applying": "Aplicando la configuración para el servicio '{service}'...", + "service_regenconf_pending_applying": "Aplicando la configuración pendiente para el servicio '{service}'...", "service_remove_failed": "No se pudo desinstalar el servicio '{service:s}'", "service_removed": "El servicio '{service:s}' ha sido desinstalado", "service_start_failed": "No se pudo iniciar el servicio '{service:s}'", @@ -219,7 +219,7 @@ "unit_unknown": "Unidad desconocida '{unit:s}'", "unlimit": "Sin cuota", "unrestore_app": "La aplicación '{app:s}' no será restaurada", - "update_cache_failed": "No se pudo actualizar la caché APT", + "update_cache_failed": "No se pudo actualizar la caché de APT", "updating_apt_cache": "Actualizando lista de paquetes disponibles...", "upgrade_complete": "Actualización finalizada", "upgrading_packages": "Actualizando paquetes...", @@ -240,5 +240,31 @@ "yunohost_ca_creation_failed": "No se pudo crear el certificado de autoridad", "yunohost_configured": "YunoHost ha sido configurado", "yunohost_installing": "Instalando YunoHost...", - "yunohost_not_installed": "YunoHost no está instalado o ha habido errores en la instalación. Ejecute 'yunohost tools postinstall'." + "yunohost_not_installed": "YunoHost no está instalado o ha habido errores en la instalación. Ejecute 'yunohost tools postinstall'", + "ldap_init_failed_to_create_admin": "La inicialización de LDAP falló al crear el usuario administrador", + "mailbox_used_space_dovecot_down": "El servicio de e-mail Dovecot debe estar funcionando si desea obtener el espacio utilizado por el buzón de correo", + "ssowat_persistent_conf_read_error": "Error al leer la configuración persistente de SSOwat: {error:s}. Edite el archivo /etc/ssowat/conf.json.persistent para corregir la sintaxis de JSON", + "ssowat_persistent_conf_write_error": "Error al guardar la configuración persistente de SSOwat: {error:s}. Edite el archivo /etc/ssowat/conf.json.persistent para corregir la sintaxis de JSON", + "certmanager_attempt_to_replace_valid_cert": "Está intentando sobrescribir un certificado correcto y válido para el dominio {domain:s}! (Use --force para omitir este mensaje)", + "certmanager_domain_unknown": "Dominio desconocido {domain:s}", + "certmanager_domain_cert_not_selfsigned": "El certificado para el dominio {domain:s} no es un certificado autofirmado. ¿Está seguro de que quiere reemplazarlo? (Use --force para omitir este mensaje)", + "certmanager_certificate_fetching_or_enabling_failed": "Parece que al habilitar el nuevo certificado para el dominio {domain:s} ha fallado de alguna manera...", + "certmanager_attempt_to_renew_nonLE_cert": "El certificado para el dominio {domain:s} no ha sido emitido por Let's Encrypt. ¡No se puede renovar automáticamente!", + "certmanager_attempt_to_renew_valid_cert": "El certificado para el dominio {domain:s} no está a punto de expirar! Utilice --force para omitir este mensaje", + "certmanager_domain_http_not_working": "Parece que no se puede acceder al dominio {domain:s} a través de HTTP. Compruebe que la configuración del DNS y de nginx es correcta", + "certmanager_error_no_A_record": "No se ha encontrado un registro DNS 'A' para el dominio {domain:s}. Debe hacer que su nombre de dominio apunte a su máquina para poder instalar un certificado Let's Encrypt. (Si sabe lo que está haciendo, use --no-checks para desactivar esas comprobaciones.)", + "certmanager_domain_dns_ip_differs_from_public_ip": "El registro DNS 'A' para el dominio {domain:s} es diferente de la IP de este servidor. Si recientemente modificó su registro A, espere a que se propague (existen algunos controladores de propagación DNS disponibles en línea). (Si sabe lo que está haciendo, use --no-checks para desactivar esas comprobaciones.)", + "certmanager_cannot_read_cert": "Se ha producido un error al intentar abrir el certificado actual para el dominio {domain:s} (archivo: {file:s}), razón: {reason:s}", + "certmanager_cert_install_success_selfsigned": "¡Se ha instalado correctamente un certificado autofirmado para el dominio {domain:s}!", + "certmanager_cert_install_success": "¡Se ha instalado correctamente un certificado Let's Encrypt para el dominio {domain:s}!", + "certmanager_cert_renew_success": "¡Se ha renovado correctamente el certificado Let's Encrypt para el dominio {domain:s}!", + "certmanager_old_letsencrypt_app_detected": "\nYunohost ha detectado que la aplicación 'letsencrypt' está instalada, esto produce conflictos con las nuevas funciones de administración de certificados integradas en Yunohost. Si desea utilizar las nuevas funciones integradas, ejecute los siguientes comandos para migrar su instalación:\n\n Yunohost app remove letsencrypt\n Yunohost domain cert-install\n\nP.D.: esto intentará reinstalar los certificados para todos los dominios con un certificado Let's Encrypt o con un certificado autofirmado", + "certmanager_hit_rate_limit": "Se han emitido demasiados certificados recientemente para el conjunto de dominios {domain:s}. Por favor, inténtelo de nuevo más tarde. Consulte https://letsencrypt.org/docs/rate-limits/ para obtener más detalles", + "certmanager_cert_signing_failed": "No se pudo firmar el nuevo certificado", + "certmanager_no_cert_file": "No se puede leer el certificado para el dominio {domain:s} (archivo: {file:s})", + "certmanager_conflicting_nginx_file": "No se puede preparar el dominio para el desafío ACME: el archivo de configuración nginx {filepath:s} está en conflicto y debe ser eliminado primero", + "domain_cannot_remove_main": "No se puede eliminar el dominio principal. Primero debe establecer un nuevo dominio principal", + "certmanager_self_ca_conf_file_not_found": "No se ha encontrado el archivo de configuración para la autoridad de autofirma (file: {file:s})", + "certmanager_unable_to_parse_self_CA_name": "No se puede procesar el nombre de la autoridad de autofirma (file: {file:s} 1)", + "domains_available": "Dominios instalados:" } diff --git a/locales/fr.json b/locales/fr.json index 7898de57f..6f1369b7a 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -59,7 +59,7 @@ "backup_hook_unknown": "Script de sauvegarde « {hook:s} » inconnu", "backup_invalid_archive": "Archive de sauvegarde incorrecte", "backup_nothings_done": "Il n'y a rien à sauvegarder", - "backup_output_directory_forbidden": "Dossier de sortie interdit. Les sauvegardes ne peuvent être créées dans les dossiers /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var ou /home/yunohost.backup/archives.", + "backup_output_directory_forbidden": "Dossier de destination interdit. Les sauvegardes ne peuvent être créées dans les dossiers /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var ou /home/yunohost.backup/archives", "backup_output_directory_not_empty": "Le dossier de sortie n'est pas vide", "backup_output_directory_required": "Vous devez spécifier un dossier de sortie pour la sauvegarde", "backup_running_app_script": "Lancement du script de sauvegarde de l'application « {app:s} »...", @@ -82,11 +82,11 @@ "domain_dyndns_invalid": "Domaine incorrect pour un usage avec DynDNS", "domain_dyndns_root_unknown": "Domaine DynDNS principal inconnu", "domain_exists": "Le domaine existe déjà", - "domain_uninstall_app_first": "Une ou plusieurs applications sont installées sur ce domaine. Veuillez d'abord les désinstaller avant de supprimer ce domaine.", + "domain_uninstall_app_first": "Une ou plusieurs applications sont installées sur ce domaine. Veuillez d'abord les désinstaller avant de supprimer ce domaine", "domain_unknown": "Domaine inconnu", "domain_zone_exists": "Le fichier de zone DNS existe déjà", "domain_zone_not_found": "Fichier de zone DNS introuvable pour le domaine {:s}", - "done": "Terminé.", + "done": "Terminé", "downloading": "Téléchargement...", "dyndns_cron_installed": "La tâche cron pour le domaine DynDNS a été installée", "dyndns_cron_remove_failed": "Impossible d'enlever la tâche cron pour le domaine DynDNS", @@ -105,23 +105,23 @@ "field_invalid": "Champ incorrect : « {:s} »", "firewall_reload_failed": "Impossible de recharger le pare-feu", "firewall_reloaded": "Le pare-feu a été rechargé", - "firewall_rules_cmd_failed": "Certaines règles du pare-feu n'ont pas pu être appliquées. Pour plus d'informations, consultez le journal.", + "firewall_rules_cmd_failed": "Certaines règles du pare-feu n'ont pas pu être appliquées. Pour plus d'informations, consultez le journal", "format_datetime_short": "%d/%m/%Y %H:%M", "hook_argument_missing": "Argument manquant : '{:s}'", "hook_choice_invalid": "Choix incorrect : '{:s}'", "hook_exec_failed": "Échec de l'exécution du script « {path:s} »", "hook_exec_not_terminated": "L'exécution du script « {path:s} » ne s'est pas terminée", - "hook_list_by_invalid": "Propriété pour lister les scripts incorrecte", + "hook_list_by_invalid": "Propriété pour lister les scripts incorrects", "hook_name_unknown": "Nom de script « {name:s} » inconnu", "installation_complete": "Installation terminée", "installation_failed": "Échec de l'installation", - "ip6tables_unavailable": "Vous ne pouvez pas jouer avec ip6tables ici. Vous êtes sûrement dans un conteneur, ou alors votre noyau ne le supporte pas.", - "iptables_unavailable": "Vous ne pouvez pas jouer avec iptables ici. Vous êtes sûrement dans un conteneur, autrement votre noyau ne le supporte pas.", + "ip6tables_unavailable": "Vous ne pouvez pas jouer avec ip6tables ici. Vous êtes soit dans un conteneur, soit votre noyau ne le supporte pas", + "iptables_unavailable": "Vous ne pouvez pas jouer avec iptables ici. Vous êtes soit dans un conteneur, soit votre noyau ne le supporte pas", "ldap_initialized": "L'annuaire LDAP a été initialisé", "license_undefined": "indéfinie", - "mail_alias_remove_failed": "Impossible de supprimer l'adresse courriel supplémentaire « {mail:s} »", - "mail_domain_unknown": "Le domaine « {domain:s} » de l'adresse courriel est inconnu", - "mail_forward_remove_failed": "Impossible de supprimer l'adresse courriel de transfert « {mail:s} »", + "mail_alias_remove_failed": "Impossible de supprimer l'alias courriel « {mail:s} »", + "mail_domain_unknown": "Le domaine « {domain:s} » du courriel est inconnu", + "mail_forward_remove_failed": "Impossible de supprimer le courriel de transfert « {mail:s} »", "maindomain_change_failed": "Impossible de modifier le domaine principal", "maindomain_changed": "Le domaine principal a été modifié", "monitor_disabled": "La supervision du serveur a été désactivé", @@ -155,7 +155,7 @@ "path_removal_failed": "Impossible de supprimer le chemin {:s}", "pattern_backup_archive_name": "Doit être un nom de fichier valide composé de caractères alphanumérique et -_. uniquement", "pattern_domain": "Doit être un nom de domaine valide (ex : mon-domaine.org)", - "pattern_email": "Doit être une adresse courriel valide (ex. : someone@domain.org)", + "pattern_email": "Doit être une adresse courriel valide (ex. : pseudo@domain.org)", "pattern_firstname": "Doit être un prénom valide", "pattern_lastname": "Doit être un nom valide", "pattern_listname": "Doit être composé uniquement de caractères alphanumériques et de tirets bas", @@ -248,5 +248,36 @@ "yunohost_ca_creation_failed": "Impossible de créer l'autorité de certification", "yunohost_configured": "YunoHost a été configuré", "yunohost_installing": "Installation de YunoHost...", - "yunohost_not_installed": "YunoHost n'est pas ou pas correctement installé. Veuillez exécuter « yunohost tools postinstall »." + "yunohost_not_installed": "YunoHost n'est pas ou pas correctement installé. Veuillez exécuter « yunohost tools postinstall »", + "certmanager_attempt_to_replace_valid_cert": "Vous êtes en train de remplacer un certificat correct et valide pour le domaine {domain:s} ! (Utilisez --force pour contourner)", + "certmanager_domain_unknown": "Domaine inconnu {domain:s}", + "certmanager_domain_cert_not_selfsigned": "Le certificat du domaine {domain:s} n’est pas auto-signé. Voulez-vous vraiment le remplacer ? (Utilisez --force)", + "certmanager_certificate_fetching_or_enabling_failed": "Il semble que l'activation du nouveau certificat pour {domain:s} a échoué…", + "certmanager_attempt_to_renew_nonLE_cert": "Le certificat pour le domaine {domain:s} n’est pas fourni par Let’s Encrypt. Impossible de le renouveler automatiquement !", + "certmanager_attempt_to_renew_valid_cert": "Le certificat pour le domaine {domain:s} est sur le point d’expirer ! Utilisez --force pour contourner", + "certmanager_domain_http_not_working": "Il semble que le domaine {domain:s} n’est pas accessible via HTTP. Veuillez vérifier que vos configuration DNS et nginx sont correctes", + "certmanager_error_no_A_record": "Aucun enregistrement DNS « A » n’a été trouvé pour {domain:s}. De devez faire pointer votre nom de domaine vers votre machine pour être capable d’installer un certificat Let’s Encrypt ! (Si vous savez ce que vous faites, utilisez --no-checks pour désactiver ces contrôles)", + "certmanager_domain_dns_ip_differs_from_public_ip": "L’enregistrement DNS « A » du domaine {domain:s} est différent de l’adresse IP de ce serveur. Si vous avez modifié récemment votre enregistrement « A », veuillez attendre sa propagation (quelques vérificateur de propagation sont disponibles en ligne). (Si vous savez ce que vous faites, utilisez --no-checks pour désactiver ces contrôles)", + "certmanager_cannot_read_cert": "Quelque chose s’est mal passé lors de la tentative d’ouverture du certificat actuel pour le domaine {domain:s} (fichier : {file:s}), cause : {reason:s}", + "certmanager_cert_install_success_selfsigned": "Installation avec succès d’un certificat auto-signé pour le domaine {domain:s} !", + "certmanager_cert_install_success": "Installation avec succès d’un certificat Let’s Encrypt pour le domaine {domain:s} !", + "certmanager_cert_renew_success": "Renouvellement avec succès d’un certificat Let’s Encrypt pour le domaine {domain:s} !", + "certmanager_old_letsencrypt_app_detected": "\nYunoHost a détecté que l’application « letsencrypt » est installé, ce qui est en conflit avec les nouvelles fonctionnalités de gestion intégrée de certificats dans YunoHost. Si vous souhaitez utiliser ces nouvelles fonctionnalités intégrées, veuillez lancer les commandes suivantes pour migrer votre installation :\n\n yunohost app remove letsencrypt\n yunohost domain cert-install\n\nN.B. : cela tentera de réinstaller les certificats de tous les domaines avec un certificat Let's Encrypt ou ceux auto-signés", + "certmanager_cert_signing_failed": "La signature du nouveau certificat a échoué", + "certmanager_no_cert_file": "Impossible de lire le fichier de certificat pour le domaine {domain:s} (fichier : {file:s})", + "certmanager_conflicting_nginx_file": "Impossible de préparer le domaine pour de défi ACME : le fichier de configuration nginx {filepath:s} est en conflit et doit être retiré au préalable", + "certmanager_hit_rate_limit": "Trop de certificats ont déjà été demandés récemment pour cet ensemble précis de domaines {domain:s}. Veuillez réessayer plus tard. Lisez https://letsencrypt.org/docs/rate-limits/ pour obtenir plus de détails", + "ldap_init_failed_to_create_admin": "L’initialisation de LDAP n’a pas réussi à créer l’utilisateur admin", + "ssowat_persistent_conf_read_error": "Erreur lors de la lecture de la configuration persistante de SSOwat : {error:s}. Modifiez le fichier /etc/ssowat/conf.json.persistent pour réparer la syntaxe JSON", + "ssowat_persistent_conf_write_error": "Erreur lors de la sauvegarde de la configuration persistante de SSOwat : {error:s}. Modifiez le fichier /etc/ssowat/conf.json.persistent pour réparer la syntaxe JSON", + "domain_cannot_remove_main": "Impossible de retirer le domaine principal. Définissez un nouveau domaine principal au préalable.", + "certmanager_self_ca_conf_file_not_found": "Le fichier de configuration pour l’autorité du certificat auto-signé est introuvable (fichier : {file:s})", + "certmanager_unable_to_parse_self_CA_name": "Impossible d’analyser le nom de l’autorité du certificat auto-signé (fichier : {file:s})", + "mailbox_used_space_dovecot_down": "Le service de mail Dovecot doit être démarré, si vous souhaitez voir l'espace disque occupé par la messagerie", + "domains_available": "Domaines disponibles :", + "backup_archive_broken_link": "Impossible d'accéder à l'archive de la sauvegarde (lien cassé à {path:s})", + "certmanager_acme_not_configured_for_domain": "Le certificat du domaine {domain:s} ne semble pas être correctement installé. Veuillez préalablement exécuter cert-install pour ce domaine.", + "certmanager_domain_not_resolved_locally": "Le domaine {domain:s} ne peut être déterminé depuis votre serveur YunoHost. Cela peut arriver si vous avez récemment modifié votre enregistrement DNS. Auquel cas, merci d’attendre quelques heures qu’il se propage. Si le problème persiste, envisager d’ajouter {domain:s} au fichier /etc/hosts. (Si vous savez ce que vous faites, utilisez --no-checks pour désactiver ces vérifications.)", + "certmanager_http_check_timeout": "Expiration du délai lors de la tentative du serveur de se contacter via HTTP en utilisant son adresse IP publique (domaine {domain:s} avec l’IP {ip:s}). Vous rencontrez peut-être un problème d’hairpinning ou alors le pare-feu/routeur en amont de votre serveur est mal configuré.", + "certmanager_couldnt_fetch_intermediate_cert": "Expiration du délai lors de la tentative de récupération du certificat intermédiaire depuis Let’s Encrypt. L’installation/le renouvellement du certificat a été interrompu - veuillez réessayer prochainement." } diff --git a/locales/hi.json b/locales/hi.json index 0967ef424..2ed328282 100644 --- a/locales/hi.json +++ b/locales/hi.json @@ -1 +1,81 @@ -{} +{ + "action_invalid": "अवैध कार्रवाई '{action:s}'", + "admin_password": "व्यवस्थापक पासवर्ड", + "admin_password_change_failed": "पासवर्ड बदलने में असमर्थ", + "admin_password_changed": "व्यवस्थापक पासवर्ड बदल दिया गया है", + "app_already_installed": "'{app:s}' पहले से ही इंस्टाल्ड है", + "app_argument_choice_invalid": "गलत तर्क का चयन किया गया '{name:s}' , तर्क इन विकल्पों में से होने चाहिए {choices:s}", + "app_argument_invalid": "तर्क के लिए अमान्य मान '{name:s}': {error:s}", + "app_argument_required": "तर्क '{name:s}' की आवश्यकता है", + "app_extraction_failed": "इन्सटाल्ड फ़ाइलों को निकालने में असमर्थ", + "app_id_invalid": "अवैध एप्लिकेशन id", + "app_incompatible": "यह एप्लिकेशन युनोहोस्ट की इस वर्जन के लिए नहीं है", + "app_install_files_invalid": "फाइलों की अमान्य स्थापना", + "app_location_already_used": "इस लोकेशन पे पहले से ही कोई एप्लीकेशन इन्सटाल्ड है", + "app_location_install_failed": "इस लोकेशन पे एप्लीकेशन इंस्टाल करने में असमर्थ", + "app_manifest_invalid": "एप्लीकेशन का मैनिफेस्ट अमान्य", + "app_no_upgrade": "कोई भी एप्लीकेशन को अपडेट की जरूरत नहीं", + "app_not_correctly_installed": "{app:s} ठीक ढंग से इनस्टॉल नहीं हुई", + "app_not_installed": "{app:s} इनस्टॉल नहीं हुई", + "app_not_properly_removed": "{app:s} ठीक ढंग से नहीं अनइन्सटॉल की गई", + "app_package_need_update": "इस एप्लीकेशन पैकेज को युनोहोस्ट के नए बदलावों/गाइडलिनेज़ के कारण उपडटेशन की जरूरत", + "app_removed": "{app:s} को अनइन्सटॉल कर दिया गया", + "app_requirements_checking": "जरूरी पैकेजेज़ की जाँच हो रही है ....", + "app_requirements_failed": "आवश्यकताओं को पूरा करने में असमर्थ: {error}", + "app_requirements_unmeet": "आवश्यकताए पूरी नहीं हो सकी, पैकेज {pkgname}({version})यह होना चाहिए {spec}", + "app_sources_fetch_failed": "सोर्स फाइल्स प्राप्त करने में असमर्थ", + "app_unknown": "अनजान एप्लीकेशन", + "app_unsupported_remote_type": "एप्लीकेशन के लिए उन्सुपपोर्टेड रिमोट टाइप इस्तेमाल किया गया", + "app_upgrade_failed": "{app:s} अपडेट करने में असमर्थ", + "app_upgraded": "{app:s} अपडेट हो गयी हैं", + "appslist_fetched": "एप्लीकेशन की सूचि अपडेट हो गयी", + "appslist_removed": "एप्लीकेशन की सूचि निकल दी गयी है", + "appslist_retrieve_error": "दूरस्थ एप्लिकेशन सूची प्राप्त करने में असमर्थ", + "appslist_unknown": "अनजान एप्लिकेशन सूची", + "ask_current_admin_password": "वर्तमान व्यवस्थापक पासवर्ड", + "ask_email": "ईमेल का पता", + "ask_firstname": "नाम", + "ask_lastname": "अंतिम नाम", + "ask_list_to_remove": "सूचि जिसको हटाना है", + "ask_main_domain": "मुख्य डोमेन", + "ask_new_admin_password": "नया व्यवस्थापक पासवर्ड", + "ask_password": "पासवर्ड", + "backup_action_required": "आप को सेव करने के लिए कुछ लिखना होगा", + "backup_app_failed": "एप्लीकेशन का बैकअप करने में असमर्थ '{app:s}'", + "backup_archive_app_not_found": "'{app:s}' बैकअप आरचिव में नहीं मिला", + "backup_archive_hook_not_exec": "हुक '{hook:s}' इस बैकअप में एक्सेक्युट नहीं किया गया", + "backup_archive_name_exists": "इस बैकअप आरचिव का नाम पहले से ही मौजूद है", + "backup_archive_name_unknown": "'{name:s}' इस नाम की लोकल बैकअप आरचिव मौजूद नहीं", + "backup_archive_open_failed": "बैकअप आरचिव को खोलने में असमर्थ", + "backup_cleaning_failed": "टेम्पोरेरी बैकअप डायरेक्टरी को उड़ने में असमर्थ", + "backup_created": "बैकअप सफलतापूर्वक किया गया", + "backup_creating_archive": "बैकअप आरचिव बनाई जा रही है ...", + "backup_creation_failed": "बैकअप बनाने में विफल", + "backup_delete_error": "'{path:s}' डिलीट करने में असमर्थ", + "backup_deleted": "इस बैकअप को डिलीट दिया गया है", + "backup_extracting_archive": "बैकअप आरचिव को एक्सट्रेक्ट किया जा रहा है ...", + "backup_hook_unknown": "'{hook:s}' यह बैकअप हुक नहीं मिला", + "backup_invalid_archive": "अवैध बैकअप आरचिव", + "backup_nothings_done": "सेव करने के लिए कुछ नहीं", + "backup_output_directory_forbidden": "निषिद्ध आउटपुट डायरेक्टरी। निम्न दिए गए डायरेक्टरी में बैकअप नहीं बन सकता /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var और /home/yunohost.backup/archives के सब-फोल्डर।", + "backup_output_directory_not_empty": "आउटपुट डायरेक्टरी खाली नहीं है", + "backup_output_directory_required": "बैकअप करने के लिए आउट पुट डायरेक्टरी की आवश्यकता है", + "backup_running_app_script": "'{app:s}' एप्लीकेशन की बैकअप स्क्रिप्ट चल रही है...", + "backup_running_hooks": "बैकअप हुक्स चल रहे है...", + "custom_app_url_required": "आप को अपनी कस्टम एप्लिकेशन '{app:s}' को अपग्रेड करने के लिए यूआरएल(URL) देने की आवश्यकता है", + "custom_appslist_name_required": "आप को अपनी कस्टम एप्लीकेशन के लिए नाम देने की आवश्यकता है", + "diagnostic_debian_version_error": "डेबियन वर्जन प्राप्त करने में असफलता {error}", + "diagnostic_kernel_version_error": "कर्नेल वर्जन प्राप्त नहीं की जा पा रही : {error}", + "diagnostic_monitor_disk_error": "डिस्क की मॉनिटरिंग नहीं की जा पा रही: {error}", + "diagnostic_monitor_network_error": "नेटवर्क की मॉनिटरिंग नहीं की जा पा रही: {error}", + "diagnostic_monitor_system_error": "सिस्टम की मॉनिटरिंग नहीं की जा पा रही: {error}", + "diagnostic_no_apps": "कोई एप्लीकेशन इन्सटाल्ड नहीं है", + "dnsmasq_isnt_installed": "dnsmasq इन्सटाल्ड नहीं लगता,इनस्टॉल करने के लिए किप्या ये कमांड चलाये 'apt-get remove bind9 && apt-get install dnsmasq'", + "domain_cert_gen_failed": "सर्टिफिकेट उत्पन करने में असमर्थ", + "domain_created": "डोमेन बनाया गया", + "domain_creation_failed": "डोमेन बनाने में असमर्थ", + "domain_deleted": "डोमेन डिलीट कर दिया गया है", + "domain_deletion_failed": "डोमेन डिलीट करने में असमर्थ", + "domain_dyndns_already_subscribed": "DynDNS डोमेन पहले ही सब्स्क्राइड है", + "domain_dyndns_invalid": "DynDNS के साथ इनवैलिड डोमिन इस्तेमाल किया गया" +} diff --git a/src/yunohost/app.py b/src/yunohost/app.py index caa38e95b..a35ccb7e6 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -24,10 +24,8 @@ Manage apps """ import os -import sys import json import shutil -import stat import yaml import time import re @@ -101,7 +99,7 @@ def app_fetchlist(url=None, name=None): m18n.n('custom_appslist_name_required')) list_file = '%s/%s.json' % (repo_path, name) - if os.system('wget "%s" -O "%s.tmp"' % (url, list_file)) != 0: + if os.system('wget --timeout=30 "%s" -O "%s.tmp"' % (url, list_file)) != 0: os.remove('%s.tmp' % list_file) raise MoulinetteError(errno.EBADR, m18n.n('appslist_retrieve_error')) @@ -440,7 +438,7 @@ def app_upgrade(auth, app=[], url=None, file=None): logger.success(m18n.n('upgrade_complete')) -def app_install(auth, app, label=None, args=None): +def app_install(auth, app, label=None, args=None, no_remove_on_failure=False): """ Install apps @@ -448,6 +446,7 @@ def app_install(auth, app, label=None, args=None): app -- Name, local path or git URL of the app to install label -- Custom name for the app args -- Serialize arguments for app installation + no_remove_on_failure -- Debug option to avoid removing the app on a failed installation """ from yunohost.hook import hook_add, hook_remove, hook_exec @@ -541,19 +540,20 @@ def app_install(auth, app, label=None, args=None): logger.exception(m18n.n('unexpected_error')) finally: if install_retcode != 0: - # Setup environment for remove script - env_dict_remove = {} - env_dict_remove["YNH_APP_ID"] = app_id - env_dict_remove["YNH_APP_INSTANCE_NAME"] = app_instance_name - env_dict_remove["YNH_APP_INSTANCE_NUMBER"] = str(instance_number) + if not no_remove_on_failure: + # Setup environment for remove script + env_dict_remove = {} + env_dict_remove["YNH_APP_ID"] = app_id + env_dict_remove["YNH_APP_INSTANCE_NAME"] = app_instance_name + env_dict_remove["YNH_APP_INSTANCE_NUMBER"] = str(instance_number) - # Execute remove script - remove_retcode = hook_exec( - os.path.join(extracted_app_folder, 'scripts/remove'), - args=[app_instance_name], env=env_dict_remove) - if remove_retcode != 0: - logger.warning(m18n.n('app_not_properly_removed', - app=app_instance_name)) + # Execute remove script + remove_retcode = hook_exec( + os.path.join(extracted_app_folder, 'scripts/remove'), + args=[app_instance_name], env=env_dict_remove) + if remove_retcode != 0: + logger.warning(m18n.n('app_not_properly_removed', + app=app_instance_name)) # Clean tmp folders shutil.rmtree(app_setting_path) @@ -811,6 +811,9 @@ def app_makedefault(auth, app, domain=None): try: with open('/etc/ssowat/conf.json.persistent') as json_conf: ssowat_conf = json.loads(str(json_conf.read())) + except ValueError as e: + raise MoulinetteError(errno.EINVAL, + m18n.n('ssowat_persistent_conf_read_error', error=e.strerror)) except IOError: ssowat_conf = {} @@ -819,8 +822,13 @@ def app_makedefault(auth, app, domain=None): ssowat_conf['redirected_urls'][domain +'/'] = app_domain + app_path - with open('/etc/ssowat/conf.json.persistent', 'w+') as f: - json.dump(ssowat_conf, f, sort_keys=True, indent=4) + try: + with open('/etc/ssowat/conf.json.persistent', 'w+') as f: + json.dump(ssowat_conf, f, sort_keys=True, indent=4) + except IOError as e: + raise MoulinetteError(errno.EPERM, + m18n.n('ssowat_persistent_conf_write_error', error=e.strerror)) + os.system('chmod 644 /etc/ssowat/conf.json.persistent') @@ -909,10 +917,6 @@ def app_checkurl(auth, url, app=None): raise MoulinetteError(errno.EINVAL, m18n.n('domain_unknown')) if domain in apps_map: - # Domain already has apps on sub path - if path == '/': - raise MoulinetteError(errno.EPERM, - m18n.n('app_location_install_failed')) # Loop through apps for p, a in apps_map[domain].items(): # Skip requested app checking @@ -922,7 +926,7 @@ def app_checkurl(auth, url, app=None): if path == p: raise MoulinetteError(errno.EINVAL, m18n.n('app_location_already_used')) - elif path.startswith(p): + elif path.startswith(p) or p.startswith(path): raise MoulinetteError(errno.EPERM, m18n.n('app_location_install_failed')) @@ -991,7 +995,6 @@ def app_ssowatconf(auth): redirected_regex = { main_domain +'/yunohost[\/]?$': 'https://'+ main_domain +'/yunohost/sso/' } redirected_urls ={} - apps = {} try: apps_list = app_list()['apps'] except: @@ -1008,19 +1011,19 @@ def app_ssowatconf(auth): for item in _get_setting(app_settings, 'skipped_uris'): if item[-1:] == '/': item = item[:-1] - skipped_urls.append(app_settings['domain'] + app_settings['path'][:-1] + item) + skipped_urls.append(app_settings['domain'] + app_settings['path'].rstrip('/') + item) for item in _get_setting(app_settings, 'skipped_regex'): skipped_regex.append(item) for item in _get_setting(app_settings, 'unprotected_uris'): if item[-1:] == '/': item = item[:-1] - unprotected_urls.append(app_settings['domain'] + app_settings['path'][:-1] + item) + unprotected_urls.append(app_settings['domain'] + app_settings['path'].rstrip('/') + item) for item in _get_setting(app_settings, 'unprotected_regex'): unprotected_regex.append(item) for item in _get_setting(app_settings, 'protected_uris'): if item[-1:] == '/': item = item[:-1] - protected_urls.append(app_settings['domain'] + app_settings['path'][:-1] + item) + protected_urls.append(app_settings['domain'] + app_settings['path'].rstrip('/') + item) for item in _get_setting(app_settings, 'protected_regex'): protected_regex.append(item) if 'redirected_urls' in app_settings: @@ -1031,6 +1034,9 @@ def app_ssowatconf(auth): for domain in domains: skipped_urls.extend([domain + '/yunohost/admin', domain + '/yunohost/api']) + # Authorize ACME challenge url + skipped_regex.append("^[^/]*/%.well%-known/acme%-challenge/.*$") + conf_dict = { 'portal_domain': main_domain, 'portal_path': '/yunohost/sso/', @@ -1259,8 +1265,13 @@ def _fetch_app_from_git(app): url = url[:tree_index] branch = app[tree_index+6:] try: + # We use currently git 2.1 so we can't use --shallow-submodules + # option. When git will be in 2.9 (with the new debian version) + # we will be able to use it. Without this option all the history + # of the submodules repo is downloaded. subprocess.check_call([ - 'git', 'clone', '--depth=1', url, extracted_app_folder]) + 'git', 'clone', '--depth=1', '--recursive', url, + extracted_app_folder]) subprocess.check_call([ 'git', 'reset', '--hard', branch ], cwd=extracted_app_folder) @@ -1486,7 +1497,7 @@ def _parse_args_from_manifest(manifest, action, args={}, auth=None): args -- A dictionnary of arguments to parse """ - from yunohost.domain import domain_list + from yunohost.domain import domain_list, _get_maindomain from yunohost.user import user_info args_dict = OrderedDict() @@ -1526,6 +1537,13 @@ def _parse_args_from_manifest(manifest, action, args={}, auth=None): # Check for a password argument is_password = True if arg_type == 'password' else False + if arg_type == 'domain': + arg_default = _get_maindomain() + ask_string += ' (default: {0})'.format(arg_default) + msignals.display(m18n.n('domains_available')) + for domain in domain_list(auth)['domains']: + msignals.display("- {}".format(domain)) + try: input_string = msignals.prompt(ask_string, is_password) except NotImplementedError: diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index 32dda58a1..587da31c7 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -25,7 +25,6 @@ """ import os import re -import sys import json import errno import time @@ -48,7 +47,7 @@ from yunohost.hook import ( from yunohost.monitor import binary_to_human from yunohost.tools import tools_postinstall -backup_path = '/home/yunohost.backup' +backup_path = '/home/yunohost.backup' archives_path = '%s/archives' % backup_path logger = getActionLogger('yunohost.backup') @@ -120,8 +119,10 @@ def backup_create(name=None, description=None, output_directory=None, env_var['CAN_BIND'] = 0 else: output_directory = archives_path - if not os.path.isdir(archives_path): - os.mkdir(archives_path, 0750) + + # Create archives directory if it does not exists + if not os.path.isdir(archives_path): + os.mkdir(archives_path, 0750) def _clean_tmp_dir(retcode=0): ret = hook_callback('post_backup_create', args=[tmp_dir, retcode]) @@ -288,7 +289,7 @@ def backup_create(name=None, description=None, output_directory=None, raise MoulinetteError(errno.EIO, m18n.n('backup_archive_open_failed')) - # Add files to the arvhice + # Add files to the archive try: tar.add(tmp_dir, arcname='') tar.close() @@ -298,10 +299,20 @@ def backup_create(name=None, description=None, output_directory=None, raise MoulinetteError(errno.EIO, m18n.n('backup_creation_failed')) + # FIXME : it looks weird that the "move info file" is not enabled if + # user activated "no_compress" ... or does it really means + # "dont_keep_track_of_this_backup_in_history" ? + # Move info file - os.rename(tmp_dir + '/info.json', + shutil.move(tmp_dir + '/info.json', '{:s}/{:s}.info.json'.format(archives_path, name)) + # If backuped to a non-default location, keep a symlink of the archive + # to that location + if output_directory != archives_path: + link = "%s/%s.tar.gz" % (archives_path, name) + os.symlink(archive_file, link) + # Clean temporary directory if tmp_dir != output_directory: _clean_tmp_dir() @@ -310,7 +321,7 @@ def backup_create(name=None, description=None, output_directory=None, # Return backup info info['name'] = name - return { 'archive': info } + return {'archive': info} def backup_restore(auth, name, hooks=[], ignore_hooks=False, @@ -588,7 +599,7 @@ def backup_list(with_info=False, human_readable=False): d[a] = backup_info(a, human_readable=human_readable) result = d - return { 'archives': result } + return {'archives': result} def backup_info(name, with_details=False, human_readable=False): @@ -602,11 +613,23 @@ def backup_info(name, with_details=False, human_readable=False): """ archive_file = '%s/%s.tar.gz' % (archives_path, name) - if not os.path.isfile(archive_file): + + # Check file exist (even if it's a broken symlink) + if not os.path.lexists(archive_file): raise MoulinetteError(errno.EIO, m18n.n('backup_archive_name_unknown', name=name)) + # If symlink, retrieve the real path + if os.path.islink(archive_file): + archive_file = os.path.realpath(archive_file) + + # Raise exception if link is broken (e.g. on unmounted external storage) + if not os.path.exists(archive_file): + raise MoulinetteError(errno.EIO, + m18n.n('backup_archive_broken_link', path=archive_file)) + info_file = "%s/%s.info.json" % (archives_path, name) + try: with open(info_file) as f: # Retrieve backup info @@ -620,7 +643,7 @@ def backup_info(name, with_details=False, human_readable=False): size = info.get('size', 0) if not size: tar = tarfile.open(archive_file, "r:gz") - size = reduce(lambda x,y: getattr(x, 'size', x)+getattr(y, 'size', y), + size = reduce(lambda x, y: getattr(x, 'size', x) + getattr(y, 'size', y), tar.getmembers()) tar.close() if human_readable: @@ -653,7 +676,7 @@ def backup_delete(name): archive_file = '%s/%s.tar.gz' % (archives_path, name) info_file = "%s/%s.info.json" % (archives_path, name) - for backup_file in [archive_file,info_file]: + for backup_file in [archive_file, info_file]: if not os.path.isfile(backup_file): raise MoulinetteError(errno.EIO, m18n.n('backup_archive_name_unknown', name=backup_file)) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py new file mode 100644 index 000000000..bd7d02962 --- /dev/null +++ b/src/yunohost/certificate.py @@ -0,0 +1,893 @@ +# -*- coding: utf-8 -*- + +""" License + + Copyright (C) 2016 YUNOHOST.ORG + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program; if not, see http://www.gnu.org/licenses + + yunohost_certificate.py + + Manage certificates, in particular Let's encrypt +""" + +import os +import sys +import errno +import shutil +import pwd +import grp +import smtplib +import requests +import subprocess +import socket +import dns.resolver +import glob + +from OpenSSL import crypto +from datetime import datetime +from requests.exceptions import Timeout + +from yunohost.vendor.acme_tiny.acme_tiny import get_crt as sign_certificate + +from moulinette.core import MoulinetteError +from moulinette.utils.log import getActionLogger + +import yunohost.domain + +from yunohost.app import app_ssowatconf +from yunohost.service import _run_service_command + + +logger = getActionLogger('yunohost.certmanager') + +CERT_FOLDER = "/etc/yunohost/certs/" +TMP_FOLDER = "/tmp/acme-challenge-private/" +WEBROOT_FOLDER = "/tmp/acme-challenge-public/" + +SELF_CA_FILE = "/etc/ssl/certs/ca-yunohost_crt.pem" +ACCOUNT_KEY_FILE = "/etc/yunohost/letsencrypt_account.pem" + +SSL_DIR = '/usr/share/yunohost/yunohost-config/ssl/yunoCA' + +KEY_SIZE = 3072 + +VALIDITY_LIMIT = 15 # days + +# For tests +STAGING_CERTIFICATION_AUTHORITY = "https://acme-staging.api.letsencrypt.org" +# For prod +PRODUCTION_CERTIFICATION_AUTHORITY = "https://acme-v01.api.letsencrypt.org" + +INTERMEDIATE_CERTIFICATE_URL = "https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem" + +DNS_RESOLVERS = [ + # FFDN DNS resolvers + # See https://www.ffdn.org/wiki/doku.php?id=formations:dns + "80.67.169.12", # FDN + "80.67.169.40", # + "89.234.141.66", # ARN + "141.255.128.100", # Aquilenet + "141.255.128.101", + "89.234.186.18", # Grifon + "80.67.188.188" # LDN +] + +############################################################################### +# Front-end stuff # +############################################################################### + + +def certificate_status(auth, domain_list, full=False): + """ + Print the status of certificate for given domains (all by default) + + Keyword argument: + domain_list -- Domains to be checked + full -- Display more info about the certificates + """ + + # Check if old letsencrypt_ynh is installed + # TODO / FIXME - Remove this in the future once the letsencrypt app is + # not used anymore + _check_old_letsencrypt_app() + + # If no domains given, consider all yunohost domains + if domain_list == []: + domain_list = yunohost.domain.domain_list(auth)['domains'] + # Else, validate that yunohost knows the domains given + else: + yunohost_domains_list = yunohost.domain.domain_list(auth)['domains'] + for domain in domain_list: + # Is it in Yunohost domain list? + if domain not in yunohost_domains_list: + raise MoulinetteError(errno.EINVAL, m18n.n( + 'certmanager_domain_unknown', domain=domain)) + + certificates = {} + + for domain in domain_list: + status = _get_status(domain) + + if not full: + del status["subject"] + del status["CA_name"] + del status["ACME_eligible"] + status["CA_type"] = status["CA_type"]["verbose"] + status["summary"] = status["summary"]["verbose"] + + del status["domain"] + certificates[domain] = status + + return {"certificates": certificates} + + +def certificate_install(auth, domain_list, force=False, no_checks=False, self_signed=False, staging=False): + """ + Install a Let's Encrypt certificate for given domains (all by default) + + Keyword argument: + domain_list -- Domains on which to install certificates + force -- Install even if current certificate is not self-signed + no-check -- Disable some checks about the reachability of web server + before attempting the install + self-signed -- Instal self-signed certificates instead of Let's Encrypt + """ + + # Check if old letsencrypt_ynh is installed + # TODO / FIXME - Remove this in the future once the letsencrypt app is + # not used anymore + _check_old_letsencrypt_app() + + if self_signed: + _certificate_install_selfsigned(domain_list, force) + else: + _certificate_install_letsencrypt( + auth, domain_list, force, no_checks, staging) + + +def _certificate_install_selfsigned(domain_list, force=False): + + for domain in domain_list: + + # Paths of files and folder we'll need + date_tag = datetime.now().strftime("%Y%m%d.%H%M%S") + new_cert_folder = "%s/%s-history/%s-selfsigned" % ( + CERT_FOLDER, domain, date_tag) + + conf_template = os.path.join(SSL_DIR, "openssl.cnf") + + csr_file = os.path.join(SSL_DIR, "certs", "yunohost_csr.pem") + conf_file = os.path.join(new_cert_folder, "openssl.cnf") + key_file = os.path.join(new_cert_folder, "key.pem") + crt_file = os.path.join(new_cert_folder, "crt.pem") + ca_file = os.path.join(new_cert_folder, "ca.pem") + + # Check we ain't trying to overwrite a good cert ! + current_cert_file = os.path.join(CERT_FOLDER, domain, "crt.pem") + if not force and os.path.isfile(current_cert_file): + status = _get_status(domain) + + if status["summary"]["code"] in ('good', 'great'): + raise MoulinetteError(errno.EINVAL, m18n.n( + 'certmanager_attempt_to_replace_valid_cert', domain=domain)) + + # Create output folder for new certificate stuff + os.makedirs(new_cert_folder) + + # Create our conf file, based on template, replacing the occurences of + # "yunohost.org" with the given domain + with open(conf_file, "w") as f, open(conf_template, "r") as template: + for line in template: + f.write(line.replace("yunohost.org", domain)) + + # Use OpenSSL command line to create a certificate signing request, + # and self-sign the cert + commands = [ + "openssl req -new -config %s -days 3650 -out %s -keyout %s -nodes -batch" + % (conf_file, csr_file, key_file), + "openssl ca -config %s -days 3650 -in %s -out %s -batch" + % (conf_file, csr_file, crt_file), + ] + + for command in commands: + p = subprocess.Popen( + command.split(), stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + + out, _ = p.communicate() + + if p.returncode != 0: + logger.warning(out) + raise MoulinetteError( + errno.EIO, m18n.n('domain_cert_gen_failed')) + else: + logger.info(out) + + # Link the CA cert (not sure it's actually needed in practice though, + # since we append it at the end of crt.pem. For instance for Let's + # Encrypt certs, we only need the crt.pem and key.pem) + os.symlink(SELF_CA_FILE, ca_file) + + # Append ca.pem at the end of crt.pem + with open(ca_file, "r") as ca_pem, open(crt_file, "a") as crt_pem: + crt_pem.write("\n") + crt_pem.write(ca_pem.read()) + + # Set appropriate permissions + _set_permissions(new_cert_folder, "root", "root", 0755) + _set_permissions(key_file, "root", "metronome", 0640) + _set_permissions(crt_file, "root", "metronome", 0640) + _set_permissions(conf_file, "root", "root", 0600) + + # Actually enable the certificate we created + _enable_certificate(domain, new_cert_folder) + + # Check new status indicate a recently created self-signed certificate + status = _get_status(domain) + + if status and status["CA_type"]["code"] == "self-signed" and status["validity"] > 3648: + logger.success( + m18n.n("certmanager_cert_install_success_selfsigned", domain=domain)) + else: + logger.error( + "Installation of self-signed certificate installation for %s failed !", domain) + + +def _certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=False, staging=False): + if not os.path.exists(ACCOUNT_KEY_FILE): + _generate_account_key() + + # If no domains given, consider all yunohost domains with self-signed + # certificates + if domain_list == []: + for domain in yunohost.domain.domain_list(auth)['domains']: + + status = _get_status(domain) + if status["CA_type"]["code"] != "self-signed": + continue + + domain_list.append(domain) + + # Else, validate that yunohost knows the domains given + else: + for domain in domain_list: + yunohost_domains_list = yunohost.domain.domain_list(auth)['domains'] + if domain not in yunohost_domains_list: + raise MoulinetteError(errno.EINVAL, m18n.n( + 'certmanager_domain_unknown', domain=domain)) + + # Is it self-signed? + status = _get_status(domain) + if not force and status["CA_type"]["code"] != "self-signed": + raise MoulinetteError(errno.EINVAL, m18n.n( + 'certmanager_domain_cert_not_selfsigned', domain=domain)) + + if staging: + logger.warning( + "Please note that you used the --staging option, and that no new certificate will actually be enabled !") + + # Actual install steps + for domain in domain_list: + + logger.info( + "Now attempting install of certificate for domain %s!", domain) + + try: + if not no_checks: + _check_domain_is_ready_for_ACME(domain) + + _configure_for_acme_challenge(auth, domain) + _fetch_and_enable_new_certificate(domain, staging) + _install_cron() + + logger.success( + m18n.n("certmanager_cert_install_success", domain=domain)) + + except Exception as e: + logger.error("Certificate installation for %s failed !\nException: %s", domain, e) + + +def certificate_renew(auth, domain_list, force=False, no_checks=False, email=False, staging=False): + """ + Renew Let's Encrypt certificate for given domains (all by default) + + Keyword argument: + domain_list -- Domains for which to renew the certificates + force -- Ignore the validity threshold (15 days) + no-check -- Disable some checks about the reachability of web server + before attempting the renewing + email -- Emails root if some renewing failed + """ + + # Check if old letsencrypt_ynh is installed + # TODO / FIXME - Remove this in the future once the letsencrypt app is + # not used anymore + _check_old_letsencrypt_app() + + # If no domains given, consider all yunohost domains with Let's Encrypt + # certificates + if domain_list == []: + for domain in yunohost.domain.domain_list(auth)['domains']: + + # Does it have a Let's Encrypt cert? + status = _get_status(domain) + if status["CA_type"]["code"] != "lets-encrypt": + continue + + # Does it expire soon? + if status["validity"] > VALIDITY_LIMIT and not force: + continue + + # Check ACME challenge configured for given domain + if not _check_acme_challenge_configuration(domain): + logger.warning(m18n.n( + 'certmanager_acme_not_configured_for_domain', domain=domain)) + continue + + domain_list.append(domain) + + if len(domain_list) == 0: + logger.info("No certificate needs to be renewed.") + + # Else, validate the domain list given + else: + for domain in domain_list: + + # Is it in Yunohost dmomain list? + if domain not in yunohost.domain.domain_list(auth)['domains']: + raise MoulinetteError(errno.EINVAL, m18n.n( + 'certmanager_domain_unknown', domain=domain)) + + status = _get_status(domain) + + # Does it expire soon? + if status["validity"] > VALIDITY_LIMIT and not force: + raise MoulinetteError(errno.EINVAL, m18n.n( + 'certmanager_attempt_to_renew_valid_cert', domain=domain)) + + # Does it have a Let's Encrypt cert? + if status["CA_type"]["code"] != "lets-encrypt": + raise MoulinetteError(errno.EINVAL, m18n.n( + 'certmanager_attempt_to_renew_nonLE_cert', domain=domain)) + + # Check ACME challenge configured for given domain + if not _check_acme_challenge_configuration(domain): + raise MoulinetteError(errno.EINVAL, m18n.n( + 'certmanager_acme_not_configured_for_domain', domain=domain)) + + if staging: + logger.warning( + "Please note that you used the --staging option, and that no new certificate will actually be enabled !") + + # Actual renew steps + for domain in domain_list: + logger.info( + "Now attempting renewing of certificate for domain %s !", domain) + + try: + if not no_checks: + _check_domain_is_ready_for_ACME(domain) + + _fetch_and_enable_new_certificate(domain, staging) + + logger.success( + m18n.n("certmanager_cert_renew_success", domain=domain)) + + except Exception as e: + import traceback + from StringIO import StringIO + stack = StringIO() + traceback.print_exc(file=stack) + logger.error("Certificate renewing for %s failed !", domain) + logger.error(stack.getvalue()) + logger.error(str(e)) + + if email: + logger.error("Sending email with details to root ...") + _email_renewing_failed(domain, e, stack.getvalue()) + + +############################################################################### +# Back-end stuff # +############################################################################### + +def _check_old_letsencrypt_app(): + installedAppIds = [app["id"] for app in yunohost.app.app_list(installed=True)["apps"]] + + if "letsencrypt" not in installedAppIds: + return + + raise MoulinetteError(errno.EINVAL, m18n.n( + 'certmanager_old_letsencrypt_app_detected')) + + +def _install_cron(): + cron_job_file = "/etc/cron.daily/yunohost-certificate-renew" + + with open(cron_job_file, "w") as f: + f.write("#!/bin/bash\n") + f.write("yunohost domain cert-renew --email\n") + + _set_permissions(cron_job_file, "root", "root", 0755) + + +def _email_renewing_failed(domain, exception_message, stack): + from_ = "certmanager@%s (Certificate Manager)" % domain + to_ = "root" + subject_ = "Certificate renewing attempt for %s failed!" % domain + + logs = _tail(50, "/var/log/yunohost/yunohost-cli.log") + text = """ +An attempt for renewing the certificate for domain %s failed with the following +error : + +%s +%s + +Here's the tail of /var/log/yunohost/yunohost-cli.log, which might help to +investigate : + +%s + +-- Certificate Manager + +""" % (domain, exception_message, stack, logs) + + message = """ +From: %s +To: %s +Subject: %s + +%s +""" % (from_, to_, subject_, text) + + smtp = smtplib.SMTP("localhost") + smtp.sendmail(from_, [to_], message) + smtp.quit() + + +def _configure_for_acme_challenge(auth, domain): + + nginx_conf_folder = "/etc/nginx/conf.d/%s.d" % domain + nginx_conf_file = "%s/000-acmechallenge.conf" % nginx_conf_folder + + nginx_configuration = ''' +location '/.well-known/acme-challenge' +{ + default_type "text/plain"; + alias %s; +} + ''' % WEBROOT_FOLDER + + # Check there isn't a conflicting file for the acme-challenge well-known + # uri + for path in glob.glob('%s/*.conf' % nginx_conf_folder): + + if path == nginx_conf_file: + continue + + with open(path) as f: + contents = f.read() + + if '/.well-known/acme-challenge' in contents: + raise MoulinetteError(errno.EINVAL, m18n.n( + 'certmanager_conflicting_nginx_file', filepath=path)) + + # Write the conf + if os.path.exists(nginx_conf_file): + logger.info( + "Nginx configuration file for ACME challenge already exists for domain, skipping.") + return + + logger.info( + "Adding Nginx configuration file for Acme challenge for domain %s.", domain) + + with open(nginx_conf_file, "w") as f: + f.write(nginx_configuration) + + # Assume nginx conf is okay, and reload it + # (FIXME : maybe add a check that it is, using nginx -t, haven't found + # any clean function already implemented in yunohost to do this though) + _run_service_command("reload", "nginx") + + app_ssowatconf(auth) + + +def _check_acme_challenge_configuration(domain): + # Check nginx conf file exists + nginx_conf_folder = "/etc/nginx/conf.d/%s.d" % domain + nginx_conf_file = "%s/000-acmechallenge.conf" % nginx_conf_folder + + if not os.path.exists(nginx_conf_file): + return False + else: + return True + + +def _fetch_and_enable_new_certificate(domain, staging=False): + # Make sure tmp folder exists + logger.debug("Making sure tmp folders exists...") + + if not os.path.exists(WEBROOT_FOLDER): + os.makedirs(WEBROOT_FOLDER) + + if not os.path.exists(TMP_FOLDER): + os.makedirs(TMP_FOLDER) + + _set_permissions(WEBROOT_FOLDER, "root", "www-data", 0650) + _set_permissions(TMP_FOLDER, "root", "root", 0640) + + # Prepare certificate signing request + logger.info( + "Prepare key and certificate signing request (CSR) for %s...", domain) + + domain_key_file = "%s/%s.pem" % (TMP_FOLDER, domain) + _generate_key(domain_key_file) + _set_permissions(domain_key_file, "root", "metronome", 0640) + + _prepare_certificate_signing_request(domain, domain_key_file, TMP_FOLDER) + + # Sign the certificate + logger.info("Now using ACME Tiny to sign the certificate...") + + domain_csr_file = "%s/%s.csr" % (TMP_FOLDER, domain) + + if staging: + certification_authority = STAGING_CERTIFICATION_AUTHORITY + else: + certification_authority = PRODUCTION_CERTIFICATION_AUTHORITY + + try: + signed_certificate = sign_certificate(ACCOUNT_KEY_FILE, + domain_csr_file, + WEBROOT_FOLDER, + log=logger, + CA=certification_authority) + except ValueError as e: + if "urn:acme:error:rateLimited" in str(e): + raise MoulinetteError(errno.EINVAL, m18n.n( + 'certmanager_hit_rate_limit', domain=domain)) + else: + logger.error(str(e)) + raise MoulinetteError(errno.EINVAL, m18n.n( + 'certmanager_cert_signing_failed')) + + except Exception as e: + logger.error(str(e)) + + raise MoulinetteError(errno.EINVAL, m18n.n( + 'certmanager_cert_signing_failed')) + + try: + intermediate_certificate = requests.get(INTERMEDIATE_CERTIFICATE_URL, timeout=30).text + except Timeout as e: + raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_couldnt_fetch_intermediate_cert')) + + # Now save the key and signed certificate + logger.info("Saving the key and signed certificate...") + + # Create corresponding directory + date_tag = datetime.now().strftime("%Y%m%d.%H%M%S") + + if staging: + folder_flag = "staging" + else: + folder_flag = "letsencrypt" + + new_cert_folder = "%s/%s-history/%s-%s" % ( + CERT_FOLDER, domain, date_tag, folder_flag) + + os.makedirs(new_cert_folder) + + _set_permissions(new_cert_folder, "root", "root", 0655) + + # Move the private key + domain_key_file_finaldest = os.path.join(new_cert_folder, "key.pem") + shutil.move(domain_key_file, domain_key_file_finaldest) + _set_permissions(domain_key_file_finaldest, "root", "metronome", 0640) + + # Write the cert + domain_cert_file = os.path.join(new_cert_folder, "crt.pem") + + with open(domain_cert_file, "w") as f: + f.write(signed_certificate) + f.write(intermediate_certificate) + + _set_permissions(domain_cert_file, "root", "metronome", 0640) + + if staging: + return + + _enable_certificate(domain, new_cert_folder) + + # Check the status of the certificate is now good + status_summary = _get_status(domain)["summary"] + + if status_summary["code"] != "great": + raise MoulinetteError(errno.EINVAL, m18n.n( + 'certmanager_certificate_fetching_or_enabling_failed', domain=domain)) + + +def _prepare_certificate_signing_request(domain, key_file, output_folder): + # Init a request + csr = crypto.X509Req() + + # Set the domain + csr.get_subject().CN = domain + + # Set the key + with open(key_file, 'rt') as f: + key = crypto.load_privatekey(crypto.FILETYPE_PEM, f.read()) + + csr.set_pubkey(key) + + # Sign the request + csr.sign(key, "sha256") + + # Save the request in tmp folder + csr_file = output_folder + domain + ".csr" + logger.info("Saving to %s.", csr_file) + + with open(csr_file, "w") as f: + f.write(crypto.dump_certificate_request(crypto.FILETYPE_PEM, csr)) + + +def _get_status(domain): + + cert_file = os.path.join(CERT_FOLDER, domain, "crt.pem") + + if not os.path.isfile(cert_file): + raise MoulinetteError(errno.EINVAL, m18n.n( + 'certmanager_no_cert_file', domain=domain, file=cert_file)) + + try: + cert = crypto.load_certificate( + crypto.FILETYPE_PEM, open(cert_file).read()) + except Exception as exception: + import traceback + traceback.print_exc(file=sys.stdout) + raise MoulinetteError(errno.EINVAL, m18n.n( + 'certmanager_cannot_read_cert', domain=domain, file=cert_file, reason=exception)) + + cert_subject = cert.get_subject().CN + cert_issuer = cert.get_issuer().CN + valid_up_to = datetime.strptime(cert.get_notAfter(), "%Y%m%d%H%M%SZ") + days_remaining = (valid_up_to - datetime.now()).days + + if cert_issuer == _name_self_CA(): + CA_type = { + "code": "self-signed", + "verbose": "Self-signed", + } + + elif cert_issuer.startswith("Let's Encrypt"): + CA_type = { + "code": "lets-encrypt", + "verbose": "Let's Encrypt", + } + + elif cert_issuer.startswith("Fake LE"): + CA_type = { + "code": "fake-lets-encrypt", + "verbose": "Fake Let's Encrypt", + } + + else: + CA_type = { + "code": "other-unknown", + "verbose": "Other / Unknown", + } + + 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", + } + + 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!", + } + + else: + status_summary = { + "code": "unknown", + "verbose": "Unknown?", + } + + try: + _check_domain_is_ready_for_ACME(domain) + ACME_eligible = True + except: + ACME_eligible = False + + return { + "domain": domain, + "subject": cert_subject, + "CA_name": cert_issuer, + "CA_type": CA_type, + "validity": days_remaining, + "summary": status_summary, + "ACME_eligible": ACME_eligible + } + +############################################################################### +# Misc small stuff ... # +############################################################################### + + +def _generate_account_key(): + logger.info("Generating account key ...") + _generate_key(ACCOUNT_KEY_FILE) + _set_permissions(ACCOUNT_KEY_FILE, "root", "root", 0400) + + +def _generate_key(destination_path): + k = crypto.PKey() + k.generate_key(crypto.TYPE_RSA, KEY_SIZE) + + with open(destination_path, "w") as f: + f.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, k)) + + +def _set_permissions(path, user, group, permissions): + uid = pwd.getpwnam(user).pw_uid + gid = grp.getgrnam(group).gr_gid + + os.chown(path, uid, gid) + os.chmod(path, permissions) + + +def _enable_certificate(domain, new_cert_folder): + logger.info("Enabling the certificate for domain %s ...", domain) + + live_link = os.path.join(CERT_FOLDER, domain) + + # If a live link (or folder) already exists + if os.path.exists(live_link): + # If it's not a link ... expect if to be a folder + if not os.path.islink(live_link): + # Backup it and remove it + _backup_current_cert(domain) + shutil.rmtree(live_link) + # Else if it's a link, simply delete it + elif os.path.lexists(live_link): + os.remove(live_link) + + os.symlink(new_cert_folder, live_link) + + logger.info("Restarting services...") + + for service in ("postfix", "dovecot", "metronome"): + _run_service_command("restart", service) + + _run_service_command("reload", "nginx") + + +def _backup_current_cert(domain): + logger.info("Backuping existing certificate for domain %s", domain) + + cert_folder_domain = os.path.join(CERT_FOLDER, domain) + + date_tag = datetime.now().strftime("%Y%m%d.%H%M%S") + backup_folder = "%s-backups/%s" % (cert_folder_domain, date_tag) + + shutil.copytree(cert_folder_domain, backup_folder) + + +def _check_domain_is_ready_for_ACME(domain): + public_ip = yunohost.domain.get_public_ip() + + # Check if IP from DNS matches public IP + if not _dns_ip_match_public_ip(public_ip, domain): + raise MoulinetteError(errno.EINVAL, m18n.n( + 'certmanager_domain_dns_ip_differs_from_public_ip', domain=domain)) + + # Check if domain seems to be accessible through HTTP? + if not _domain_is_accessible_through_HTTP(public_ip, domain): + raise MoulinetteError(errno.EINVAL, m18n.n( + 'certmanager_domain_http_not_working', domain=domain)) + + # Check if domain is resolved locally (Might happen despite the previous + # checks because of dns propagation ?... Acme-tiny won't work in that case, + # because it explicitly requests() the domain.) + if not _domain_is_resolved_locally(public_ip, domain): + raise MoulinetteError(errno.EINVAL, m18n.n( + 'certmanager_domain_not_resolved_locally', domain=domain)) + + +def _dns_ip_match_public_ip(public_ip, domain): + try: + resolver = dns.resolver.Resolver() + resolver.nameservers = DNS_RESOLVERS + answers = resolver.query(domain, "A") + except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN): + raise MoulinetteError(errno.EINVAL, m18n.n( + 'certmanager_error_no_A_record', domain=domain)) + + dns_ip = str(answers[0]) + + return dns_ip == public_ip + + +def _domain_is_accessible_through_HTTP(ip, domain): + try: + requests.head("http://" + ip, headers={"Host": domain}, timeout=10) + except Timeout as e: + logger.warning(m18n.n('certmanager_http_check_timeout', domain=domain, ip=ip)) + return False + except Exception as e: + logger.debug("Couldn't reach domain '%s' by requesting this ip '%s' because: %s" % (domain, ip, e)) + return False + + return True + + +def _domain_is_resolved_locally(public_ip, domain): + try: + ip = socket.gethostbyname(domain) + except socket.error as e: + logger.debug("Couldn't get domain '%s' ip because: %s" % (domain, e)) + return False + + logger.debug("Domain '%s' ip is %s, except it to be 127.0.0.1 or %s" % (domain, ip, public_ip)) + return ip in ["127.0.0.1", public_ip] + + +def _name_self_CA(): + ca_conf = os.path.join(SSL_DIR, "openssl.ca.cnf") + + if not os.path.exists(ca_conf): + logger.warning(m18n.n('certmanager_self_ca_conf_file_not_found', file=ca_conf)) + return "" + + with open(ca_conf) as f: + lines = f.readlines() + + for line in lines: + if line.startswith("commonName_default"): + return line.split()[2] + + logger.warning(m18n.n('certmanager_unable_to_parse_self_CA_name', file=ca_conf)) + return "" + + +def _tail(n, file_path): + stdin, stdout = os.popen2("tail -n %s '%s'" % (n, file_path)) + + stdin.close() + + lines = stdout.readlines() + stdout.close() + + return "".join(lines) diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index 98fa368ed..ded8bb27a 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -24,19 +24,20 @@ Manage domains """ import os -import sys import datetime import re -import shutil import json import yaml import errno import requests + from urllib import urlopen from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger +import yunohost.certificate + from yunohost.service import service_regen_conf logger = getActionLogger('yunohost.domain') @@ -65,10 +66,10 @@ def domain_list(auth, filter=None, limit=None, offset=None): result = auth.search('ou=domains,dc=yunohost,dc=org', filter, ['virtualdomain']) if len(result) > offset and limit > 0: - for domain in result[offset:offset+limit]: + for domain in result[offset:offset + limit]: result_list.append(domain['virtualdomain'][0]) - return { 'domains': result_list } + return {'domains': result_list} def domain_add(auth, domain, dyndns=False): @@ -82,7 +83,7 @@ def domain_add(auth, domain, dyndns=False): """ from yunohost.hook import hook_callback - attr_dict = { 'objectClass' : ['mailDomain', 'top'] } + attr_dict = {'objectClass': ['mailDomain', 'top']} now = datetime.datetime.now() timestamp = str(now.year) + str(now.month) + str(now.day) @@ -102,7 +103,7 @@ def domain_add(auth, domain, dyndns=False): pass else: dyndomains = json.loads(r.text) - dyndomain = '.'.join(domain.split('.')[1:]) + dyndomain = '.'.join(domain.split('.')[1:]) if dyndomain in dyndomains: if os.path.exists('/etc/cron.d/yunohost-dyndns'): raise MoulinetteError(errno.EPERM, @@ -113,44 +114,13 @@ def domain_add(auth, domain, dyndns=False): m18n.n('domain_dyndns_root_unknown')) try: - # Commands - ssl_dir = '/usr/share/yunohost/yunohost-config/ssl/yunoCA' - ssl_domain_path = '/etc/yunohost/certs/%s' % domain - with open('%s/serial' % ssl_dir, 'r') as f: - serial = f.readline().rstrip() - try: os.listdir(ssl_domain_path) - except OSError: os.makedirs(ssl_domain_path) - - command_list = [ - 'cp %s/openssl.cnf %s' % (ssl_dir, ssl_domain_path), - 'sed -i "s/yunohost.org/%s/g" %s/openssl.cnf' % (domain, ssl_domain_path), - 'openssl req -new -config %s/openssl.cnf -days 3650 -out %s/certs/yunohost_csr.pem -keyout %s/certs/yunohost_key.pem -nodes -batch' - % (ssl_domain_path, ssl_dir, ssl_dir), - 'openssl ca -config %s/openssl.cnf -days 3650 -in %s/certs/yunohost_csr.pem -out %s/certs/yunohost_crt.pem -batch' - % (ssl_domain_path, ssl_dir, ssl_dir), - 'ln -s /etc/ssl/certs/ca-yunohost_crt.pem %s/ca.pem' % ssl_domain_path, - 'cp %s/certs/yunohost_key.pem %s/key.pem' % (ssl_dir, ssl_domain_path), - 'cp %s/newcerts/%s.pem %s/crt.pem' % (ssl_dir, serial, ssl_domain_path), - 'chmod 755 %s' % ssl_domain_path, - 'chmod 640 %s/key.pem' % ssl_domain_path, - 'chmod 640 %s/crt.pem' % ssl_domain_path, - 'chmod 600 %s/openssl.cnf' % ssl_domain_path, - 'chown root:metronome %s/key.pem' % ssl_domain_path, - 'chown root:metronome %s/crt.pem' % ssl_domain_path, - 'cat %s/ca.pem >> %s/crt.pem' % (ssl_domain_path, ssl_domain_path) - ] - - for command in command_list: - if os.system(command) != 0: - raise MoulinetteError(errno.EIO, - m18n.n('domain_cert_gen_failed')) + yunohost.certificate._certificate_install_selfsigned([domain], False) try: - auth.validate_uniqueness({ 'virtualdomain': domain }) + auth.validate_uniqueness({'virtualdomain': domain}) except MoulinetteError: raise MoulinetteError(errno.EEXIST, m18n.n('domain_exists')) - attr_dict['virtualdomain'] = domain if not auth.add('virtualdomain=%s,ou=domains' % domain, attr_dict): @@ -161,11 +131,14 @@ def domain_add(auth, domain, dyndns=False): service_regen_conf(names=[ 'nginx', 'metronome', 'dnsmasq', 'rmilter']) os.system('yunohost app ssowatconf > /dev/null 2>&1') - except IOError: pass + except IOError: + pass except: # Force domain removal silently - try: domain_remove(auth, domain, True) - except: pass + try: + domain_remove(auth, domain, True) + except: + pass raise hook_callback('post_domain_add', args=[domain]) @@ -187,9 +160,13 @@ def domain_remove(auth, domain, force=False): if not force and domain not in domain_list(auth)['domains']: raise MoulinetteError(errno.EINVAL, m18n.n('domain_unknown')) + # Check domain is not the main domain + if domain == _get_maindomain(): + raise MoulinetteError(errno.EINVAL, m18n.n('domain_cannot_remove_main')) + # Check if apps are installed on the domain for app in os.listdir('/etc/yunohost/apps/'): - with open('/etc/yunohost/apps/' + app +'/settings.yml') as f: + with open('/etc/yunohost/apps/' + app + '/settings.yml') as f: try: app_domain = yaml.load(f)['domain'] except: @@ -248,13 +225,13 @@ def domain_dns_conf(domain, ttl=None): "muc {ttl} IN CNAME @\n" "pubsub {ttl} IN CNAME @\n" "vjud {ttl} IN CNAME @\n" - ).format(ttl=ttl, domain=domain) + ).format(ttl=ttl, domain=domain) # Email result += ('\n' '@ {ttl} IN MX 10 {domain}.\n' '@ {ttl} IN TXT "v=spf1 a mx ip4:{ip4}' - ).format(ttl=ttl, domain=domain, ip4=ip4) + ).format(ttl=ttl, domain=domain, ip4=ip4) if ip6 is not None: result += ' ip6:{ip6}'.format(ip6=ip6) result += ' -all"' @@ -270,7 +247,7 @@ def domain_dns_conf(domain, ttl=None): r'^(?P[a-z_\-\.]+)[\s]+([0-9]+[\s]+)?IN[\s]+TXT[\s]+[^"]*' '(?=.*(;[\s]*|")v=(?P[^";]+))' '(?=.*(;[\s]*|")k=(?P[^";]+))' - '(?=.*(;[\s]*|")p=(?P

[^";]+))'), dkim_content, re.M|re.S + '(?=.*(;[\s]*|")p=(?P

[^";]+))'), dkim_content, re.M | re.S ) if dkim: result += '\n{host}. {ttl} IN TXT "v={v}; k={k}; p={p}"'.format( @@ -286,6 +263,18 @@ def domain_dns_conf(domain, ttl=None): return result +def domain_cert_status(auth, domain_list, full=False): + return yunohost.certificate.certificate_status(auth, domain_list, full) + + +def domain_cert_install(auth, domain_list, force=False, no_checks=False, self_signed=False, staging=False): + return yunohost.certificate.certificate_install(auth, domain_list, force, no_checks, self_signed, staging) + + +def domain_cert_renew(auth, domain_list, force=False, no_checks=False, email=False, staging=False): + return yunohost.certificate.certificate_renew(auth, domain_list, force, no_checks, email, staging) + + def get_public_ip(protocol=4): """Retrieve the public IP address from ip.yunohost.org""" if protocol == 4: @@ -301,3 +290,14 @@ def get_public_ip(protocol=4): logger.debug('cannot retrieve public IPv%d' % protocol, exc_info=1) raise MoulinetteError(errno.ENETUNREACH, m18n.n('no_internet_connection')) + + +def _get_maindomain(): + with open('/etc/yunohost/current_host', 'r') as f: + maindomain = f.readline().rstrip() + return maindomain + + +def _set_maindomain(domain): + with open('/etc/yunohost/current_host', 'w') as f: + f.write(domain) diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index 878bc577e..7553e417c 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -94,8 +94,8 @@ def dyndns_subscribe(subscribe_host="dyndns.yunohost.org", domain=None, key=None logger.info(m18n.n('dyndns_key_generating')) - os.system('cd /etc/yunohost/dyndns && ' \ - 'dnssec-keygen -a hmac-md5 -b 128 -n USER %s' % domain) + os.system('cd /etc/yunohost/dyndns && ' + 'dnssec-keygen -a hmac-md5 -b 128 -r /dev/urandom -n USER %s' % domain) os.system('chmod 600 /etc/yunohost/dyndns/*.key /etc/yunohost/dyndns/*.private') key_file = glob.glob('/etc/yunohost/dyndns/*.key')[0] @@ -104,12 +104,14 @@ def dyndns_subscribe(subscribe_host="dyndns.yunohost.org", domain=None, key=None # Send subscription try: - r = requests.post('https://%s/key/%s' % (subscribe_host, base64.b64encode(key)), data={ 'subdomain': domain }) + r = requests.post('https://%s/key/%s' % (subscribe_host, base64.b64encode(key)), data={'subdomain': domain}) except requests.ConnectionError: raise MoulinetteError(errno.ENETUNREACH, m18n.n('no_internet_connection')) if r.status_code != 201: - try: error = json.loads(r.text)['error'] - except: error = "Server error" + try: + error = json.loads(r.text)['error'] + except: + error = "Server error" raise MoulinetteError(errno.EPERM, m18n.n('dyndns_registration_failed', error=error)) @@ -204,33 +206,33 @@ def dyndns_update(dyn_host="dyndns.yunohost.org", domain=None, key=None, lines = [ 'server %s' % dyn_host, 'zone %s' % host, - 'update delete %s. A' % domain, - 'update delete %s. AAAA' % domain, - 'update delete %s. MX' % domain, - 'update delete %s. TXT' % domain, + 'update delete %s. A' % domain, + 'update delete %s. AAAA' % domain, + 'update delete %s. MX' % domain, + 'update delete %s. TXT' % domain, 'update delete pubsub.%s. A' % domain, 'update delete pubsub.%s. AAAA' % domain, - 'update delete muc.%s. A' % domain, + 'update delete muc.%s. A' % domain, 'update delete muc.%s. AAAA' % domain, - 'update delete vjud.%s. A' % domain, + 'update delete vjud.%s. A' % domain, 'update delete vjud.%s. AAAA' % domain, 'update delete _xmpp-client._tcp.%s. SRV' % domain, 'update delete _xmpp-server._tcp.%s. SRV' % domain, - 'update add %s. 1800 A %s' % (domain, ipv4), + 'update add %s. 1800 A %s' % (domain, ipv4), 'update add %s. 14400 MX 5 %s.' % (domain, domain), 'update add %s. 14400 TXT "v=spf1 a mx -all"' % domain, - 'update add pubsub.%s. 1800 A %s' % (domain, ipv4), - 'update add muc.%s. 1800 A %s' % (domain, ipv4), - 'update add vjud.%s. 1800 A %s' % (domain, ipv4), + 'update add pubsub.%s. 1800 A %s' % (domain, ipv4), + 'update add muc.%s. 1800 A %s' % (domain, ipv4), + 'update add vjud.%s. 1800 A %s' % (domain, ipv4), 'update add _xmpp-client._tcp.%s. 14400 SRV 0 5 5222 %s.' % (domain, domain), 'update add _xmpp-server._tcp.%s. 14400 SRV 0 5 5269 %s.' % (domain, domain) ] if ipv6 is not None: lines += [ - 'update add %s. 1800 AAAA %s' % (domain, ipv6), + 'update add %s. 1800 AAAA %s' % (domain, ipv6), 'update add pubsub.%s. 1800 AAAA %s' % (domain, ipv6), - 'update add muc.%s. 1800 AAAA %s' % (domain, ipv6), - 'update add vjud.%s. 1800 AAAA %s' % (domain, ipv6), + 'update add muc.%s. 1800 AAAA %s' % (domain, ipv6), + 'update add vjud.%s. 1800 AAAA %s' % (domain, ipv6), ] lines += [ 'show', diff --git a/src/yunohost/firewall.py b/src/yunohost/firewall.py index 1291cf86a..91f484f48 100644 --- a/src/yunohost/firewall.py +++ b/src/yunohost/firewall.py @@ -67,14 +67,14 @@ def firewall_allow(protocol, port, ipv4_only=False, ipv6_only=False, # Validate protocols protocols = ['TCP', 'UDP'] if protocol != 'Both' and protocol in protocols: - protocols = [protocol,] + protocols = [protocol, ] # Validate IP versions ipvs = ['ipv4', 'ipv6'] if ipv4_only and not ipv6_only: - ipvs = ['ipv4',] + ipvs = ['ipv4', ] elif ipv6_only and not ipv4_only: - ipvs = ['ipv6',] + ipvs = ['ipv6', ] for p in protocols: # Iterate over IP versions to add port @@ -117,18 +117,18 @@ def firewall_disallow(protocol, port, ipv4_only=False, ipv6_only=False, # Validate protocols protocols = ['TCP', 'UDP'] if protocol != 'Both' and protocol in protocols: - protocols = [protocol,] + protocols = [protocol, ] # Validate IP versions and UPnP ipvs = ['ipv4', 'ipv6'] upnp = True if ipv4_only and ipv6_only: - upnp = True # automatically disallow UPnP + upnp = True # automatically disallow UPnP elif ipv4_only: - ipvs = ['ipv4',] + ipvs = ['ipv4', ] upnp = upnp_only elif ipv6_only: - ipvs = ['ipv6',] + ipvs = ['ipv6', ] upnp = upnp_only elif upnp_only: ipvs = [] @@ -178,7 +178,7 @@ def firewall_list(raw=False, by_ip_version=False, list_forwarded=False): ports = sorted(set(ports['ipv4']) | set(ports['ipv6'])) # Format returned dict - ret = { "opened_ports": ports } + ret = {"opened_ports": ports} if list_forwarded: # Combine TCP and UDP forwarded ports ret['forwarded_ports'] = sorted( @@ -224,8 +224,8 @@ def firewall_reload(skip_upnp=False): # Iterate over ports and add rule for protocol in ['TCP', 'UDP']: for port in firewall['ipv4'][protocol]: - rules.append("iptables -w -A INPUT -p %s --dport %s -j ACCEPT" \ - % (protocol, process.quote(str(port)))) + rules.append("iptables -w -A INPUT -p %s --dport %s -j ACCEPT" + % (protocol, process.quote(str(port)))) rules += [ "iptables -w -A INPUT -i lo -j ACCEPT", "iptables -w -A INPUT -p icmp -j ACCEPT", @@ -253,8 +253,8 @@ def firewall_reload(skip_upnp=False): # Iterate over ports and add rule for protocol in ['TCP', 'UDP']: for port in firewall['ipv6'][protocol]: - rules.append("ip6tables -w -A INPUT -p %s --dport %s -j ACCEPT" \ - % (protocol, process.quote(str(port)))) + rules.append("ip6tables -w -A INPUT -p %s --dport %s -j ACCEPT" + % (protocol, process.quote(str(port)))) rules += [ "ip6tables -w -A INPUT -i lo -j ACCEPT", "ip6tables -w -A INPUT -p icmpv6 -j ACCEPT", @@ -308,13 +308,14 @@ def firewall_upnp(action='status', no_refresh=False): try: # Remove old cron job os.remove('/etc/cron.d/yunohost-firewall') - except: pass + except: + pass action = 'status' no_refresh = False if action == 'status' and no_refresh: # Only return current state - return { 'enabled': enabled } + return {'enabled': enabled} elif action == 'enable' or (enabled and action == 'status'): # Add cron job with open(upnp_cron_job, 'w+') as f: @@ -330,7 +331,8 @@ def firewall_upnp(action='status', no_refresh=False): try: # Remove cron job os.remove(upnp_cron_job) - except: pass + except: + pass enabled = False if action == 'status': no_refresh = True @@ -364,7 +366,8 @@ def firewall_upnp(action='status', no_refresh=False): if upnpc.getspecificportmapping(port, protocol): try: upnpc.deleteportmapping(port, protocol) - except: pass + except: + pass if not enabled: continue try: @@ -403,7 +406,7 @@ def firewall_upnp(action='status', no_refresh=False): if action == 'enable' and not enabled: raise MoulinetteError(errno.ENXIO, m18n.n('upnp_port_open_failed')) - return { 'enabled': enabled } + return {'enabled': enabled} def firewall_stop(): @@ -444,12 +447,14 @@ def _get_ssh_port(default=22): pass return default + def _update_firewall_file(rules): """Make a backup and write new rules to firewall file""" os.system("cp {0} {0}.old".format(firewall_file)) with open(firewall_file, 'w') as f: yaml.safe_dump(rules, f, default_flow_style=False) + def _on_rule_command_error(returncode, cmd, output): """Callback for rules commands error""" # Log error and continue commands execution diff --git a/src/yunohost/hook.py b/src/yunohost/hook.py index 500db919f..db7cd9504 100644 --- a/src/yunohost/hook.py +++ b/src/yunohost/hook.py @@ -24,11 +24,8 @@ Manage hooks """ import os -import sys import re -import json import errno -import subprocess from glob import iglob from moulinette.core import MoulinetteError @@ -315,7 +312,7 @@ def hook_exec(path, args=None, raise_on_error=False, no_trace=False, if path[0] != '/': path = os.path.realpath(path) if not os.path.isfile(path): - raise MoulinetteError(errno.EIO, m18n.g('file_not_exist')) + raise MoulinetteError(errno.EIO, m18n.g('file_not_exist', path=path)) # Construct command variables cmd_args = '' diff --git a/src/yunohost/monitor.py b/src/yunohost/monitor.py index d0fe224e9..18089e328 100644 --- a/src/yunohost/monitor.py +++ b/src/yunohost/monitor.py @@ -35,7 +35,7 @@ import errno import os import dns.resolver import cPickle as pickle -from datetime import datetime, timedelta +from datetime import datetime from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger @@ -44,8 +44,8 @@ from yunohost.domain import get_public_ip logger = getActionLogger('yunohost.monitor') -glances_uri = 'http://127.0.0.1:61209' -stats_path = '/var/lib/yunohost/stats' +glances_uri = 'http://127.0.0.1:61209' +stats_path = '/var/lib/yunohost/stats' crontab_path = '/etc/cron.d/yunohost-monitor' @@ -87,13 +87,13 @@ def monitor_disk(units=None, mountpoint=None, human_readable=False): # Retrieve monitoring for unit(s) for u in units: if u == 'io': - ## Define setter + # Define setter if len(units) > 1: def _set(dn, dvalue): try: result[dn][u] = dvalue except KeyError: - result[dn] = { u: dvalue } + result[dn] = {u: dvalue} else: def _set(dn, dvalue): result[dn] = dvalue @@ -111,13 +111,13 @@ def monitor_disk(units=None, mountpoint=None, human_readable=False): for dname in devices_names: _set(dname, 'not-available') elif u == 'filesystem': - ## Define setter + # Define setter if len(units) > 1: def _set(dn, dvalue): try: result[dn][u] = dvalue except KeyError: - result[dn] = { u: dvalue } + result[dn] = {u: dvalue} else: def _set(dn, dvalue): result[dn] = dvalue @@ -183,11 +183,11 @@ def monitor_network(units=None, human_readable=False): smtp_check = m18n.n('network_check_smtp_ko') try: - answers = dns.resolver.query(domain,'MX') + answers = dns.resolver.query(domain, 'MX') mx_check = {} i = 0 for server in answers: - mx_id = 'mx%s' %i + mx_id = 'mx%s' % i mx_check[mx_id] = server i = i + 1 except: @@ -307,7 +307,7 @@ def monitor_update_stats(period): stats = _retrieve_stats(period) if not stats: - stats = { 'disk': {}, 'network': {}, 'system': {}, 'timestamp': [] } + stats = {'disk': {}, 'network': {}, 'system': {}, 'timestamp': []} monitor = None # Get monitoring stats @@ -357,7 +357,7 @@ def monitor_update_stats(period): if 'usage' in stats['network'] and iname in stats['network']['usage']: curr = stats['network']['usage'][iname] net_usage[iname] = _append_to_stats(curr, values, 'time_since_update') - stats['network'] = { 'usage': net_usage, 'infos': monitor['network']['infos'] } + stats['network'] = {'usage': net_usage, 'infos': monitor['network']['infos']} # Append system stats for unit, values in monitor['system'].items(): @@ -421,7 +421,7 @@ def monitor_enable(with_stats=False): rules = ('*/5 * * * * root {cmd} day >> /dev/null\n' '3 * * * * root {cmd} week >> /dev/null\n' '6 */4 * * * root {cmd} month >> /dev/null').format( - cmd='/usr/bin/yunohost --quiet monitor update-stats') + cmd='/usr/bin/yunohost --quiet monitor update-stats') with open(crontab_path, 'w') as f: f.write(rules) @@ -530,7 +530,7 @@ def binary_to_human(n, customary=False): symbols = ('K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y') prefix = {} for i, s in enumerate(symbols): - prefix[s] = 1 << (i+1)*10 + prefix[s] = 1 << (i + 1) * 10 for s in reversed(symbols): if n >= prefix[s]: value = float(n) / prefix[s] @@ -590,7 +590,7 @@ def _save_stats(stats, period, date=None): # Limit stats if date is None: t = stats['timestamp'] - limit = { 'day': 86400, 'week': 604800, 'month': 2419200 } + limit = {'day': 86400, 'week': 604800, 'month': 2419200} if (t[len(t) - 1] - t[0]) > limit[period]: begin = t[len(t) - 1] - limit[period] stats = _filter_stats(stats, begin) @@ -612,7 +612,7 @@ def _monitor_all(period=None, since=None): since -- Timestamp of the stats beginning """ - result = { 'disk': {}, 'network': {}, 'system': {} } + result = {'disk': {}, 'network': {}, 'system': {}} # Real-time stats if period == 'day' and since is None: @@ -697,7 +697,7 @@ def _calculate_stats_mean(stats): s[k] = _mean(v, t, ts) elif isinstance(v, list): try: - nums = [ float(x * t[i]) for i, x in enumerate(v) ] + nums = [float(x * t[i]) for i, x in enumerate(v)] except: pass else: diff --git a/src/yunohost/service.py b/src/yunohost/service.py index ab26dd2bc..dcd3dee83 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -36,7 +36,7 @@ from difflib import unified_diff from moulinette.core import MoulinetteError from moulinette.utils import log, filesystem -from yunohost.hook import hook_list, hook_callback +from yunohost.hook import hook_callback base_conf_path = '/home/yunohost.conf' @@ -60,9 +60,9 @@ def service_add(name, status=None, log=None, runlevel=None): services = _get_services() if not status: - services[name] = { 'status': 'service' } + services[name] = {'status': 'service'} else: - services[name] = { 'status': status } + services[name] = {'status': status} if log is not None: services[name]['log'] = log @@ -202,7 +202,7 @@ def service_status(names=[]): status = None if 'status' not in services[name] or \ - services[name]['status'] == 'service': + services[name]['status'] == 'service': status = 'service %s status' % name else: status = str(services[name]['status']) @@ -211,7 +211,7 @@ def service_status(names=[]): if 'runlevel' in services[name].keys(): runlevel = int(services[name]['runlevel']) - result[name] = { 'status': 'unknown', 'loaded': 'unknown' } + result[name] = {'status': 'unknown', 'loaded': 'unknown'} # Retrieve service status try: @@ -261,7 +261,7 @@ def service_log(name, number=50): for log_path in log_list: if os.path.isdir(log_path): - for log in [ f for f in os.listdir(log_path) if os.path.isfile(os.path.join(log_path, f)) and f[-4:] == '.log' ]: + for log in [f for f in os.listdir(log_path) if os.path.isfile(os.path.join(log_path, f)) and f[-4:] == '.log']: result[os.path.join(log_path, log)] = _tail(os.path.join(log_path, log), int(number)) else: result[log_path] = _tail(log_path, int(number)) @@ -314,13 +314,14 @@ def service_regen_conf(names=[], with_diff=False, force=False, dry_run=False, common_args = [1 if force else 0, 1 if dry_run else 0] # Execute hooks for pre-regen - pre_args = ['pre',] + common_args + pre_args = ['pre', ] + common_args + def _pre_call(name, priority, path, args): # create the pending conf directory for the service service_pending_path = os.path.join(pending_conf_dir, name) filesystem.mkdir(service_pending_path, 0755, True, uid='admin') # return the arguments to pass to the script - return pre_args + [service_pending_path,] + return pre_args + [service_pending_path, ] pre_result = hook_callback('conf_regen', names, pre_callback=_pre_call) # Update the services name @@ -336,8 +337,8 @@ def service_regen_conf(names=[], with_diff=False, force=False, dry_run=False, # Iterate over services and process pending conf for service, conf_files in _get_pending_conf(names).items(): logger.info(m18n.n( - 'service_regenconf_pending_applying' if not dry_run else \ - 'service_regenconf_dry_pending_applying', + 'service_regenconf_pending_applying' if not dry_run else + 'service_regenconf_dry_pending_applying', service=service)) conf_hashes = _get_conf_hashes(service) @@ -444,8 +445,8 @@ def service_regen_conf(names=[], with_diff=False, force=False, dry_run=False, continue elif not failed_regen: logger.success(m18n.n( - 'service_conf_updated' if not dry_run else \ - 'service_conf_would_be_updated', + 'service_conf_updated' if not dry_run else + 'service_conf_would_be_updated', service=service)) if succeed_regen and not dry_run: _update_conf_hashes(service, conf_hashes) @@ -461,14 +462,15 @@ def service_regen_conf(names=[], with_diff=False, force=False, dry_run=False, return result # Execute hooks for post-regen - post_args = ['post',] + common_args + post_args = ['post', ] + common_args + def _pre_call(name, priority, path, args): # append coma-separated applied changes for the service if name in result and result[name]['applied']: regen_conf_files = ','.join(result[name]['applied'].keys()) else: regen_conf_files = '' - return post_args + [regen_conf_files,] + return post_args + [regen_conf_files, ] hook_callback('conf_regen', names, pre_callback=_pre_call) return result @@ -556,7 +558,8 @@ def _tail(file, n, offset=None): return lines[-to_read:offset and -offset or None] avg_line_length *= 1.3 - except IOError: return [] + except IOError: + return [] def _get_files_diff(orig_file, new_file, as_string=False, skip_header=True): diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index f78e32363..15c8a98f8 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -24,14 +24,13 @@ Specific tools """ import os -import sys import yaml -import re -import getpass import requests import json import errno import logging +import subprocess +import pwd from collections import OrderedDict import apt @@ -40,11 +39,11 @@ import apt.progress from moulinette.core import MoulinetteError, init_authenticator from moulinette.utils.log import getActionLogger from yunohost.app import app_fetchlist, app_info, app_upgrade, app_ssowatconf, app_list -from yunohost.domain import domain_add, domain_list, get_public_ip +from yunohost.domain import domain_add, domain_list, get_public_ip, _get_maindomain, _set_maindomain from yunohost.dyndns import dyndns_subscribe -from yunohost.firewall import firewall_upnp, firewall_reload +from yunohost.firewall import firewall_upnp from yunohost.service import service_status, service_regen_conf, service_log -from yunohost.monitor import monitor_disk, monitor_network, monitor_system +from yunohost.monitor import monitor_disk, monitor_system from yunohost.utils.packages import ynh_packages_version apps_setting_path= '/etc/yunohost/apps/' @@ -52,22 +51,34 @@ apps_setting_path= '/etc/yunohost/apps/' logger = getActionLogger('yunohost.tools') -def tools_ldapinit(auth): +def tools_ldapinit(): """ YunoHost LDAP initialization """ + + # Instantiate LDAP Authenticator + auth = init_authenticator(('ldap', 'default'), + {'uri': "ldap://localhost:389", + 'base_dn': "dc=yunohost,dc=org", + 'user_rdn': "cn=admin" }) + auth.authenticate('yunohost') + with open('/usr/share/yunohost/yunohost-config/moulinette/ldap_scheme.yml') as f: ldap_map = yaml.load(f) for rdn, attr_dict in ldap_map['parents'].items(): - try: auth.add(rdn, attr_dict) - except: pass + try: + auth.add(rdn, attr_dict) + except: + pass for rdn, attr_dict in ldap_map['children'].items(): - try: auth.add(rdn, attr_dict) - except: pass + try: + auth.add(rdn, attr_dict) + except: + pass admin_dict = { 'cn': 'admin', @@ -83,8 +94,18 @@ def tools_ldapinit(auth): auth.update('cn=admin', admin_dict) - logger.success(m18n.n('ldap_initialized')) + # Force nscd to refresh cache to take admin creation into account + subprocess.call(['nscd', '-i', 'passwd']) + # Check admin actually exists now + try: + pwd.getpwnam("admin") + except KeyError: + logger.error(m18n.n('ldap_init_failed_to_create_admin')) + raise MoulinetteError(errno.EINVAL, m18n.n('installation_failed')) + + logger.success(m18n.n('ldap_initialized')) + return auth def tools_adminpw(auth, new_password): """ @@ -104,56 +125,49 @@ def tools_adminpw(auth, new_password): logger.success(m18n.n('admin_password_changed')) -def tools_maindomain(auth, old_domain=None, new_domain=None, dyndns=False): +def tools_maindomain(auth, new_domain=None): """ - Main domain change tool + Check the current main domain, or change it Keyword argument: - new_domain - old_domain + new_domain -- The new domain to be set as the main domain """ - if not old_domain: - with open('/etc/yunohost/current_host', 'r') as f: - old_domain = f.readline().rstrip() - - if not new_domain: - return { 'current_main_domain': old_domain } + # If no new domain specified, we return the current main domain if not new_domain: - raise MoulinetteError(errno.EINVAL, m18n.n('new_domain_required')) + return {'current_main_domain': _get_maindomain()} + + # Check domain exists if new_domain not in domain_list(auth)['domains']: - domain_add(auth, new_domain) + raise MoulinetteError(errno.EINVAL, m18n.n('domain_unknown')) - os.system('rm /etc/ssl/private/yunohost_key.pem') - os.system('rm /etc/ssl/certs/yunohost_crt.pem') + # Apply changes to ssl certs + ssl_key = "/etc/ssl/private/yunohost_key.pem" + ssl_crt = "/etc/ssl/private/yunohost_crt.pem" + new_ssl_key = "/etc/yunohost/certs/%s/key.pem" % new_domain + new_ssl_crt = "/etc/yunohost/certs/%s/crt.pem" % new_domain - command_list = [ - 'ln -s /etc/yunohost/certs/%s/key.pem /etc/ssl/private/yunohost_key.pem' % new_domain, - 'ln -s /etc/yunohost/certs/%s/crt.pem /etc/ssl/certs/yunohost_crt.pem' % new_domain, - 'echo %s > /etc/yunohost/current_host' % new_domain, - ] + try: + if os.path.exists(ssl_key) or os.path.lexists(ssl_key): + os.remove(ssl_key) + if os.path.exists(ssl_crt) or os.path.lexists(ssl_crt): + os.remove(ssl_crt) - for command in command_list: - if os.system(command) != 0: - raise MoulinetteError(errno.EPERM, - m18n.n('maindomain_change_failed')) + os.symlink(new_ssl_key, ssl_key) + os.symlink(new_ssl_crt, ssl_crt) - if dyndns and len(new_domain.split('.')) >= 3: - try: - r = requests.get('https://dyndns.yunohost.org/domains') - except requests.ConnectionError: - pass - else: - dyndomains = json.loads(r.text) - dyndomain = '.'.join(new_domain.split('.')[1:]) - if dyndomain in dyndomains: - dyndns_subscribe(domain=new_domain) + _set_maindomain(new_domain) + except Exception as e: + logger.warning("%s" % e, exc_info=1) + raise MoulinetteError(errno.EPERM, m18n.n('maindomain_change_failed')) + # Regen configurations try: with open('/etc/yunohost/installed', 'r') as f: service_regen_conf() - except IOError: pass + except IOError: + pass logger.success(m18n.n('maindomain_changed')) @@ -164,7 +178,8 @@ def tools_postinstall(domain, password, ignore_dyndns=False): Keyword argument: domain -- YunoHost main domain - ignore_dyndns -- Do not subscribe domain to a DynDNS service + ignore_dyndns -- Do not subscribe domain to a DynDNS service (only + needed for nohost.me, noho.st domains) password -- YunoHost admin password """ @@ -182,25 +197,23 @@ def tools_postinstall(domain, password, ignore_dyndns=False): else: dyndomains = json.loads(r.text) dyndomain = '.'.join(domain.split('.')[1:]) + if dyndomain in dyndomains: if requests.get('https://dyndns.yunohost.org/test/%s' % domain).status_code == 200: dyndns = True else: raise MoulinetteError(errno.EEXIST, m18n.n('dyndns_unavailable')) + else: + dyndns = False + else: + dyndns = False logger.info(m18n.n('yunohost_installing')) - # Instantiate LDAP Authenticator - auth = init_authenticator(('ldap', 'default'), - {'uri': "ldap://localhost:389", - 'base_dn': "dc=yunohost,dc=org", - 'user_rdn': "cn=admin" }) - auth.authenticate('yunohost') - # Initialize LDAP for YunoHost # TODO: Improve this part by integrate ldapinit into conf_regen hook - tools_ldapinit(auth) + auth = tools_ldapinit() # Create required folders folders_to_create = [ @@ -212,8 +225,10 @@ def tools_postinstall(domain, password, ignore_dyndns=False): ] for folder in folders_to_create: - try: os.listdir(folder) - except OSError: os.makedirs(folder) + try: + os.listdir(folder) + except OSError: + os.makedirs(folder) # Change folders permissions os.system('chmod 755 /home/yunohost.app') @@ -226,6 +241,9 @@ def tools_postinstall(domain, password, ignore_dyndns=False): try: with open('/etc/ssowat/conf.json.persistent') as json_conf: ssowat_conf = json.loads(str(json_conf.read())) + except ValueError as e: + raise MoulinetteError(errno.EINVAL, + m18n.n('ssowat_persistent_conf_read_error', error=e.strerror)) except IOError: ssowat_conf = {} @@ -234,8 +252,13 @@ def tools_postinstall(domain, password, ignore_dyndns=False): ssowat_conf['redirected_urls']['/'] = domain +'/yunohost/admin' - with open('/etc/ssowat/conf.json.persistent', 'w+') as f: - json.dump(ssowat_conf, f, sort_keys=True, indent=4) + try: + with open('/etc/ssowat/conf.json.persistent', 'w+') as f: + json.dump(ssowat_conf, f, sort_keys=True, indent=4) + except IOError as e: + raise MoulinetteError(errno.EPERM, + m18n.n('ssowat_persistent_conf_write_error', error=e.strerror)) + os.system('chmod 644 /etc/ssowat/conf.json.persistent') @@ -259,7 +282,8 @@ def tools_postinstall(domain, password, ignore_dyndns=False): m18n.n('yunohost_ca_creation_failed')) # New domain config - tools_maindomain(auth, old_domain='yunohost.org', new_domain=domain, dyndns=dyndns) + domain_add(auth, domain, dyndns) + tools_maindomain(auth, domain) # Generate SSOwat configuration file app_ssowatconf(auth) @@ -277,7 +301,6 @@ def tools_postinstall(domain, password, ignore_dyndns=False): os.system('service yunohost-firewall start') service_regen_conf(force=True) - logger.success(m18n.n('yunohost_configured')) @@ -298,6 +321,7 @@ def tools_update(ignore_apps=False, ignore_packages=False): logger.info(m18n.n('updating_apt_cache')) if not cache.update(): raise MoulinetteError(errno.EPERM, m18n.n('update_cache_failed')) + logger.info(m18n.n('done')) cache.open(None) @@ -345,7 +369,7 @@ def tools_update(ignore_apps=False, ignore_packages=False): if len(apps) == 0 and len(packages) == 0: logger.info(m18n.n('packages_no_upgrade')) - return { 'packages': packages, 'apps': apps } + return {'packages': packages, 'apps': apps} def tools_upgrade(auth, ignore_apps=False, ignore_packages=False): @@ -378,6 +402,7 @@ def tools_upgrade(auth, ignore_apps=False, ignore_packages=False): critical_upgrades.add(pkg.name) # Temporarily keep package ... pkg.mark_keep() + # ... and set a hourly cron up to upgrade critical packages if critical_upgrades: logger.info(m18n.n('packages_upgrade_critical_later', @@ -387,6 +412,7 @@ def tools_upgrade(auth, ignore_apps=False, ignore_packages=False): if cache.get_changes(): logger.info(m18n.n('upgrading_packages')) + try: # Apply APT changes # TODO: Logs output for the API @@ -394,7 +420,7 @@ def tools_upgrade(auth, ignore_apps=False, ignore_packages=False): apt.progress.base.InstallProgress()) except Exception as e: failure = True - logging.warning('unable to upgrade packages: %s' % str(e)) + logger.warning('unable to upgrade packages: %s' % str(e)) logger.error(m18n.n('packages_upgrade_failed')) else: logger.info(m18n.n('done')) @@ -406,7 +432,7 @@ def tools_upgrade(auth, ignore_apps=False, ignore_packages=False): app_upgrade(auth) except Exception as e: failure = True - logging.warning('unable to upgrade apps: %s' % str(e)) + logger.warning('unable to upgrade apps: %s' % str(e)) logger.error(m18n.n('app_upgrade_failed')) if not failure: @@ -414,7 +440,7 @@ def tools_upgrade(auth, ignore_apps=False, ignore_packages=False): # Return API logs if it is an API call if is_api: - return { "log": service_log('yunohost-api', number="100").values()[0] } + return {"log": service_log('yunohost-api', number="100").values()[0]} def tools_diagnosis(auth, private=False): @@ -473,6 +499,7 @@ def tools_diagnosis(auth, private=False): # Services status services = service_status() diagnosis['services'] = {} + for service in services: diagnosis['services'][service] = "%s (%s)" % (services[service]['status'], services[service]['loaded']) diff --git a/src/yunohost/user.py b/src/yunohost/user.py index ec7dd539c..9de9595f4 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -30,11 +30,11 @@ import string import json import errno import subprocess -import math import re from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger +from yunohost.service import service_status logger = getActionLogger('yunohost.user') @@ -50,12 +50,12 @@ def user_list(auth, fields=None, filter=None, limit=None, offset=None): fields -- fields to fetch """ - user_attrs = { 'uid': 'username', - 'cn': 'fullname', - 'mail': 'mail', - 'maildrop': 'mail-forward', - 'mailuserquota': 'mailbox-quota' } - attrs = [ 'uid' ] + user_attrs = {'uid': 'username', + 'cn': 'fullname', + 'mail': 'mail', + 'maildrop': 'mail-forward', + 'mailuserquota': 'mailbox-quota'} + attrs = ['uid'] users = {} # Set default arguments values @@ -74,12 +74,12 @@ def user_list(auth, fields=None, filter=None, limit=None, offset=None): raise MoulinetteError(errno.EINVAL, m18n.n('field_invalid', attr)) else: - attrs = [ 'uid', 'cn', 'mail', 'mailuserquota' ] + attrs = ['uid', 'cn', 'mail', 'mailuserquota'] result = auth.search('ou=users,dc=yunohost,dc=org', filter, attrs) if len(result) > offset and limit > 0: - for user in result[offset:offset+limit]: + for user in result[offset:offset + limit]: entry = {} for attr, values in user.items(): try: @@ -88,11 +88,11 @@ def user_list(auth, fields=None, filter=None, limit=None, offset=None): pass uid = entry[user_attrs['uid']] users[uid] = entry - return { 'users' : users } + return {'users': users} def user_create(auth, username, firstname, lastname, mail, password, - mailbox_quota=0): + mailbox_quota="0"): """ Create user @@ -112,8 +112,8 @@ def user_create(auth, username, firstname, lastname, mail, password, # Validate uniqueness of username and mail in LDAP auth.validate_uniqueness({ - 'uid' : username, - 'mail' : mail + 'uid': username, + 'mail': mail }) # Validate uniqueness of username in system users @@ -125,10 +125,10 @@ def user_create(auth, username, firstname, lastname, mail, password, raise MoulinetteError(errno.EEXIST, m18n.n('system_username_exists')) # Check that the mail domain exists - if mail[mail.find('@')+1:] not in domain_list(auth)['domains']: + if mail[mail.find('@') + 1:] not in domain_list(auth)['domains']: raise MoulinetteError(errno.EINVAL, m18n.n('mail_domain_unknown', - domain=mail[mail.find('@')+1:])) + domain=mail[mail.find('@') + 1:])) # Get random UID/GID uid_check = gid_check = 0 @@ -141,24 +141,24 @@ def user_create(auth, username, firstname, lastname, mail, password, fullname = '%s %s' % (firstname, lastname) rdn = 'uid=%s,ou=users' % username char_set = string.ascii_uppercase + string.digits - salt = ''.join(random.sample(char_set,8)) + salt = ''.join(random.sample(char_set, 8)) salt = '$1$' + salt + '$' user_pwd = '{CRYPT}' + crypt.crypt(str(password), salt) attr_dict = { - 'objectClass' : ['mailAccount', 'inetOrgPerson', 'posixAccount'], - 'givenName' : firstname, - 'sn' : lastname, - 'displayName' : fullname, - 'cn' : fullname, - 'uid' : username, - 'mail' : mail, - 'maildrop' : username, - 'mailuserquota' : mailbox_quota, - 'userPassword' : user_pwd, - 'gidNumber' : uid, - 'uidNumber' : uid, - 'homeDirectory' : '/home/' + username, - 'loginShell' : '/bin/false' + 'objectClass': ['mailAccount', 'inetOrgPerson', 'posixAccount'], + 'givenName': firstname, + 'sn': lastname, + 'displayName': fullname, + 'cn': fullname, + 'uid': username, + 'mail': mail, + 'maildrop': username, + 'mailuserquota': mailbox_quota, + 'userPassword': user_pwd, + 'gidNumber': uid, + 'uidNumber': uid, + 'homeDirectory': '/home/' + username, + 'loginShell': '/bin/false' } # If it is the first user, add some aliases @@ -166,26 +166,31 @@ def user_create(auth, username, firstname, lastname, mail, password, with open('/etc/yunohost/current_host') as f: main_domain = f.readline().rstrip() aliases = [ - 'root@'+ main_domain, - 'admin@'+ main_domain, - 'webmaster@'+ main_domain, - 'postmaster@'+ main_domain, + 'root@' + main_domain, + 'admin@' + main_domain, + 'webmaster@' + main_domain, + 'postmaster@' + main_domain, ] - attr_dict['mail'] = [ attr_dict['mail'] ] + aliases + attr_dict['mail'] = [attr_dict['mail']] + aliases # If exists, remove the redirection from the SSO try: with open('/etc/ssowat/conf.json.persistent') as json_conf: ssowat_conf = json.loads(str(json_conf.read())) + except ValueError as e: + raise MoulinetteError(errno.EINVAL, + m18n.n('ssowat_persistent_conf_read_error', error=e.strerror)) + except IOError: + ssowat_conf = {} - if 'redirected_urls' in ssowat_conf and '/' in ssowat_conf['redirected_urls']: - del ssowat_conf['redirected_urls']['/'] - - with open('/etc/ssowat/conf.json.persistent', 'w+') as f: - json.dump(ssowat_conf, f, sort_keys=True, indent=4) - - except IOError: pass - + if 'redirected_urls' in ssowat_conf and '/' in ssowat_conf['redirected_urls']: + del ssowat_conf['redirected_urls']['/'] + try: + with open('/etc/ssowat/conf.json.persistent', 'w+') as f: + json.dump(ssowat_conf, f, sort_keys=True, indent=4) + except IOError as e: + raise MoulinetteError(errno.EPERM, + m18n.n('ssowat_persistent_conf_write_error', error=e.strerror)) if auth.add(rdn, attr_dict): # Invalidate passwd to take user creation into account @@ -194,7 +199,7 @@ def user_create(auth, username, firstname, lastname, mail, password, # Update SFTP user group memberlist = auth.search(filter='cn=sftpusers', attrs=['memberUid'])[0]['memberUid'] memberlist.append(username) - if auth.update('cn=sftpusers,ou=groups', { 'memberUid': memberlist }): + if auth.update('cn=sftpusers,ou=groups', {'memberUid': memberlist}): try: # Attempt to create user home folder subprocess.check_call( @@ -204,12 +209,12 @@ def user_create(auth, username, firstname, lastname, mail, password, logger.warning(m18n.n('user_home_creation_failed'), exc_info=1) app_ssowatconf(auth) - #TODO: Send a welcome mail to user + # TODO: Send a welcome mail to user logger.success(m18n.n('user_created')) hook_callback('post_user_create', args=[username, mail, password, firstname, lastname]) - return { 'fullname' : fullname, 'username' : username, 'mail' : mail } + return {'fullname': fullname, 'username': username, 'mail': mail} raise MoulinetteError(169, m18n.n('user_creation_failed')) @@ -232,9 +237,11 @@ def user_delete(auth, username, purge=False): # Update SFTP user group memberlist = auth.search(filter='cn=sftpusers', attrs=['memberUid'])[0]['memberUid'] - try: memberlist.remove(username) - except: pass - if auth.update('cn=sftpusers,ou=groups', { 'memberUid': memberlist }): + try: + memberlist.remove(username) + except: + pass + if auth.update('cn=sftpusers,ou=groups', {'memberUid': memberlist}): if purge: subprocess.call(['rm', '-rf', '/home/{0}'.format(username)]) else: @@ -280,11 +287,11 @@ def user_update(auth, username, firstname=None, lastname=None, mail=None, # Get modifications from arguments if firstname: - new_attr_dict['givenName'] = firstname # TODO: Validate + new_attr_dict['givenName'] = firstname # TODO: Validate new_attr_dict['cn'] = new_attr_dict['displayName'] = firstname + ' ' + user['sn'][0] if lastname: - new_attr_dict['sn'] = lastname # TODO: Validate + new_attr_dict['sn'] = lastname # TODO: Validate new_attr_dict['cn'] = new_attr_dict['displayName'] = user['givenName'][0] + ' ' + lastname if lastname and firstname: @@ -292,34 +299,34 @@ def user_update(auth, username, firstname=None, lastname=None, mail=None, if change_password: char_set = string.ascii_uppercase + string.digits - salt = ''.join(random.sample(char_set,8)) + salt = ''.join(random.sample(char_set, 8)) salt = '$1$' + salt + '$' new_attr_dict['userPassword'] = '{CRYPT}' + crypt.crypt(str(change_password), salt) if mail: - auth.validate_uniqueness({ 'mail': mail }) - if mail[mail.find('@')+1:] not in domains: + auth.validate_uniqueness({'mail': mail}) + if mail[mail.find('@') + 1:] not in domains: raise MoulinetteError(errno.EINVAL, m18n.n('mail_domain_unknown', - domain=mail[mail.find('@')+1:])) + domain=mail[mail.find('@') + 1:])) del user['mail'][0] new_attr_dict['mail'] = [mail] + user['mail'] if add_mailalias: if not isinstance(add_mailalias, list): - add_mailalias = [ add_mailalias ] + add_mailalias = [add_mailalias] for mail in add_mailalias: - auth.validate_uniqueness({ 'mail': mail }) - if mail[mail.find('@')+1:] not in domains: + auth.validate_uniqueness({'mail': mail}) + if mail[mail.find('@') + 1:] not in domains: raise MoulinetteError(errno.EINVAL, m18n.n('mail_domain_unknown', - domain=mail[mail.find('@')+1:])) + domain=mail[mail.find('@') + 1:])) user['mail'].append(mail) new_attr_dict['mail'] = user['mail'] if remove_mailalias: if not isinstance(remove_mailalias, list): - remove_mailalias = [ remove_mailalias ] + remove_mailalias = [remove_mailalias] for mail in remove_mailalias: if len(user['mail']) > 1 and mail in user['mail'][1:]: user['mail'].remove(mail) @@ -330,7 +337,7 @@ def user_update(auth, username, firstname=None, lastname=None, mail=None, if add_mailforward: if not isinstance(add_mailforward, list): - add_mailforward = [ add_mailforward ] + add_mailforward = [add_mailforward] for mail in add_mailforward: if mail in user['maildrop'][1:]: continue @@ -339,7 +346,7 @@ def user_update(auth, username, firstname=None, lastname=None, mail=None, if remove_mailforward: if not isinstance(remove_mailforward, list): - remove_mailforward = [ remove_mailforward ] + remove_mailforward = [remove_mailforward] for mail in remove_mailforward: if len(user['maildrop']) > 1 and mail in user['maildrop'][1:]: user['maildrop'].remove(mail) @@ -352,11 +359,11 @@ def user_update(auth, username, firstname=None, lastname=None, mail=None, new_attr_dict['mailuserquota'] = mailbox_quota if auth.update('uid=%s,ou=users' % username, new_attr_dict): - logger.success(m18n.n('user_updated')) - app_ssowatconf(auth) - return user_info(auth, username) + logger.success(m18n.n('user_updated')) + app_ssowatconf(auth) + return user_info(auth, username) else: - raise MoulinetteError(169, m18n.n('user_update_failed')) + raise MoulinetteError(169, m18n.n('user_update_failed')) def user_info(auth, username): @@ -372,9 +379,9 @@ def user_info(auth, username): ] if len(username.split('@')) is 2: - filter = 'mail='+ username + filter = 'mail=' + username else: - filter = 'uid='+ username + filter = 'uid=' + username result = auth.search('ou=users,dc=yunohost,dc=org', filter, user_attrs) @@ -398,27 +405,50 @@ def user_info(auth, username): result_dict['mail-forward'] = user['maildrop'][1:] if 'mailuserquota' in user: - if user['mailuserquota'][0] != '0': - cmd = 'doveadm -f flow quota get -u %s' % user['uid'][0] - userquota = subprocess.check_output(cmd,stderr=subprocess.STDOUT, - shell=True) - quotavalue = re.findall(r'\d+', userquota) - result = '%s (%s%s)' % ( _convertSize(eval(quotavalue[0])), - quotavalue[2], '%') - result_dict['mailbox-quota'] = { - 'limit' : user['mailuserquota'][0], - 'use' : result - } + userquota = user['mailuserquota'][0] + + if isinstance(userquota, int): + userquota = str(userquota) + + # Test if userquota is '0' or '0M' ( quota pattern is ^(\d+[bkMGT])|0$ ) + is_limited = not re.match('0[bkMGT]?', userquota) + storage_use = '?' + + if service_status("dovecot")["status"] != "running": + logger.warning(m18n.n('mailbox_used_space_dovecot_down')) else: - result_dict['mailbox-quota'] = m18n.n('unlimit') - + cmd = 'doveadm -f flow quota get -u %s' % user['uid'][0] + cmd_result = subprocess.check_output(cmd, stderr=subprocess.STDOUT, + shell=True) + # Exemple of return value for cmd: + # """Quota name=User quota Type=STORAGE Value=0 Limit=- %=0 + # Quota name=User quota Type=MESSAGE Value=0 Limit=- %=0""" + has_value = re.search(r'Value=(\d+)', cmd_result) + + if has_value: + storage_use = int(has_value.group(1)) + storage_use = _convertSize(storage_use) + + if is_limited: + has_percent = re.search(r'%=(\d+)', cmd_result) + + if has_percent: + percentage = int(has_percent.group(1)) + storage_use += ' (%s%%)' % percentage + + result_dict['mailbox-quota'] = { + 'limit': userquota if is_limited else m18n.n('unlimit'), + 'use': storage_use + } + if result: return result_dict else: raise MoulinetteError(167, m18n.n('user_info_failed')) + def _convertSize(num, suffix=''): - for unit in ['K','M','G','T','P','E','Z']: + for unit in ['K', 'M', 'G', 'T', 'P', 'E', 'Z']: if abs(num) < 1024.0: return "%3.1f%s%s" % (num, unit, suffix) num /= 1024.0 diff --git a/src/yunohost/utils/packages.py b/src/yunohost/utils/packages.py index 5be2103e5..2372e7442 100644 --- a/src/yunohost/utils/packages.py +++ b/src/yunohost/utils/packages.py @@ -424,6 +424,7 @@ def get_installed_version(*pkgnames, **kwargs): return versions[pkgnames[0]] return versions + def meets_version_specifier(pkgname, specifier): """Check if a package installed version meets specifier""" spec = SpecifierSet(specifier) diff --git a/src/yunohost/vendor/__init__.py b/src/yunohost/vendor/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/yunohost/vendor/acme_tiny/__init__.py b/src/yunohost/vendor/acme_tiny/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/yunohost/vendor/acme_tiny/acme_tiny.py b/src/yunohost/vendor/acme_tiny/acme_tiny.py new file mode 100644 index 000000000..d0ba33d1e --- /dev/null +++ b/src/yunohost/vendor/acme_tiny/acme_tiny.py @@ -0,0 +1,212 @@ +#!/usr/bin/env python +import argparse +import subprocess +import json +import os +import sys +import base64 +import binascii +import time +import hashlib +import re +import copy +import textwrap +import logging +try: + from urllib.request import urlopen # Python 3 +except ImportError: + from urllib2 import urlopen # Python 2 + +#DEFAULT_CA = "https://acme-staging.api.letsencrypt.org" +DEFAULT_CA = "https://acme-v01.api.letsencrypt.org" + +LOGGER = logging.getLogger(__name__) +LOGGER.addHandler(logging.StreamHandler()) +LOGGER.setLevel(logging.INFO) + + +def get_crt(account_key, csr, acme_dir, log=LOGGER, CA=DEFAULT_CA): + # helper function base64 encode for jose spec + def _b64(b): + return base64.urlsafe_b64encode(b).decode('utf8').replace("=", "") + + # parse account key to get public key + log.info("Parsing account key...") + proc = subprocess.Popen(["openssl", "rsa", "-in", account_key, "-noout", "-text"], + stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + out, err = proc.communicate() + if proc.returncode != 0: + raise IOError("OpenSSL Error: {0}".format(err)) + pub_hex, pub_exp = re.search( + r"modulus:\n\s+00:([a-f0-9\:\s]+?)\npublicExponent: ([0-9]+)", + out.decode('utf8'), re.MULTILINE | re.DOTALL).groups() + pub_exp = "{0:x}".format(int(pub_exp)) + pub_exp = "0{0}".format(pub_exp) if len(pub_exp) % 2 else pub_exp + header = { + "alg": "RS256", + "jwk": { + "e": _b64(binascii.unhexlify(pub_exp.encode("utf-8"))), + "kty": "RSA", + "n": _b64(binascii.unhexlify(re.sub(r"(\s|:)", "", pub_hex).encode("utf-8"))), + }, + } + accountkey_json = json.dumps(header['jwk'], sort_keys=True, separators=(',', ':')) + thumbprint = _b64(hashlib.sha256(accountkey_json.encode('utf8')).digest()) + + # helper function make signed requests + def _send_signed_request(url, payload): + payload64 = _b64(json.dumps(payload).encode('utf8')) + protected = copy.deepcopy(header) + protected["nonce"] = urlopen(CA + "/directory").headers['Replay-Nonce'] + protected64 = _b64(json.dumps(protected).encode('utf8')) + proc = subprocess.Popen(["openssl", "dgst", "-sha256", "-sign", account_key], + stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + out, err = proc.communicate("{0}.{1}".format(protected64, payload64).encode('utf8')) + if proc.returncode != 0: + raise IOError("OpenSSL Error: {0}".format(err)) + data = json.dumps({ + "header": header, "protected": protected64, + "payload": payload64, "signature": _b64(out), + }) + try: + resp = urlopen(url, data.encode('utf8')) + return resp.getcode(), resp.read() + except IOError as e: + return getattr(e, "code", None), getattr(e, "read", e.__str__)() + + # find domains + log.info("Parsing CSR...") + proc = subprocess.Popen(["openssl", "req", "-in", csr, "-noout", "-text"], + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + out, err = proc.communicate() + if proc.returncode != 0: + raise IOError("Error loading {0}: {1}".format(csr, err)) + domains = set([]) + common_name = re.search(r"Subject:.*? CN=([^\s,;/]+)", out.decode('utf8')) + if common_name is not None: + domains.add(common_name.group(1)) + subject_alt_names = re.search(r"X509v3 Subject Alternative Name: \n +([^\n]+)\n", out.decode('utf8'), re.MULTILINE | re.DOTALL) + if subject_alt_names is not None: + for san in subject_alt_names.group(1).split(", "): + if san.startswith("DNS:"): + domains.add(san[4:]) + + # get the certificate domains and expiration + log.info("Registering account...") + code, result = _send_signed_request(CA + "/acme/new-reg", { + "resource": "new-reg", + "agreement": "https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf", + }) + if code == 201: + log.info("Registered!") + elif code == 409: + log.info("Already registered!") + else: + raise ValueError("Error registering: {0} {1}".format(code, result)) + + # verify each domain + for domain in domains: + log.info("Verifying {0}...".format(domain)) + + # get new challenge + code, result = _send_signed_request(CA + "/acme/new-authz", { + "resource": "new-authz", + "identifier": {"type": "dns", "value": domain}, + }) + if code != 201: + raise ValueError("Error requesting challenges: {0} {1}".format(code, result)) + + # make the challenge file + challenge = [c for c in json.loads(result.decode('utf8'))['challenges'] if c['type'] == "http-01"][0] + token = re.sub(r"[^A-Za-z0-9_\-]", "_", challenge['token']) + keyauthorization = "{0}.{1}".format(token, thumbprint) + wellknown_path = os.path.join(acme_dir, token) + with open(wellknown_path, "w") as wellknown_file: + wellknown_file.write(keyauthorization) + + # check that the file is in place + wellknown_url = "http://{0}/.well-known/acme-challenge/{1}".format(domain, token) + try: + resp = urlopen(wellknown_url) + resp_data = resp.read().decode('utf8').strip() + assert resp_data == keyauthorization + except (IOError, AssertionError): + os.remove(wellknown_path) + raise ValueError("Wrote file to {0}, but couldn't download {1}".format( + wellknown_path, wellknown_url)) + + # notify challenge are met + code, result = _send_signed_request(challenge['uri'], { + "resource": "challenge", + "keyAuthorization": keyauthorization, + }) + if code != 202: + raise ValueError("Error triggering challenge: {0} {1}".format(code, result)) + + # wait for challenge to be verified + while True: + try: + resp = urlopen(challenge['uri']) + challenge_status = json.loads(resp.read().decode('utf8')) + except IOError as e: + raise ValueError("Error checking challenge: {0} {1}".format( + e.code, json.loads(e.read().decode('utf8')))) + if challenge_status['status'] == "pending": + time.sleep(2) + elif challenge_status['status'] == "valid": + log.info("{0} verified!".format(domain)) + os.remove(wellknown_path) + break + else: + raise ValueError("{0} challenge did not pass: {1}".format( + domain, challenge_status)) + + # get the new certificate + log.info("Signing certificate...") + proc = subprocess.Popen(["openssl", "req", "-in", csr, "-outform", "DER"], + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + csr_der, err = proc.communicate() + code, result = _send_signed_request(CA + "/acme/new-cert", { + "resource": "new-cert", + "csr": _b64(csr_der), + }) + if code != 201: + raise ValueError("Error signing certificate: {0} {1}".format(code, result)) + + # return signed certificate! + log.info("Certificate signed!") + return """-----BEGIN CERTIFICATE-----\n{0}\n-----END CERTIFICATE-----\n""".format( + "\n".join(textwrap.wrap(base64.b64encode(result).decode('utf8'), 64))) + + +def main(argv): + parser = argparse.ArgumentParser( + formatter_class=argparse.RawDescriptionHelpFormatter, + description=textwrap.dedent("""\ + This script automates the process of getting a signed TLS certificate from + Let's Encrypt using the ACME protocol. It will need to be run on your server + and have access to your private account key, so PLEASE READ THROUGH IT! It's + only ~200 lines, so it won't take long. + + ===Example Usage=== + python acme_tiny.py --account-key ./account.key --csr ./domain.csr --acme-dir /usr/share/nginx/html/.well-known/acme-challenge/ > signed.crt + =================== + + ===Example Crontab Renewal (once per month)=== + 0 0 1 * * python /path/to/acme_tiny.py --account-key /path/to/account.key --csr /path/to/domain.csr --acme-dir /usr/share/nginx/html/.well-known/acme-challenge/ > /path/to/signed.crt 2>> /var/log/acme_tiny.log + ============================================== + """) + ) + parser.add_argument("--account-key", required=True, help="path to your Let's Encrypt account private key") + parser.add_argument("--csr", required=True, help="path to your certificate signing request") + parser.add_argument("--acme-dir", required=True, help="path to the .well-known/acme-challenge/ directory") + parser.add_argument("--quiet", action="store_const", const=logging.ERROR, help="suppress output except for errors") + parser.add_argument("--ca", default=DEFAULT_CA, help="certificate authority, default is Let's Encrypt") + + args = parser.parse_args(argv) + LOGGER.setLevel(args.quiet or LOGGER.level) + signed_crt = get_crt(args.account_key, args.csr, args.acme_dir, log=LOGGER, CA=args.ca) + sys.stdout.write(signed_crt) + +if __name__ == "__main__": # pragma: no cover + main(sys.argv[1:]) diff --git a/tests/test_actionmap.py b/tests/test_actionmap.py new file mode 100644 index 000000000..08b868839 --- /dev/null +++ b/tests/test_actionmap.py @@ -0,0 +1,4 @@ +import yaml + +def test_yaml_syntax(): + yaml.load(open("data/actionsmap/yunohost.yml"))