Merge branch 'testing' into stable

This commit is contained in:
opi 2017-02-02 11:23:14 +01:00
commit 93553f540c
44 changed files with 2191 additions and 427 deletions

5
.travis.yml Normal file
View file

@ -0,0 +1,5 @@
language: python
install: "pip install pytest pyyaml"
python:
- "2.7"
script: "py.test tests"

88
CONTRIBUTORS.md Normal file
View file

@ -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

View file

@ -1,2 +1,42 @@
Please report issues here (no registration needed): # YunoHost core
https://dev.yunohost.org/projects/yunohost/issues
- [YunoHost project website](https://yunohost.org)
This repository is the core of YunoHost code.
<a href="https://translate.yunohost.org/engage/yunohost/?utm_source=widget">
<img src="https://translate.yunohost.org/widgets/yunohost/-/287x66-white.png" alt="Translation status" />
</a>
## 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.

View file

@ -66,6 +66,10 @@ def _parse_cli_args():
action='store_true', default=False, action='store_true', default=False,
help="Don't produce any output", 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', parser.add_argument('--admin-password',
default=None, dest='password', metavar='PASSWORD', default=None, dest='password', metavar='PASSWORD',
help="The admin password to use to authenticate", help="The admin password to use to authenticate",
@ -209,6 +213,7 @@ if __name__ == '__main__':
ret = moulinette.cli( ret = moulinette.cli(
_retrieve_namespaces(), args, _retrieve_namespaces(), args,
use_cache=opts.use_cache, output_as=opts.output_as, 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) sys.exit(ret)

View file

@ -305,8 +305,71 @@ domain:
- !!str ^[0-9]+$ - !!str ^[0-9]+$
- "pattern_positive_number" - "pattern_positive_number"
### certificate_status()
cert-status:
action_help: List status of current certificates (all by default).
api: GET /domains/cert-status/<domain_list>
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/<domain_list>
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/<domain_list>
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: # info:
# action_help: Get domain informations # action_help: Get domain informations
# api: GET /domains/<domain> # api: GET /domains/<domain>
@ -436,6 +499,10 @@ app:
-a: -a:
full: --args full: --args
help: Serialized arguments for app script (i.e. "domain=domain.tld&path=/path") 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 ### app_remove() TODO: Write help
remove: remove:
@ -1200,7 +1267,7 @@ tools:
### tools_maindomain() ### tools_maindomain()
maindomain: maindomain:
action_help: Main domain change tool action_help: Check the current main domain, or change it
api: api:
- GET /domains/main - GET /domains/main
- PUT /domains/main - PUT /domains/main
@ -1208,12 +1275,9 @@ tools:
authenticate: all authenticate: all
lock: false lock: false
arguments: arguments:
-o:
full: --old-domain
extra:
pattern: *pattern_domain
-n: -n:
full: --new-domain full: --new-domain
help: Change the current main domain
extra: extra:
pattern: *pattern_domain pattern: *pattern_domain

View file

@ -17,7 +17,7 @@ ynh_backup() {
# validate arguments # validate arguments
[[ -e "${SRCPATH}" ]] || { [[ -e "${SRCPATH}" ]] || {
echo "Source path '${DESTPATH}' does not exist" >&2 echo "Source path '${SRCPATH}' does not exist" >&2
return 1 return 1
} }

View file

@ -6,6 +6,6 @@
# | arg: length - the string length to generate (default: 24) # | arg: length - the string length to generate (default: 24)
ynh_string_random() { ynh_string_random() {
dd if=/dev/urandom bs=1 count=200 2> /dev/null \ 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' | sed -n 's/\(.\{'"${1:-24}"'\}\).*/\1/p'
} }

View file

@ -102,6 +102,23 @@ do_post_regen() {
fi fi
sudo service slapd force-reload 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} FORCE=${2:-0}

View file

@ -26,11 +26,18 @@ do_pre_regen() {
's/^\(listen =\).*/\1 */' \ 's/^\(listen =\).*/\1 */' \
"${dovecot_dir}/dovecot.conf" "${dovecot_dir}/dovecot.conf"
fi 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() { do_post_regen() {
regen_conf_files=$1 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 # create vmail user
id vmail > /dev/null 2>&1 \ id vmail > /dev/null 2>&1 \
|| sudo adduser --system --ingroup mail --uid 500 vmail || sudo adduser --system --ingroup mail --uid 500 vmail

View file

@ -9,7 +9,9 @@ do_pre_regen() {
install -D -m 644 rmilter.conf \ install -D -m 644 rmilter.conf \
"${pending_dir}/etc/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" "${pending_dir}/etc/systemd/system/rmilter.socket"
} }
@ -37,17 +39,19 @@ do_post_regen() {
sudo chown _rmilter /etc/dkim/*.mail.key sudo chown _rmilter /etc/dkim/*.mail.key
sudo chmod 400 /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 [ -z "$regen_conf_files" ] && exit 0
# reload systemd daemon # 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 # Restart rmilter due to the rspamd update
# started again by the socket as needed # https://rspamd.com/announce/2016/08/01/rspamd-1.3.1.html
sudo systemctl -q start rmilter.socket sudo systemctl -q restart rmilter.service
sudo systemctl -q stop rmilter.service 2>&1 || true
} }
FORCE=${2:-0} FORCE=${2:-0}

View file

@ -25,10 +25,9 @@ do_post_regen() {
sudo systemctl restart dovecot sudo systemctl restart dovecot
} }
# ensure that the socket is listening and stop the service - it will be # Restart rspamd due to the upgrade
# started again by the socket as needed # https://rspamd.com/announce/2016/08/01/rspamd-1.3.1.html
sudo systemctl -q start rspamd.socket sudo systemctl -q restart rspamd.service
sudo systemctl -q stop rspamd.service 2>&1 || true
} }
FORCE=${2:-0} FORCE=${2:-0}

View file

@ -1,18 +1,48 @@
# 2.1.7: /etc/dovecot/dovecot.conf !include yunohost.d/pre-ext.conf
# OS: Linux 3.2.0-3-686-pae i686 Debian wheezy/sid ext4
listen = *, :: listen = *, ::
auth_mechanisms = plain login auth_mechanisms = plain login
login_greeting = Dovecot ready!!
mail_gid = 8 mail_gid = 8
mail_home = /var/mail/%n mail_home = /var/mail/%n
mail_location = maildir:/var/mail/%n mail_location = maildir:/var/mail/%n
mail_uid = 500 mail_uid = 500
protocols = imap sieve
mail_plugins = $mail_plugins quota
ssl = yes
ssl_cert = </etc/yunohost/certs/{{ main_domain }}/crt.pem
ssl_key = </etc/yunohost/certs/{{ main_domain }}/key.pem
ssl_protocols = !SSLv2 !SSLv3
passdb { passdb {
args = /etc/dovecot/dovecot-ldap.conf args = /etc/dovecot/dovecot-ldap.conf
driver = ldap driver = ldap
} }
protocols = imap sieve
mail_plugins = $mail_plugins quota userdb {
args = /etc/dovecot/dovecot-ldap.conf
driver = ldap
}
protocol imap {
imap_client_workarounds =
mail_plugins = $mail_plugins imap_quota antispam
}
protocol lda {
auth_socket_path = /var/run/dovecot/auth-master
mail_plugins = quota sieve
postmaster_address = postmaster@{{ main_domain }}
}
protocol sieve {
}
service auth { service auth {
unix_listener /var/spool/postfix/private/auth { unix_listener /var/spool/postfix/private/auth {
group = postfix group = postfix
@ -26,26 +56,11 @@ service auth {
} }
} }
protocol sieve { service quota-warning {
} executable = script /usr/bin/quota-warning.sh
user = vmail
ssl = yes unix_listener quota-warning {
ssl_cert = </etc/yunohost/certs/{{ main_domain }}/crt.pem }
ssl_key = </etc/yunohost/certs/{{ main_domain }}/key.pem
ssl_protocols = !SSLv2 !SSLv3
userdb {
args = /etc/dovecot/dovecot-ldap.conf
driver = ldap
}
protocol imap {
imap_client_workarounds =
mail_plugins = $mail_plugins imap_quota antispam
}
protocol lda {
auth_socket_path = /var/run/dovecot/auth-master
mail_plugins = quota sieve
postmaster_address = postmaster@{{ main_domain }}
} }
plugin { plugin {
@ -66,11 +81,6 @@ plugin {
antispam_pipe_program_notspam_arg = learn_ham antispam_pipe_program_notspam_arg = learn_ham
} }
plugin {
autosubscribe = Trash
autosubscribe2 = Junk
}
plugin { plugin {
quota = maildir:User quota quota = maildir:User quota
quota_rule2 = SPAM:ignore quota_rule2 = SPAM:ignore
@ -83,9 +93,4 @@ plugin {
quota_warning3 = -storage=100%% quota-warning below %u # user is no longer over quota quota_warning3 = -storage=100%% quota-warning below %u # user is no longer over quota
} }
service quota-warning { !include yunohost.d/post-ext.conf
executable = script /usr/bin/quota-warning.sh
user = vmail
unix_listener quota-warning {
}
}

View file

@ -0,0 +1 @@
!include_try post-ext.d/*.conf

View file

@ -0,0 +1 @@
!include_try pre-ext.d/*.conf

View file

@ -581,4 +581,5 @@ enabled = true
port = http,https port = http,https
protocol = tcp protocol = tcp
filter = yunohost filter = yunohost
logpath = /var/log/nginx/*.log logpath = /var/log/nginx*/*error.log
maxretry = 6

View file

@ -14,7 +14,7 @@
# (?:::f{4,6}:)?(?P<host>[\w\-.^_]+) # (?:::f{4,6}:)?(?P<host>[\w\-.^_]+)
# Values: TEXT # Values: TEXT
# #
failregex = access.lua:[1-9]+: authenticate\(\): Connection failed for: .*, client: <HOST> failregex = helpers.lua:[1-9]+: authenticate\(\): Connection failed for: .*, client: <HOST>
^<HOST> -.*\"POST /yunohost/api/login HTTP/1.1\" 401 22 ^<HOST> -.*\"POST /yunohost/api/login HTTP/1.1\" 401 22
# Option: ignoreregex # Option: ignoreregex

View file

@ -141,7 +141,7 @@ smtp_reply_filter = pcre:/etc/postfix/smtp_reply_filter
# Rmilter # Rmilter
milter_mail_macros = i {mail_addr} {client_addr} {client_name} {auth_authen} milter_mail_macros = i {mail_addr} {client_addr} {client_name} {auth_authen}
milter_protocol = 6 milter_protocol = 6
smtpd_milters = inet:localhost:11000 smtpd_milters = unix:/run/rmilter/rmilter.sock
# Skip email without checking if milter has died # Skip email without checking if milter has died
milter_default_action = accept milter_default_action = accept

View file

@ -5,8 +5,7 @@
# pidfile - path to pid file # pidfile - path to pid file
pidfile = /run/rmilter/rmilter.pid; pidfile = /run/rmilter/rmilter.pid;
# rmilter is socket-activated under systemd bind_socket = unix:/var/spool/postfix/run/rmilter/rmilter.sock;
bind_socket = fd:3;
# DKIM signing # DKIM signing
dkim { dkim {

View file

@ -1,5 +0,0 @@
.include /lib/systemd/system/rmilter.socket
[Socket]
ListenStream=
ListenStream=127.0.0.1:11000

138
debian/changelog vendored
View file

@ -1,3 +1,141 @@
yunohost (2.5.3.1) testing; urgency=low
* super quickfix release for a typo that break LE certificates
-- Laurent Peuch <cortex@worlddomination.be> 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 <cortex@worlddomination.be> 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 <cortex@worlddomination.be> 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 <cortex@worlddomination.be> 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 <opi@zeropi.net> Thu, 01 Dec 2016 21:22:19 +0100
yunohost (2.4.2) stable; urgency=low yunohost (2.4.2) stable; urgency=low
[ Laurent Peuch ] [ Laurent Peuch ]

5
debian/control vendored
View file

@ -11,13 +11,13 @@ Package: yunohost
Architecture: all Architecture: all
Depends: ${python:Depends}, ${misc:Depends} Depends: ${python:Depends}, ${misc:Depends}
, moulinette (>= 2.3.5.1) , moulinette (>= 2.3.5.1)
, python-psutil, python-requests, python-dnspython , python-psutil, python-requests, python-dnspython, python-openssl
, python-apt, python-miniupnpc , python-apt, python-miniupnpc
, glances , glances
, dnsutils, bind9utils, unzip, git, curl, cron , dnsutils, bind9utils, unzip, git, curl, cron
, ca-certificates, netcat-openbsd, iproute , ca-certificates, netcat-openbsd, iproute
, mariadb-server | mysql-server, php5-mysql | php5-mysqlnd , 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 , postfix-ldap, postfix-policyd-spf-perl, postfix-pcre, procmail
, dovecot-ldap, dovecot-lmtpd, dovecot-managesieved , dovecot-ldap, dovecot-lmtpd, dovecot-managesieved
, dovecot-antispam, fail2ban , dovecot-antispam, fail2ban
@ -25,6 +25,7 @@ Depends: ${python:Depends}, ${misc:Depends}
, dnsmasq, openssl, avahi-daemon , dnsmasq, openssl, avahi-daemon
, ssowat, metronome , ssowat, metronome
, rspamd (>= 1.2.0), rmilter (>=1.7.0), redis-server, opendkim-tools , rspamd (>= 1.2.0), rmilter (>=1.7.0), redis-server, opendkim-tools
, haveged
Recommends: yunohost-admin Recommends: yunohost-admin
, openssh-server, ntp, inetutils-ping | iputils-ping , openssh-server, ntp, inetutils-ping | iputils-ping
, bash-completion, rsyslog, etckeeper , bash-completion, rsyslog, etckeeper

1
locales/br.json Normal file
View file

@ -0,0 +1 @@
{}

View file

@ -209,5 +209,14 @@
"yunohost_ca_creation_failed": "Zertifikatsstelle konnte nicht erstellt werden", "yunohost_ca_creation_failed": "Zertifikatsstelle konnte nicht erstellt werden",
"yunohost_configured": "YunoHost wurde erfolgreich konfiguriert", "yunohost_configured": "YunoHost wurde erfolgreich konfiguriert",
"yunohost_installing": "YunoHost wird installiert...", "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"
} }

View file

@ -43,6 +43,7 @@
"backup_action_required": "You must specify something to save", "backup_action_required": "You must specify something to save",
"backup_app_failed": "Unable to back up the app '{app:s}'", "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_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_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_exists": "The backup's archive name already exists",
"backup_archive_name_unknown": "Unknown local backup archive named '{name:s}'", "backup_archive_name_unknown": "Unknown local backup archive named '{name:s}'",
@ -57,7 +58,7 @@
"backup_hook_unknown": "Backup hook '{hook:s}' unknown", "backup_hook_unknown": "Backup hook '{hook:s}' unknown",
"backup_invalid_archive": "Invalid backup archive", "backup_invalid_archive": "Invalid backup archive",
"backup_nothings_done": "There is nothing to save", "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_not_empty": "The output directory is not empty",
"backup_output_directory_required": "You must provide an output directory for the backup", "backup_output_directory_required": "You must provide an output directory for the backup",
"backup_running_app_script": "Running backup script of app '{app:s}'...", "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_monitor_system_error": "Can't monitor system: {error}",
"diagnostic_no_apps": "No installed application", "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'", "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_created": "The domain has been created",
"domain_creation_failed": "Unable to create domain", "domain_creation_failed": "Unable to create domain",
"domain_deleted": "The domain has been deleted", "domain_deleted": "The domain has been deleted",
@ -80,11 +80,12 @@
"domain_dyndns_invalid": "Invalid domain to use with DynDNS", "domain_dyndns_invalid": "Invalid domain to use with DynDNS",
"domain_dyndns_root_unknown": "Unknown DynDNS root domain", "domain_dyndns_root_unknown": "Unknown DynDNS root domain",
"domain_exists": "Domain already exists", "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_unknown": "Unknown domain",
"domain_zone_exists": "DNS zone file already exists", "domain_zone_exists": "DNS zone file already exists",
"domain_zone_not_found": "DNS zone file not found for domain {:s}", "domain_zone_not_found": "DNS zone file not found for domain {:s}",
"done": "Done.", "done": "Done",
"domains_available": "Available domains:",
"downloading": "Downloading...", "downloading": "Downloading...",
"dyndns_cron_installed": "The DynDNS cron job has been installed", "dyndns_cron_installed": "The DynDNS cron job has been installed",
"dyndns_cron_remove_failed": "Unable to remove the DynDNS cron job", "dyndns_cron_remove_failed": "Unable to remove the DynDNS cron job",
@ -103,7 +104,7 @@
"field_invalid": "Invalid field '{:s}'", "field_invalid": "Invalid field '{:s}'",
"firewall_reload_failed": "Unable to reload the firewall", "firewall_reload_failed": "Unable to reload the firewall",
"firewall_reloaded": "The firewall has been reloaded", "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", "format_datetime_short": "%m/%d/%Y %I:%M %p",
"hook_exec_failed": "Script execution failed: {path:s}", "hook_exec_failed": "Script execution failed: {path:s}",
"hook_exec_not_terminated": "Script execution hasnt terminated: {path:s}", "hook_exec_not_terminated": "Script execution hasnt terminated: {path:s}",
@ -111,13 +112,15 @@
"hook_name_unknown": "Unknown hook name '{name:s}'", "hook_name_unknown": "Unknown hook name '{name:s}'",
"installation_complete": "Installation complete", "installation_complete": "Installation complete",
"installation_failed": "Installation failed", "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.", "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.", "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_initialized": "LDAP has been initialized",
"ldap_init_failed_to_create_admin": "LDAP initialization failed to create admin user",
"license_undefined": "undefined", "license_undefined": "undefined",
"mail_alias_remove_failed": "Unable to remove mail alias '{mail:s}'", "mail_alias_remove_failed": "Unable to remove mail alias '{mail:s}'",
"mail_domain_unknown": "Unknown mail address domain '{domain:s}'", "mail_domain_unknown": "Unknown mail address domain '{domain:s}'",
"mail_forward_remove_failed": "Unable to remove mail forward '{mail: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_change_failed": "Unable to change the main domain",
"maindomain_changed": "The main domain has been changed", "maindomain_changed": "The main domain has been changed",
"monitor_disabled": "The server monitoring has been disabled", "monitor_disabled": "The server monitoring has been disabled",
@ -209,6 +212,8 @@
"service_unknown": "Unknown service '{service:s}'", "service_unknown": "Unknown service '{service:s}'",
"ssowat_conf_generated": "The SSOwat configuration has been generated", "ssowat_conf_generated": "The SSOwat configuration has been generated",
"ssowat_conf_updated": "The SSOwat configuration has been updated", "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_upgraded": "The system has been upgraded",
"system_username_exists": "Username already exists in the system users", "system_username_exists": "Username already exists in the system users",
"unbackup_app": "App '{app:s}' will not be saved", "unbackup_app": "App '{app:s}' will not be saved",
@ -237,5 +242,31 @@
"yunohost_ca_creation_failed": "Unable to create certificate authority", "yunohost_ca_creation_failed": "Unable to create certificate authority",
"yunohost_configured": "YunoHost has been configured", "yunohost_configured": "YunoHost has been configured",
"yunohost_installing": "Installing YunoHost...", "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})"
} }

1
locales/eo.json Normal file
View file

@ -0,0 +1 @@
{}

View file

@ -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": "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", "admin_password_changed": "La contraseña administrativa ha sido cambiada",
"app_already_installed": "{app:s} ya está instalada", "app_already_installed": "{app:s} 2 ya está instalada",
"app_argument_choice_invalid": "Opción no válida para el argumento '{name:s}', deber una de {choices:s}", "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}': {error:s}", "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}'", "app_argument_required": "Se requiere el argumento '{name:s} 7'",
"app_extraction_failed": "No se pudieron extraer los archivos de instalación", "app_extraction_failed": "No se pudieron extraer los archivos de instalación",
"app_id_invalid": "Id de la aplicación no válida", "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", "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_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_manifest_invalid": "El manifiesto de la aplicación no es válido",
"app_no_upgrade": "No hay aplicaciones para actualizar", "app_no_upgrade": "No hay aplicaciones para actualizar",
"app_not_correctly_installed": "La aplicación {app:s} parece estar incorrectamente instalada", "app_not_correctly_installed": "La aplicación {app:s} 8 parece estar incorrectamente instalada",
"app_not_installed": "{app:s} no está instalada", "app_not_installed": "{app:s} 9 no está instalada",
"app_not_properly_removed": "La {app:s} no ha sido desinstalada correctamente", "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_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_recent_version_required": "{:s} requiere una versión más reciente de moulinette ",
"app_removed": "{app:s} ha sido eliminada", "app_removed": "{app:s} ha sido eliminada",
@ -58,7 +58,7 @@
"backup_hook_unknown": "Hook de copia de seguridad desconocido '{hook:s}'", "backup_hook_unknown": "Hook de copia de seguridad desconocido '{hook:s}'",
"backup_invalid_archive": "La copia de seguridad no es válida", "backup_invalid_archive": "La copia de seguridad no es válida",
"backup_nothings_done": "No hay nada que guardar", "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_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_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}'...", "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_network_error": "No se puede monitorizar la red: {error}",
"diagnostic_monitor_system_error": "No se puede monitorizar el sistema: {error}", "diagnostic_monitor_system_error": "No se puede monitorizar el sistema: {error}",
"diagnostic_no_apps": "Aplicación no instalada", "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_cert_gen_failed": "No se pudo crear el certificado",
"domain_created": "El dominio ha sido creado", "domain_created": "El dominio ha sido creado",
"domain_creation_failed": "No se pudo crear el dominio", "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_uninstall_app_first": "Una o más aplicaciones están instaladas en este dominio. Debe desinstalarlas antes de eliminarlo.",
"domain_unknown": "Dominio desconocido", "domain_unknown": "Dominio desconocido",
"domain_zone_exists": "El archivo de zona del DNS ya existe", "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.", "done": "Hecho.",
"downloading": "Descargando...", "downloading": "Descargando...",
"dyndns_cron_installed": "La tarea cron para DynDNS ha sido instalada", "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_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_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", "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_list_by_invalid": "Propiedad no válida para listar por hook",
"hook_name_unknown": "Nombre de hook desconocido '{name:s}'", "hook_name_unknown": "Nombre de hook desconocido '{name:s}'",
"installation_complete": "Instalación finalizada", "installation_complete": "Instalación finalizada",
"installation_failed": "No pudo realizar la instalación", "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.", "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.", "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", "ldap_initialized": "Se ha inicializado LDAP",
"license_undefined": "indefinido", "license_undefined": "indefinido",
"mail_alias_remove_failed": "No se pudo eliminar el alias de correo '{mail:s}'", "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_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}'", "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_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_disabled": "La monitorización del sistema ha sido deshabilitada",
"monitor_enabled": "La monitorización del sistema ha sido habilitada", "monitor_enabled": "La monitorización del sistema ha sido habilitada",
"monitor_glances_con_failed": "No se pudo conectar al servidor Glances", "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_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_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_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", "mountpoint_unknown": "Punto de montaje desconocido",
"mysql_db_creation_failed": "No se pudo crear la base de datos MySQL", "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_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_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", "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", "new_domain_required": "Debe proporcionar el nuevo dominio principal",
"no_appslist_found": "No se ha encontrado ninguna lista de aplicaciones", "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}'", "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}'", "not_enough_disk_space": "No hay suficiente espacio en '{path:s}'",
"package_not_installed": "El paquete '{pkgname}' no está instalado", "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}'", "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_critical_later": "Los paquetes críticos ({packages:s}) serán actualizados más tarde",
"packages_upgrade_failed": "No se pudieron actualizar todos los paquetes", "packages_upgrade_failed": "No se pudieron actualizar todos los paquetes",
"path_removal_failed": "No se pudo borrar la ruta {:s}", "path_removal_failed": "No se pudo eliminar 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 -_", "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_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_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", "pattern_firstname": "Debe ser un nombre válido",
@ -180,7 +180,7 @@
"restore_running_hooks": "Ejecutando los hooks de restauración...", "restore_running_hooks": "Ejecutando los hooks de restauración...",
"service_add_failed": "No se pudo añadir el servicio '{service:s}'", "service_add_failed": "No se pudo añadir el servicio '{service:s}'",
"service_added": "Servicio '{service:s}' ha sido añadido", "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_already_stopped": "El servicio '{service:s}' ya ha sido detenido",
"service_cmd_exec_failed": "No se pudo ejecutar el comando '{command:s}'", "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}'", "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_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_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_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_disable_failed": "No se pudo deshabilitar el servicio '{service:s}'",
"service_disabled": "El servicio '{service:s}' ha sido deshabilitado", "service_disabled": "El servicio '{service:s}' ha sido deshabilitado",
"service_enable_failed": "No se pudo habilitar el servicio '{service:s}'", "service_enable_failed": "No se pudo habilitar el servicio '{service:s}'",
"service_enabled": "El servicio '{service:s}' ha sido habilitado", "service_enabled": "El servicio '{service:s}' ha sido habilitado",
"service_no_log": "No hay ningún registro para el servicio '{service:s}'", "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_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_remove_failed": "No se pudo desinstalar el servicio '{service:s}'",
"service_removed": "El servicio '{service:s}' ha sido desinstalado", "service_removed": "El servicio '{service:s}' ha sido desinstalado",
"service_start_failed": "No se pudo iniciar el servicio '{service:s}'", "service_start_failed": "No se pudo iniciar el servicio '{service:s}'",
@ -219,7 +219,7 @@
"unit_unknown": "Unidad desconocida '{unit:s}'", "unit_unknown": "Unidad desconocida '{unit:s}'",
"unlimit": "Sin cuota", "unlimit": "Sin cuota",
"unrestore_app": "La aplicación '{app:s}' no será restaurada", "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...", "updating_apt_cache": "Actualizando lista de paquetes disponibles...",
"upgrade_complete": "Actualización finalizada", "upgrade_complete": "Actualización finalizada",
"upgrading_packages": "Actualizando paquetes...", "upgrading_packages": "Actualizando paquetes...",
@ -240,5 +240,31 @@
"yunohost_ca_creation_failed": "No se pudo crear el certificado de autoridad", "yunohost_ca_creation_failed": "No se pudo crear el certificado de autoridad",
"yunohost_configured": "YunoHost ha sido configurado", "yunohost_configured": "YunoHost ha sido configurado",
"yunohost_installing": "Instalando YunoHost...", "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:"
} }

View file

@ -59,7 +59,7 @@
"backup_hook_unknown": "Script de sauvegarde « {hook:s} » inconnu", "backup_hook_unknown": "Script de sauvegarde « {hook:s} » inconnu",
"backup_invalid_archive": "Archive de sauvegarde incorrecte", "backup_invalid_archive": "Archive de sauvegarde incorrecte",
"backup_nothings_done": "Il n'y a rien à sauvegarder", "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_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_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} »...", "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_invalid": "Domaine incorrect pour un usage avec DynDNS",
"domain_dyndns_root_unknown": "Domaine DynDNS principal inconnu", "domain_dyndns_root_unknown": "Domaine DynDNS principal inconnu",
"domain_exists": "Le domaine existe déjà", "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_unknown": "Domaine inconnu",
"domain_zone_exists": "Le fichier de zone DNS existe déjà", "domain_zone_exists": "Le fichier de zone DNS existe déjà",
"domain_zone_not_found": "Fichier de zone DNS introuvable pour le domaine {:s}", "domain_zone_not_found": "Fichier de zone DNS introuvable pour le domaine {:s}",
"done": "Terminé.", "done": "Terminé",
"downloading": "Téléchargement...", "downloading": "Téléchargement...",
"dyndns_cron_installed": "La tâche cron pour le domaine DynDNS a été installée", "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", "dyndns_cron_remove_failed": "Impossible d'enlever la tâche cron pour le domaine DynDNS",
@ -105,23 +105,23 @@
"field_invalid": "Champ incorrect : « {:s} »", "field_invalid": "Champ incorrect : « {:s} »",
"firewall_reload_failed": "Impossible de recharger le pare-feu", "firewall_reload_failed": "Impossible de recharger le pare-feu",
"firewall_reloaded": "Le pare-feu a été rechargé", "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", "format_datetime_short": "%d/%m/%Y %H:%M",
"hook_argument_missing": "Argument manquant : '{:s}'", "hook_argument_missing": "Argument manquant : '{:s}'",
"hook_choice_invalid": "Choix incorrect : '{:s}'", "hook_choice_invalid": "Choix incorrect : '{:s}'",
"hook_exec_failed": "Échec de l'exécution du script « {path: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_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", "hook_name_unknown": "Nom de script « {name:s} » inconnu",
"installation_complete": "Installation terminée", "installation_complete": "Installation terminée",
"installation_failed": "Échec de l'installation", "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.", "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 sûrement dans un conteneur, autrement 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é", "ldap_initialized": "L'annuaire LDAP a été initialisé",
"license_undefined": "indéfinie", "license_undefined": "indéfinie",
"mail_alias_remove_failed": "Impossible de supprimer l'adresse courriel supplémentaire « {mail:s} »", "mail_alias_remove_failed": "Impossible de supprimer l'alias courriel « {mail:s} »",
"mail_domain_unknown": "Le domaine « {domain:s} » de l'adresse courriel est inconnu", "mail_domain_unknown": "Le domaine « {domain:s} » du courriel est inconnu",
"mail_forward_remove_failed": "Impossible de supprimer l'adresse courriel de transfert « {mail:s} »", "mail_forward_remove_failed": "Impossible de supprimer le courriel de transfert « {mail:s} »",
"maindomain_change_failed": "Impossible de modifier le domaine principal", "maindomain_change_failed": "Impossible de modifier le domaine principal",
"maindomain_changed": "Le domaine principal a été modifié", "maindomain_changed": "Le domaine principal a été modifié",
"monitor_disabled": "La supervision du serveur a été désactivé", "monitor_disabled": "La supervision du serveur a été désactivé",
@ -155,7 +155,7 @@
"path_removal_failed": "Impossible de supprimer le chemin {:s}", "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_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_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_firstname": "Doit être un prénom valide",
"pattern_lastname": "Doit être un nom valide", "pattern_lastname": "Doit être un nom valide",
"pattern_listname": "Doit être composé uniquement de caractères alphanumériques et de tirets bas", "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_ca_creation_failed": "Impossible de créer l'autorité de certification",
"yunohost_configured": "YunoHost a été configuré", "yunohost_configured": "YunoHost a été configuré",
"yunohost_installing": "Installation de YunoHost...", "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} nest 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} nest pas fourni par Lets Encrypt. Impossible de le renouveler automatiquement !",
"certmanager_attempt_to_renew_valid_cert": "Le certificat pour le domaine {domain:s} est sur le point dexpirer ! Utilisez --force pour contourner",
"certmanager_domain_http_not_working": "Il semble que le domaine {domain:s} nest pas accessible via HTTP. Veuillez vérifier que vos configuration DNS et nginx sont correctes",
"certmanager_error_no_A_record": "Aucun enregistrement DNS « A » na été trouvé pour {domain:s}. De devez faire pointer votre nom de domaine vers votre machine pour être capable dinstaller un certificat Lets 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": "Lenregistrement DNS « A » du domaine {domain:s} est différent de ladresse 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 sest mal passé lors de la tentative douverture du certificat actuel pour le domaine {domain:s} (fichier : {file:s}), cause : {reason:s}",
"certmanager_cert_install_success_selfsigned": "Installation avec succès dun certificat auto-signé pour le domaine {domain:s} !",
"certmanager_cert_install_success": "Installation avec succès dun certificat Lets Encrypt pour le domaine {domain:s} !",
"certmanager_cert_renew_success": "Renouvellement avec succès dun certificat Lets Encrypt pour le domaine {domain:s} !",
"certmanager_old_letsencrypt_app_detected": "\nYunoHost a détecté que lapplication « 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": "Linitialisation de LDAP na pas réussi à créer lutilisateur 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 lautorité du certificat auto-signé est introuvable (fichier : {file:s})",
"certmanager_unable_to_parse_self_CA_name": "Impossible danalyser le nom de lautorité 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 dattendre quelques heures quil se propage. Si le problème persiste, envisager dajouter {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 lIP {ip:s}). Vous rencontrez peut-être un problème dhairpinning 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 Lets Encrypt. Linstallation/le renouvellement du certificat a été interrompu - veuillez réessayer prochainement."
} }

View file

@ -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 के साथ इनवैलिड डोमिन इस्तेमाल किया गया"
}

View file

@ -24,10 +24,8 @@
Manage apps Manage apps
""" """
import os import os
import sys
import json import json
import shutil import shutil
import stat
import yaml import yaml
import time import time
import re import re
@ -101,7 +99,7 @@ def app_fetchlist(url=None, name=None):
m18n.n('custom_appslist_name_required')) m18n.n('custom_appslist_name_required'))
list_file = '%s/%s.json' % (repo_path, name) 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) os.remove('%s.tmp' % list_file)
raise MoulinetteError(errno.EBADR, m18n.n('appslist_retrieve_error')) 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')) 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 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 app -- Name, local path or git URL of the app to install
label -- Custom name for the app label -- Custom name for the app
args -- Serialize arguments for app installation 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 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')) logger.exception(m18n.n('unexpected_error'))
finally: finally:
if install_retcode != 0: if install_retcode != 0:
# Setup environment for remove script if not no_remove_on_failure:
env_dict_remove = {} # Setup environment for remove script
env_dict_remove["YNH_APP_ID"] = app_id env_dict_remove = {}
env_dict_remove["YNH_APP_INSTANCE_NAME"] = app_instance_name env_dict_remove["YNH_APP_ID"] = app_id
env_dict_remove["YNH_APP_INSTANCE_NUMBER"] = str(instance_number) env_dict_remove["YNH_APP_INSTANCE_NAME"] = app_instance_name
env_dict_remove["YNH_APP_INSTANCE_NUMBER"] = str(instance_number)
# Execute remove script # Execute remove script
remove_retcode = hook_exec( remove_retcode = hook_exec(
os.path.join(extracted_app_folder, 'scripts/remove'), os.path.join(extracted_app_folder, 'scripts/remove'),
args=[app_instance_name], env=env_dict_remove) args=[app_instance_name], env=env_dict_remove)
if remove_retcode != 0: if remove_retcode != 0:
logger.warning(m18n.n('app_not_properly_removed', logger.warning(m18n.n('app_not_properly_removed',
app=app_instance_name)) app=app_instance_name))
# Clean tmp folders # Clean tmp folders
shutil.rmtree(app_setting_path) shutil.rmtree(app_setting_path)
@ -811,6 +811,9 @@ def app_makedefault(auth, app, domain=None):
try: try:
with open('/etc/ssowat/conf.json.persistent') as json_conf: with open('/etc/ssowat/conf.json.persistent') as json_conf:
ssowat_conf = json.loads(str(json_conf.read())) 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: except IOError:
ssowat_conf = {} ssowat_conf = {}
@ -819,8 +822,13 @@ def app_makedefault(auth, app, domain=None):
ssowat_conf['redirected_urls'][domain +'/'] = app_domain + app_path ssowat_conf['redirected_urls'][domain +'/'] = app_domain + app_path
with open('/etc/ssowat/conf.json.persistent', 'w+') as f: try:
json.dump(ssowat_conf, f, sort_keys=True, indent=4) 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') 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')) raise MoulinetteError(errno.EINVAL, m18n.n('domain_unknown'))
if domain in apps_map: 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 # Loop through apps
for p, a in apps_map[domain].items(): for p, a in apps_map[domain].items():
# Skip requested app checking # Skip requested app checking
@ -922,7 +926,7 @@ def app_checkurl(auth, url, app=None):
if path == p: if path == p:
raise MoulinetteError(errno.EINVAL, raise MoulinetteError(errno.EINVAL,
m18n.n('app_location_already_used')) m18n.n('app_location_already_used'))
elif path.startswith(p): elif path.startswith(p) or p.startswith(path):
raise MoulinetteError(errno.EPERM, raise MoulinetteError(errno.EPERM,
m18n.n('app_location_install_failed')) 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_regex = { main_domain +'/yunohost[\/]?$': 'https://'+ main_domain +'/yunohost/sso/' }
redirected_urls ={} redirected_urls ={}
apps = {}
try: try:
apps_list = app_list()['apps'] apps_list = app_list()['apps']
except: except:
@ -1008,19 +1011,19 @@ def app_ssowatconf(auth):
for item in _get_setting(app_settings, 'skipped_uris'): for item in _get_setting(app_settings, 'skipped_uris'):
if item[-1:] == '/': if item[-1:] == '/':
item = 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'): for item in _get_setting(app_settings, 'skipped_regex'):
skipped_regex.append(item) skipped_regex.append(item)
for item in _get_setting(app_settings, 'unprotected_uris'): for item in _get_setting(app_settings, 'unprotected_uris'):
if item[-1:] == '/': if item[-1:] == '/':
item = 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'): for item in _get_setting(app_settings, 'unprotected_regex'):
unprotected_regex.append(item) unprotected_regex.append(item)
for item in _get_setting(app_settings, 'protected_uris'): for item in _get_setting(app_settings, 'protected_uris'):
if item[-1:] == '/': if item[-1:] == '/':
item = 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'): for item in _get_setting(app_settings, 'protected_regex'):
protected_regex.append(item) protected_regex.append(item)
if 'redirected_urls' in app_settings: if 'redirected_urls' in app_settings:
@ -1031,6 +1034,9 @@ def app_ssowatconf(auth):
for domain in domains: for domain in domains:
skipped_urls.extend([domain + '/yunohost/admin', domain + '/yunohost/api']) skipped_urls.extend([domain + '/yunohost/admin', domain + '/yunohost/api'])
# Authorize ACME challenge url
skipped_regex.append("^[^/]*/%.well%-known/acme%-challenge/.*$")
conf_dict = { conf_dict = {
'portal_domain': main_domain, 'portal_domain': main_domain,
'portal_path': '/yunohost/sso/', 'portal_path': '/yunohost/sso/',
@ -1259,8 +1265,13 @@ def _fetch_app_from_git(app):
url = url[:tree_index] url = url[:tree_index]
branch = app[tree_index+6:] branch = app[tree_index+6:]
try: 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([ subprocess.check_call([
'git', 'clone', '--depth=1', url, extracted_app_folder]) 'git', 'clone', '--depth=1', '--recursive', url,
extracted_app_folder])
subprocess.check_call([ subprocess.check_call([
'git', 'reset', '--hard', branch 'git', 'reset', '--hard', branch
], cwd=extracted_app_folder) ], 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 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 from yunohost.user import user_info
args_dict = OrderedDict() args_dict = OrderedDict()
@ -1526,6 +1537,13 @@ def _parse_args_from_manifest(manifest, action, args={}, auth=None):
# Check for a password argument # Check for a password argument
is_password = True if arg_type == 'password' else False 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: try:
input_string = msignals.prompt(ask_string, is_password) input_string = msignals.prompt(ask_string, is_password)
except NotImplementedError: except NotImplementedError:

View file

@ -25,7 +25,6 @@
""" """
import os import os
import re import re
import sys
import json import json
import errno import errno
import time import time
@ -48,7 +47,7 @@ from yunohost.hook import (
from yunohost.monitor import binary_to_human from yunohost.monitor import binary_to_human
from yunohost.tools import tools_postinstall from yunohost.tools import tools_postinstall
backup_path = '/home/yunohost.backup' backup_path = '/home/yunohost.backup'
archives_path = '%s/archives' % backup_path archives_path = '%s/archives' % backup_path
logger = getActionLogger('yunohost.backup') logger = getActionLogger('yunohost.backup')
@ -120,8 +119,10 @@ def backup_create(name=None, description=None, output_directory=None,
env_var['CAN_BIND'] = 0 env_var['CAN_BIND'] = 0
else: else:
output_directory = archives_path 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): def _clean_tmp_dir(retcode=0):
ret = hook_callback('post_backup_create', args=[tmp_dir, retcode]) 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, raise MoulinetteError(errno.EIO,
m18n.n('backup_archive_open_failed')) m18n.n('backup_archive_open_failed'))
# Add files to the arvhice # Add files to the archive
try: try:
tar.add(tmp_dir, arcname='') tar.add(tmp_dir, arcname='')
tar.close() tar.close()
@ -298,10 +299,20 @@ def backup_create(name=None, description=None, output_directory=None,
raise MoulinetteError(errno.EIO, raise MoulinetteError(errno.EIO,
m18n.n('backup_creation_failed')) 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 # Move info file
os.rename(tmp_dir + '/info.json', shutil.move(tmp_dir + '/info.json',
'{:s}/{:s}.info.json'.format(archives_path, name)) '{: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 # Clean temporary directory
if tmp_dir != output_directory: if tmp_dir != output_directory:
_clean_tmp_dir() _clean_tmp_dir()
@ -310,7 +321,7 @@ def backup_create(name=None, description=None, output_directory=None,
# Return backup info # Return backup info
info['name'] = name info['name'] = name
return { 'archive': info } return {'archive': info}
def backup_restore(auth, name, hooks=[], ignore_hooks=False, 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) d[a] = backup_info(a, human_readable=human_readable)
result = d result = d
return { 'archives': result } return {'archives': result}
def backup_info(name, with_details=False, human_readable=False): 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) 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, raise MoulinetteError(errno.EIO,
m18n.n('backup_archive_name_unknown', name=name)) 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) info_file = "%s/%s.info.json" % (archives_path, name)
try: try:
with open(info_file) as f: with open(info_file) as f:
# Retrieve backup info # Retrieve backup info
@ -620,7 +643,7 @@ def backup_info(name, with_details=False, human_readable=False):
size = info.get('size', 0) size = info.get('size', 0)
if not size: if not size:
tar = tarfile.open(archive_file, "r:gz") 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.getmembers())
tar.close() tar.close()
if human_readable: if human_readable:
@ -653,7 +676,7 @@ def backup_delete(name):
archive_file = '%s/%s.tar.gz' % (archives_path, name) archive_file = '%s/%s.tar.gz' % (archives_path, name)
info_file = "%s/%s.info.json" % (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): if not os.path.isfile(backup_file):
raise MoulinetteError(errno.EIO, raise MoulinetteError(errno.EIO,
m18n.n('backup_archive_name_unknown', name=backup_file)) m18n.n('backup_archive_name_unknown', name=backup_file))

893
src/yunohost/certificate.py Normal file
View file

@ -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)

View file

@ -24,19 +24,20 @@
Manage domains Manage domains
""" """
import os import os
import sys
import datetime import datetime
import re import re
import shutil
import json import json
import yaml import yaml
import errno import errno
import requests import requests
from urllib import urlopen from urllib import urlopen
from moulinette.core import MoulinetteError from moulinette.core import MoulinetteError
from moulinette.utils.log import getActionLogger from moulinette.utils.log import getActionLogger
import yunohost.certificate
from yunohost.service import service_regen_conf from yunohost.service import service_regen_conf
logger = getActionLogger('yunohost.domain') 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']) result = auth.search('ou=domains,dc=yunohost,dc=org', filter, ['virtualdomain'])
if len(result) > offset and limit > 0: 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]) result_list.append(domain['virtualdomain'][0])
return { 'domains': result_list } return {'domains': result_list}
def domain_add(auth, domain, dyndns=False): def domain_add(auth, domain, dyndns=False):
@ -82,7 +83,7 @@ def domain_add(auth, domain, dyndns=False):
""" """
from yunohost.hook import hook_callback from yunohost.hook import hook_callback
attr_dict = { 'objectClass' : ['mailDomain', 'top'] } attr_dict = {'objectClass': ['mailDomain', 'top']}
now = datetime.datetime.now() now = datetime.datetime.now()
timestamp = str(now.year) + str(now.month) + str(now.day) timestamp = str(now.year) + str(now.month) + str(now.day)
@ -102,7 +103,7 @@ def domain_add(auth, domain, dyndns=False):
pass pass
else: else:
dyndomains = json.loads(r.text) dyndomains = json.loads(r.text)
dyndomain = '.'.join(domain.split('.')[1:]) dyndomain = '.'.join(domain.split('.')[1:])
if dyndomain in dyndomains: if dyndomain in dyndomains:
if os.path.exists('/etc/cron.d/yunohost-dyndns'): if os.path.exists('/etc/cron.d/yunohost-dyndns'):
raise MoulinetteError(errno.EPERM, raise MoulinetteError(errno.EPERM,
@ -113,44 +114,13 @@ def domain_add(auth, domain, dyndns=False):
m18n.n('domain_dyndns_root_unknown')) m18n.n('domain_dyndns_root_unknown'))
try: try:
# Commands yunohost.certificate._certificate_install_selfsigned([domain], False)
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'))
try: try:
auth.validate_uniqueness({ 'virtualdomain': domain }) auth.validate_uniqueness({'virtualdomain': domain})
except MoulinetteError: except MoulinetteError:
raise MoulinetteError(errno.EEXIST, m18n.n('domain_exists')) raise MoulinetteError(errno.EEXIST, m18n.n('domain_exists'))
attr_dict['virtualdomain'] = domain attr_dict['virtualdomain'] = domain
if not auth.add('virtualdomain=%s,ou=domains' % domain, attr_dict): 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=[ service_regen_conf(names=[
'nginx', 'metronome', 'dnsmasq', 'rmilter']) 'nginx', 'metronome', 'dnsmasq', 'rmilter'])
os.system('yunohost app ssowatconf > /dev/null 2>&1') os.system('yunohost app ssowatconf > /dev/null 2>&1')
except IOError: pass except IOError:
pass
except: except:
# Force domain removal silently # Force domain removal silently
try: domain_remove(auth, domain, True) try:
except: pass domain_remove(auth, domain, True)
except:
pass
raise raise
hook_callback('post_domain_add', args=[domain]) 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']: if not force and domain not in domain_list(auth)['domains']:
raise MoulinetteError(errno.EINVAL, m18n.n('domain_unknown')) 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 # Check if apps are installed on the domain
for app in os.listdir('/etc/yunohost/apps/'): 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: try:
app_domain = yaml.load(f)['domain'] app_domain = yaml.load(f)['domain']
except: except:
@ -248,13 +225,13 @@ def domain_dns_conf(domain, ttl=None):
"muc {ttl} IN CNAME @\n" "muc {ttl} IN CNAME @\n"
"pubsub {ttl} IN CNAME @\n" "pubsub {ttl} IN CNAME @\n"
"vjud {ttl} IN CNAME @\n" "vjud {ttl} IN CNAME @\n"
).format(ttl=ttl, domain=domain) ).format(ttl=ttl, domain=domain)
# Email # Email
result += ('\n' result += ('\n'
'@ {ttl} IN MX 10 {domain}.\n' '@ {ttl} IN MX 10 {domain}.\n'
'@ {ttl} IN TXT "v=spf1 a mx ip4:{ip4}' '@ {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: if ip6 is not None:
result += ' ip6:{ip6}'.format(ip6=ip6) result += ' ip6:{ip6}'.format(ip6=ip6)
result += ' -all"' result += ' -all"'
@ -270,7 +247,7 @@ def domain_dns_conf(domain, ttl=None):
r'^(?P<host>[a-z_\-\.]+)[\s]+([0-9]+[\s]+)?IN[\s]+TXT[\s]+[^"]*' r'^(?P<host>[a-z_\-\.]+)[\s]+([0-9]+[\s]+)?IN[\s]+TXT[\s]+[^"]*'
'(?=.*(;[\s]*|")v=(?P<v>[^";]+))' '(?=.*(;[\s]*|")v=(?P<v>[^";]+))'
'(?=.*(;[\s]*|")k=(?P<k>[^";]+))' '(?=.*(;[\s]*|")k=(?P<k>[^";]+))'
'(?=.*(;[\s]*|")p=(?P<p>[^";]+))'), dkim_content, re.M|re.S '(?=.*(;[\s]*|")p=(?P<p>[^";]+))'), dkim_content, re.M | re.S
) )
if dkim: if dkim:
result += '\n{host}. {ttl} IN TXT "v={v}; k={k}; p={p}"'.format( 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 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): def get_public_ip(protocol=4):
"""Retrieve the public IP address from ip.yunohost.org""" """Retrieve the public IP address from ip.yunohost.org"""
if protocol == 4: if protocol == 4:
@ -301,3 +290,14 @@ def get_public_ip(protocol=4):
logger.debug('cannot retrieve public IPv%d' % protocol, exc_info=1) logger.debug('cannot retrieve public IPv%d' % protocol, exc_info=1)
raise MoulinetteError(errno.ENETUNREACH, raise MoulinetteError(errno.ENETUNREACH,
m18n.n('no_internet_connection')) 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)

View file

@ -94,8 +94,8 @@ def dyndns_subscribe(subscribe_host="dyndns.yunohost.org", domain=None, key=None
logger.info(m18n.n('dyndns_key_generating')) logger.info(m18n.n('dyndns_key_generating'))
os.system('cd /etc/yunohost/dyndns && ' \ os.system('cd /etc/yunohost/dyndns && '
'dnssec-keygen -a hmac-md5 -b 128 -n USER %s' % domain) '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') os.system('chmod 600 /etc/yunohost/dyndns/*.key /etc/yunohost/dyndns/*.private')
key_file = glob.glob('/etc/yunohost/dyndns/*.key')[0] 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 # Send subscription
try: 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: except requests.ConnectionError:
raise MoulinetteError(errno.ENETUNREACH, m18n.n('no_internet_connection')) raise MoulinetteError(errno.ENETUNREACH, m18n.n('no_internet_connection'))
if r.status_code != 201: if r.status_code != 201:
try: error = json.loads(r.text)['error'] try:
except: error = "Server error" error = json.loads(r.text)['error']
except:
error = "Server error"
raise MoulinetteError(errno.EPERM, raise MoulinetteError(errno.EPERM,
m18n.n('dyndns_registration_failed', error=error)) m18n.n('dyndns_registration_failed', error=error))
@ -204,33 +206,33 @@ def dyndns_update(dyn_host="dyndns.yunohost.org", domain=None, key=None,
lines = [ lines = [
'server %s' % dyn_host, 'server %s' % dyn_host,
'zone %s' % host, 'zone %s' % host,
'update delete %s. A' % domain, 'update delete %s. A' % domain,
'update delete %s. AAAA' % domain, 'update delete %s. AAAA' % domain,
'update delete %s. MX' % domain, 'update delete %s. MX' % domain,
'update delete %s. TXT' % domain, 'update delete %s. TXT' % domain,
'update delete pubsub.%s. A' % domain, 'update delete pubsub.%s. A' % domain,
'update delete pubsub.%s. AAAA' % 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 muc.%s. AAAA' % domain,
'update delete vjud.%s. A' % domain, 'update delete vjud.%s. A' % domain,
'update delete vjud.%s. AAAA' % domain, 'update delete vjud.%s. AAAA' % domain,
'update delete _xmpp-client._tcp.%s. SRV' % domain, 'update delete _xmpp-client._tcp.%s. SRV' % domain,
'update delete _xmpp-server._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 MX 5 %s.' % (domain, domain),
'update add %s. 14400 TXT "v=spf1 a mx -all"' % domain, 'update add %s. 14400 TXT "v=spf1 a mx -all"' % domain,
'update add pubsub.%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 muc.%s. 1800 A %s' % (domain, ipv4),
'update add vjud.%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-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) 'update add _xmpp-server._tcp.%s. 14400 SRV 0 5 5269 %s.' % (domain, domain)
] ]
if ipv6 is not None: if ipv6 is not None:
lines += [ 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 pubsub.%s. 1800 AAAA %s' % (domain, ipv6),
'update add muc.%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 vjud.%s. 1800 AAAA %s' % (domain, ipv6),
] ]
lines += [ lines += [
'show', 'show',

View file

@ -67,14 +67,14 @@ def firewall_allow(protocol, port, ipv4_only=False, ipv6_only=False,
# Validate protocols # Validate protocols
protocols = ['TCP', 'UDP'] protocols = ['TCP', 'UDP']
if protocol != 'Both' and protocol in protocols: if protocol != 'Both' and protocol in protocols:
protocols = [protocol,] protocols = [protocol, ]
# Validate IP versions # Validate IP versions
ipvs = ['ipv4', 'ipv6'] ipvs = ['ipv4', 'ipv6']
if ipv4_only and not ipv6_only: if ipv4_only and not ipv6_only:
ipvs = ['ipv4',] ipvs = ['ipv4', ]
elif ipv6_only and not ipv4_only: elif ipv6_only and not ipv4_only:
ipvs = ['ipv6',] ipvs = ['ipv6', ]
for p in protocols: for p in protocols:
# Iterate over IP versions to add port # Iterate over IP versions to add port
@ -117,18 +117,18 @@ def firewall_disallow(protocol, port, ipv4_only=False, ipv6_only=False,
# Validate protocols # Validate protocols
protocols = ['TCP', 'UDP'] protocols = ['TCP', 'UDP']
if protocol != 'Both' and protocol in protocols: if protocol != 'Both' and protocol in protocols:
protocols = [protocol,] protocols = [protocol, ]
# Validate IP versions and UPnP # Validate IP versions and UPnP
ipvs = ['ipv4', 'ipv6'] ipvs = ['ipv4', 'ipv6']
upnp = True upnp = True
if ipv4_only and ipv6_only: if ipv4_only and ipv6_only:
upnp = True # automatically disallow UPnP upnp = True # automatically disallow UPnP
elif ipv4_only: elif ipv4_only:
ipvs = ['ipv4',] ipvs = ['ipv4', ]
upnp = upnp_only upnp = upnp_only
elif ipv6_only: elif ipv6_only:
ipvs = ['ipv6',] ipvs = ['ipv6', ]
upnp = upnp_only upnp = upnp_only
elif upnp_only: elif upnp_only:
ipvs = [] 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'])) ports = sorted(set(ports['ipv4']) | set(ports['ipv6']))
# Format returned dict # Format returned dict
ret = { "opened_ports": ports } ret = {"opened_ports": ports}
if list_forwarded: if list_forwarded:
# Combine TCP and UDP forwarded ports # Combine TCP and UDP forwarded ports
ret['forwarded_ports'] = sorted( ret['forwarded_ports'] = sorted(
@ -224,8 +224,8 @@ def firewall_reload(skip_upnp=False):
# Iterate over ports and add rule # Iterate over ports and add rule
for protocol in ['TCP', 'UDP']: for protocol in ['TCP', 'UDP']:
for port in firewall['ipv4'][protocol]: for port in firewall['ipv4'][protocol]:
rules.append("iptables -w -A INPUT -p %s --dport %s -j ACCEPT" \ rules.append("iptables -w -A INPUT -p %s --dport %s -j ACCEPT"
% (protocol, process.quote(str(port)))) % (protocol, process.quote(str(port))))
rules += [ rules += [
"iptables -w -A INPUT -i lo -j ACCEPT", "iptables -w -A INPUT -i lo -j ACCEPT",
"iptables -w -A INPUT -p icmp -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 # Iterate over ports and add rule
for protocol in ['TCP', 'UDP']: for protocol in ['TCP', 'UDP']:
for port in firewall['ipv6'][protocol]: for port in firewall['ipv6'][protocol]:
rules.append("ip6tables -w -A INPUT -p %s --dport %s -j ACCEPT" \ rules.append("ip6tables -w -A INPUT -p %s --dport %s -j ACCEPT"
% (protocol, process.quote(str(port)))) % (protocol, process.quote(str(port))))
rules += [ rules += [
"ip6tables -w -A INPUT -i lo -j ACCEPT", "ip6tables -w -A INPUT -i lo -j ACCEPT",
"ip6tables -w -A INPUT -p icmpv6 -j ACCEPT", "ip6tables -w -A INPUT -p icmpv6 -j ACCEPT",
@ -308,13 +308,14 @@ def firewall_upnp(action='status', no_refresh=False):
try: try:
# Remove old cron job # Remove old cron job
os.remove('/etc/cron.d/yunohost-firewall') os.remove('/etc/cron.d/yunohost-firewall')
except: pass except:
pass
action = 'status' action = 'status'
no_refresh = False no_refresh = False
if action == 'status' and no_refresh: if action == 'status' and no_refresh:
# Only return current state # Only return current state
return { 'enabled': enabled } return {'enabled': enabled}
elif action == 'enable' or (enabled and action == 'status'): elif action == 'enable' or (enabled and action == 'status'):
# Add cron job # Add cron job
with open(upnp_cron_job, 'w+') as f: with open(upnp_cron_job, 'w+') as f:
@ -330,7 +331,8 @@ def firewall_upnp(action='status', no_refresh=False):
try: try:
# Remove cron job # Remove cron job
os.remove(upnp_cron_job) os.remove(upnp_cron_job)
except: pass except:
pass
enabled = False enabled = False
if action == 'status': if action == 'status':
no_refresh = True no_refresh = True
@ -364,7 +366,8 @@ def firewall_upnp(action='status', no_refresh=False):
if upnpc.getspecificportmapping(port, protocol): if upnpc.getspecificportmapping(port, protocol):
try: try:
upnpc.deleteportmapping(port, protocol) upnpc.deleteportmapping(port, protocol)
except: pass except:
pass
if not enabled: if not enabled:
continue continue
try: try:
@ -403,7 +406,7 @@ def firewall_upnp(action='status', no_refresh=False):
if action == 'enable' and not enabled: if action == 'enable' and not enabled:
raise MoulinetteError(errno.ENXIO, m18n.n('upnp_port_open_failed')) raise MoulinetteError(errno.ENXIO, m18n.n('upnp_port_open_failed'))
return { 'enabled': enabled } return {'enabled': enabled}
def firewall_stop(): def firewall_stop():
@ -444,12 +447,14 @@ def _get_ssh_port(default=22):
pass pass
return default return default
def _update_firewall_file(rules): def _update_firewall_file(rules):
"""Make a backup and write new rules to firewall file""" """Make a backup and write new rules to firewall file"""
os.system("cp {0} {0}.old".format(firewall_file)) os.system("cp {0} {0}.old".format(firewall_file))
with open(firewall_file, 'w') as f: with open(firewall_file, 'w') as f:
yaml.safe_dump(rules, f, default_flow_style=False) yaml.safe_dump(rules, f, default_flow_style=False)
def _on_rule_command_error(returncode, cmd, output): def _on_rule_command_error(returncode, cmd, output):
"""Callback for rules commands error""" """Callback for rules commands error"""
# Log error and continue commands execution # Log error and continue commands execution

View file

@ -24,11 +24,8 @@
Manage hooks Manage hooks
""" """
import os import os
import sys
import re import re
import json
import errno import errno
import subprocess
from glob import iglob from glob import iglob
from moulinette.core import MoulinetteError 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] != '/': if path[0] != '/':
path = os.path.realpath(path) path = os.path.realpath(path)
if not os.path.isfile(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 # Construct command variables
cmd_args = '' cmd_args = ''

View file

@ -35,7 +35,7 @@ import errno
import os import os
import dns.resolver import dns.resolver
import cPickle as pickle import cPickle as pickle
from datetime import datetime, timedelta from datetime import datetime
from moulinette.core import MoulinetteError from moulinette.core import MoulinetteError
from moulinette.utils.log import getActionLogger from moulinette.utils.log import getActionLogger
@ -44,8 +44,8 @@ from yunohost.domain import get_public_ip
logger = getActionLogger('yunohost.monitor') logger = getActionLogger('yunohost.monitor')
glances_uri = 'http://127.0.0.1:61209' glances_uri = 'http://127.0.0.1:61209'
stats_path = '/var/lib/yunohost/stats' stats_path = '/var/lib/yunohost/stats'
crontab_path = '/etc/cron.d/yunohost-monitor' 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) # Retrieve monitoring for unit(s)
for u in units: for u in units:
if u == 'io': if u == 'io':
## Define setter # Define setter
if len(units) > 1: if len(units) > 1:
def _set(dn, dvalue): def _set(dn, dvalue):
try: try:
result[dn][u] = dvalue result[dn][u] = dvalue
except KeyError: except KeyError:
result[dn] = { u: dvalue } result[dn] = {u: dvalue}
else: else:
def _set(dn, dvalue): def _set(dn, dvalue):
result[dn] = dvalue result[dn] = dvalue
@ -111,13 +111,13 @@ def monitor_disk(units=None, mountpoint=None, human_readable=False):
for dname in devices_names: for dname in devices_names:
_set(dname, 'not-available') _set(dname, 'not-available')
elif u == 'filesystem': elif u == 'filesystem':
## Define setter # Define setter
if len(units) > 1: if len(units) > 1:
def _set(dn, dvalue): def _set(dn, dvalue):
try: try:
result[dn][u] = dvalue result[dn][u] = dvalue
except KeyError: except KeyError:
result[dn] = { u: dvalue } result[dn] = {u: dvalue}
else: else:
def _set(dn, dvalue): def _set(dn, dvalue):
result[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') smtp_check = m18n.n('network_check_smtp_ko')
try: try:
answers = dns.resolver.query(domain,'MX') answers = dns.resolver.query(domain, 'MX')
mx_check = {} mx_check = {}
i = 0 i = 0
for server in answers: for server in answers:
mx_id = 'mx%s' %i mx_id = 'mx%s' % i
mx_check[mx_id] = server mx_check[mx_id] = server
i = i + 1 i = i + 1
except: except:
@ -307,7 +307,7 @@ def monitor_update_stats(period):
stats = _retrieve_stats(period) stats = _retrieve_stats(period)
if not stats: if not stats:
stats = { 'disk': {}, 'network': {}, 'system': {}, 'timestamp': [] } stats = {'disk': {}, 'network': {}, 'system': {}, 'timestamp': []}
monitor = None monitor = None
# Get monitoring stats # Get monitoring stats
@ -357,7 +357,7 @@ def monitor_update_stats(period):
if 'usage' in stats['network'] and iname in stats['network']['usage']: if 'usage' in stats['network'] and iname in stats['network']['usage']:
curr = stats['network']['usage'][iname] curr = stats['network']['usage'][iname]
net_usage[iname] = _append_to_stats(curr, values, 'time_since_update') 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 # Append system stats
for unit, values in monitor['system'].items(): 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' rules = ('*/5 * * * * root {cmd} day >> /dev/null\n'
'3 * * * * root {cmd} week >> /dev/null\n' '3 * * * * root {cmd} week >> /dev/null\n'
'6 */4 * * * root {cmd} month >> /dev/null').format( '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: with open(crontab_path, 'w') as f:
f.write(rules) f.write(rules)
@ -530,7 +530,7 @@ def binary_to_human(n, customary=False):
symbols = ('K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y') symbols = ('K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y')
prefix = {} prefix = {}
for i, s in enumerate(symbols): for i, s in enumerate(symbols):
prefix[s] = 1 << (i+1)*10 prefix[s] = 1 << (i + 1) * 10
for s in reversed(symbols): for s in reversed(symbols):
if n >= prefix[s]: if n >= prefix[s]:
value = float(n) / prefix[s] value = float(n) / prefix[s]
@ -590,7 +590,7 @@ def _save_stats(stats, period, date=None):
# Limit stats # Limit stats
if date is None: if date is None:
t = stats['timestamp'] 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]: if (t[len(t) - 1] - t[0]) > limit[period]:
begin = t[len(t) - 1] - limit[period] begin = t[len(t) - 1] - limit[period]
stats = _filter_stats(stats, begin) stats = _filter_stats(stats, begin)
@ -612,7 +612,7 @@ def _monitor_all(period=None, since=None):
since -- Timestamp of the stats beginning since -- Timestamp of the stats beginning
""" """
result = { 'disk': {}, 'network': {}, 'system': {} } result = {'disk': {}, 'network': {}, 'system': {}}
# Real-time stats # Real-time stats
if period == 'day' and since is None: if period == 'day' and since is None:
@ -697,7 +697,7 @@ def _calculate_stats_mean(stats):
s[k] = _mean(v, t, ts) s[k] = _mean(v, t, ts)
elif isinstance(v, list): elif isinstance(v, list):
try: try:
nums = [ float(x * t[i]) for i, x in enumerate(v) ] nums = [float(x * t[i]) for i, x in enumerate(v)]
except: except:
pass pass
else: else:

View file

@ -36,7 +36,7 @@ from difflib import unified_diff
from moulinette.core import MoulinetteError from moulinette.core import MoulinetteError
from moulinette.utils import log, filesystem 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' base_conf_path = '/home/yunohost.conf'
@ -60,9 +60,9 @@ def service_add(name, status=None, log=None, runlevel=None):
services = _get_services() services = _get_services()
if not status: if not status:
services[name] = { 'status': 'service' } services[name] = {'status': 'service'}
else: else:
services[name] = { 'status': status } services[name] = {'status': status}
if log is not None: if log is not None:
services[name]['log'] = log services[name]['log'] = log
@ -202,7 +202,7 @@ def service_status(names=[]):
status = None status = None
if 'status' not in services[name] or \ if 'status' not in services[name] or \
services[name]['status'] == 'service': services[name]['status'] == 'service':
status = 'service %s status' % name status = 'service %s status' % name
else: else:
status = str(services[name]['status']) status = str(services[name]['status'])
@ -211,7 +211,7 @@ def service_status(names=[]):
if 'runlevel' in services[name].keys(): if 'runlevel' in services[name].keys():
runlevel = int(services[name]['runlevel']) runlevel = int(services[name]['runlevel'])
result[name] = { 'status': 'unknown', 'loaded': 'unknown' } result[name] = {'status': 'unknown', 'loaded': 'unknown'}
# Retrieve service status # Retrieve service status
try: try:
@ -261,7 +261,7 @@ def service_log(name, number=50):
for log_path in log_list: for log_path in log_list:
if os.path.isdir(log_path): 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)) result[os.path.join(log_path, log)] = _tail(os.path.join(log_path, log), int(number))
else: else:
result[log_path] = _tail(log_path, int(number)) 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] common_args = [1 if force else 0, 1 if dry_run else 0]
# Execute hooks for pre-regen # Execute hooks for pre-regen
pre_args = ['pre',] + common_args pre_args = ['pre', ] + common_args
def _pre_call(name, priority, path, args): def _pre_call(name, priority, path, args):
# create the pending conf directory for the service # create the pending conf directory for the service
service_pending_path = os.path.join(pending_conf_dir, name) service_pending_path = os.path.join(pending_conf_dir, name)
filesystem.mkdir(service_pending_path, 0755, True, uid='admin') filesystem.mkdir(service_pending_path, 0755, True, uid='admin')
# return the arguments to pass to the script # 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) pre_result = hook_callback('conf_regen', names, pre_callback=_pre_call)
# Update the services name # 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 # Iterate over services and process pending conf
for service, conf_files in _get_pending_conf(names).items(): for service, conf_files in _get_pending_conf(names).items():
logger.info(m18n.n( logger.info(m18n.n(
'service_regenconf_pending_applying' if not dry_run else \ 'service_regenconf_pending_applying' if not dry_run else
'service_regenconf_dry_pending_applying', 'service_regenconf_dry_pending_applying',
service=service)) service=service))
conf_hashes = _get_conf_hashes(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 continue
elif not failed_regen: elif not failed_regen:
logger.success(m18n.n( logger.success(m18n.n(
'service_conf_updated' if not dry_run else \ 'service_conf_updated' if not dry_run else
'service_conf_would_be_updated', 'service_conf_would_be_updated',
service=service)) service=service))
if succeed_regen and not dry_run: if succeed_regen and not dry_run:
_update_conf_hashes(service, conf_hashes) _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 return result
# Execute hooks for post-regen # Execute hooks for post-regen
post_args = ['post',] + common_args post_args = ['post', ] + common_args
def _pre_call(name, priority, path, args): def _pre_call(name, priority, path, args):
# append coma-separated applied changes for the service # append coma-separated applied changes for the service
if name in result and result[name]['applied']: if name in result and result[name]['applied']:
regen_conf_files = ','.join(result[name]['applied'].keys()) regen_conf_files = ','.join(result[name]['applied'].keys())
else: else:
regen_conf_files = '' regen_conf_files = ''
return post_args + [regen_conf_files,] return post_args + [regen_conf_files, ]
hook_callback('conf_regen', names, pre_callback=_pre_call) hook_callback('conf_regen', names, pre_callback=_pre_call)
return result return result
@ -556,7 +558,8 @@ def _tail(file, n, offset=None):
return lines[-to_read:offset and -offset or None] return lines[-to_read:offset and -offset or None]
avg_line_length *= 1.3 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): def _get_files_diff(orig_file, new_file, as_string=False, skip_header=True):

View file

@ -24,14 +24,13 @@
Specific tools Specific tools
""" """
import os import os
import sys
import yaml import yaml
import re
import getpass
import requests import requests
import json import json
import errno import errno
import logging import logging
import subprocess
import pwd
from collections import OrderedDict from collections import OrderedDict
import apt import apt
@ -40,11 +39,11 @@ import apt.progress
from moulinette.core import MoulinetteError, init_authenticator from moulinette.core import MoulinetteError, init_authenticator
from moulinette.utils.log import getActionLogger from moulinette.utils.log import getActionLogger
from yunohost.app import app_fetchlist, app_info, app_upgrade, app_ssowatconf, app_list 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.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.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 from yunohost.utils.packages import ynh_packages_version
apps_setting_path= '/etc/yunohost/apps/' apps_setting_path= '/etc/yunohost/apps/'
@ -52,22 +51,34 @@ apps_setting_path= '/etc/yunohost/apps/'
logger = getActionLogger('yunohost.tools') logger = getActionLogger('yunohost.tools')
def tools_ldapinit(auth): def tools_ldapinit():
""" """
YunoHost LDAP initialization 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: with open('/usr/share/yunohost/yunohost-config/moulinette/ldap_scheme.yml') as f:
ldap_map = yaml.load(f) ldap_map = yaml.load(f)
for rdn, attr_dict in ldap_map['parents'].items(): for rdn, attr_dict in ldap_map['parents'].items():
try: auth.add(rdn, attr_dict) try:
except: pass auth.add(rdn, attr_dict)
except:
pass
for rdn, attr_dict in ldap_map['children'].items(): for rdn, attr_dict in ldap_map['children'].items():
try: auth.add(rdn, attr_dict) try:
except: pass auth.add(rdn, attr_dict)
except:
pass
admin_dict = { admin_dict = {
'cn': 'admin', 'cn': 'admin',
@ -83,8 +94,18 @@ def tools_ldapinit(auth):
auth.update('cn=admin', admin_dict) 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): def tools_adminpw(auth, new_password):
""" """
@ -104,56 +125,49 @@ def tools_adminpw(auth, new_password):
logger.success(m18n.n('admin_password_changed')) 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: Keyword argument:
new_domain new_domain -- The new domain to be set as the main domain
old_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: 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']: 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') # Apply changes to ssl certs
os.system('rm /etc/ssl/certs/yunohost_crt.pem') 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 = [ try:
'ln -s /etc/yunohost/certs/%s/key.pem /etc/ssl/private/yunohost_key.pem' % new_domain, if os.path.exists(ssl_key) or os.path.lexists(ssl_key):
'ln -s /etc/yunohost/certs/%s/crt.pem /etc/ssl/certs/yunohost_crt.pem' % new_domain, os.remove(ssl_key)
'echo %s > /etc/yunohost/current_host' % new_domain, if os.path.exists(ssl_crt) or os.path.lexists(ssl_crt):
] os.remove(ssl_crt)
for command in command_list: os.symlink(new_ssl_key, ssl_key)
if os.system(command) != 0: os.symlink(new_ssl_crt, ssl_crt)
raise MoulinetteError(errno.EPERM,
m18n.n('maindomain_change_failed'))
if dyndns and len(new_domain.split('.')) >= 3: _set_maindomain(new_domain)
try: except Exception as e:
r = requests.get('https://dyndns.yunohost.org/domains') logger.warning("%s" % e, exc_info=1)
except requests.ConnectionError: raise MoulinetteError(errno.EPERM, m18n.n('maindomain_change_failed'))
pass
else:
dyndomains = json.loads(r.text)
dyndomain = '.'.join(new_domain.split('.')[1:])
if dyndomain in dyndomains:
dyndns_subscribe(domain=new_domain)
# Regen configurations
try: try:
with open('/etc/yunohost/installed', 'r') as f: with open('/etc/yunohost/installed', 'r') as f:
service_regen_conf() service_regen_conf()
except IOError: pass except IOError:
pass
logger.success(m18n.n('maindomain_changed')) logger.success(m18n.n('maindomain_changed'))
@ -164,7 +178,8 @@ def tools_postinstall(domain, password, ignore_dyndns=False):
Keyword argument: Keyword argument:
domain -- YunoHost main domain 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 password -- YunoHost admin password
""" """
@ -182,25 +197,23 @@ def tools_postinstall(domain, password, ignore_dyndns=False):
else: else:
dyndomains = json.loads(r.text) dyndomains = json.loads(r.text)
dyndomain = '.'.join(domain.split('.')[1:]) dyndomain = '.'.join(domain.split('.')[1:])
if dyndomain in dyndomains: if dyndomain in dyndomains:
if requests.get('https://dyndns.yunohost.org/test/%s' % domain).status_code == 200: if requests.get('https://dyndns.yunohost.org/test/%s' % domain).status_code == 200:
dyndns = True dyndns = True
else: else:
raise MoulinetteError(errno.EEXIST, raise MoulinetteError(errno.EEXIST,
m18n.n('dyndns_unavailable')) m18n.n('dyndns_unavailable'))
else:
dyndns = False
else:
dyndns = False
logger.info(m18n.n('yunohost_installing')) 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 # Initialize LDAP for YunoHost
# TODO: Improve this part by integrate ldapinit into conf_regen hook # TODO: Improve this part by integrate ldapinit into conf_regen hook
tools_ldapinit(auth) auth = tools_ldapinit()
# Create required folders # Create required folders
folders_to_create = [ folders_to_create = [
@ -212,8 +225,10 @@ def tools_postinstall(domain, password, ignore_dyndns=False):
] ]
for folder in folders_to_create: for folder in folders_to_create:
try: os.listdir(folder) try:
except OSError: os.makedirs(folder) os.listdir(folder)
except OSError:
os.makedirs(folder)
# Change folders permissions # Change folders permissions
os.system('chmod 755 /home/yunohost.app') os.system('chmod 755 /home/yunohost.app')
@ -226,6 +241,9 @@ def tools_postinstall(domain, password, ignore_dyndns=False):
try: try:
with open('/etc/ssowat/conf.json.persistent') as json_conf: with open('/etc/ssowat/conf.json.persistent') as json_conf:
ssowat_conf = json.loads(str(json_conf.read())) 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: except IOError:
ssowat_conf = {} ssowat_conf = {}
@ -234,8 +252,13 @@ def tools_postinstall(domain, password, ignore_dyndns=False):
ssowat_conf['redirected_urls']['/'] = domain +'/yunohost/admin' ssowat_conf['redirected_urls']['/'] = domain +'/yunohost/admin'
with open('/etc/ssowat/conf.json.persistent', 'w+') as f: try:
json.dump(ssowat_conf, f, sort_keys=True, indent=4) 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') 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')) m18n.n('yunohost_ca_creation_failed'))
# New domain config # 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 # Generate SSOwat configuration file
app_ssowatconf(auth) app_ssowatconf(auth)
@ -277,7 +301,6 @@ def tools_postinstall(domain, password, ignore_dyndns=False):
os.system('service yunohost-firewall start') os.system('service yunohost-firewall start')
service_regen_conf(force=True) service_regen_conf(force=True)
logger.success(m18n.n('yunohost_configured')) 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')) logger.info(m18n.n('updating_apt_cache'))
if not cache.update(): if not cache.update():
raise MoulinetteError(errno.EPERM, m18n.n('update_cache_failed')) raise MoulinetteError(errno.EPERM, m18n.n('update_cache_failed'))
logger.info(m18n.n('done')) logger.info(m18n.n('done'))
cache.open(None) cache.open(None)
@ -345,7 +369,7 @@ def tools_update(ignore_apps=False, ignore_packages=False):
if len(apps) == 0 and len(packages) == 0: if len(apps) == 0 and len(packages) == 0:
logger.info(m18n.n('packages_no_upgrade')) 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): 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) critical_upgrades.add(pkg.name)
# Temporarily keep package ... # Temporarily keep package ...
pkg.mark_keep() pkg.mark_keep()
# ... and set a hourly cron up to upgrade critical packages # ... and set a hourly cron up to upgrade critical packages
if critical_upgrades: if critical_upgrades:
logger.info(m18n.n('packages_upgrade_critical_later', 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(): if cache.get_changes():
logger.info(m18n.n('upgrading_packages')) logger.info(m18n.n('upgrading_packages'))
try: try:
# Apply APT changes # Apply APT changes
# TODO: Logs output for the API # TODO: Logs output for the API
@ -394,7 +420,7 @@ def tools_upgrade(auth, ignore_apps=False, ignore_packages=False):
apt.progress.base.InstallProgress()) apt.progress.base.InstallProgress())
except Exception as e: except Exception as e:
failure = True 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')) logger.error(m18n.n('packages_upgrade_failed'))
else: else:
logger.info(m18n.n('done')) logger.info(m18n.n('done'))
@ -406,7 +432,7 @@ def tools_upgrade(auth, ignore_apps=False, ignore_packages=False):
app_upgrade(auth) app_upgrade(auth)
except Exception as e: except Exception as e:
failure = True 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')) logger.error(m18n.n('app_upgrade_failed'))
if not failure: 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 # Return API logs if it is an API call
if is_api: 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): def tools_diagnosis(auth, private=False):
@ -473,6 +499,7 @@ def tools_diagnosis(auth, private=False):
# Services status # Services status
services = service_status() services = service_status()
diagnosis['services'] = {} diagnosis['services'] = {}
for service in services: for service in services:
diagnosis['services'][service] = "%s (%s)" % (services[service]['status'], services[service]['loaded']) diagnosis['services'][service] = "%s (%s)" % (services[service]['status'], services[service]['loaded'])

View file

@ -30,11 +30,11 @@ import string
import json import json
import errno import errno
import subprocess import subprocess
import math
import re import re
from moulinette.core import MoulinetteError from moulinette.core import MoulinetteError
from moulinette.utils.log import getActionLogger from moulinette.utils.log import getActionLogger
from yunohost.service import service_status
logger = getActionLogger('yunohost.user') logger = getActionLogger('yunohost.user')
@ -50,12 +50,12 @@ def user_list(auth, fields=None, filter=None, limit=None, offset=None):
fields -- fields to fetch fields -- fields to fetch
""" """
user_attrs = { 'uid': 'username', user_attrs = {'uid': 'username',
'cn': 'fullname', 'cn': 'fullname',
'mail': 'mail', 'mail': 'mail',
'maildrop': 'mail-forward', 'maildrop': 'mail-forward',
'mailuserquota': 'mailbox-quota' } 'mailuserquota': 'mailbox-quota'}
attrs = [ 'uid' ] attrs = ['uid']
users = {} users = {}
# Set default arguments values # Set default arguments values
@ -74,12 +74,12 @@ def user_list(auth, fields=None, filter=None, limit=None, offset=None):
raise MoulinetteError(errno.EINVAL, raise MoulinetteError(errno.EINVAL,
m18n.n('field_invalid', attr)) m18n.n('field_invalid', attr))
else: else:
attrs = [ 'uid', 'cn', 'mail', 'mailuserquota' ] attrs = ['uid', 'cn', 'mail', 'mailuserquota']
result = auth.search('ou=users,dc=yunohost,dc=org', filter, attrs) result = auth.search('ou=users,dc=yunohost,dc=org', filter, attrs)
if len(result) > offset and limit > 0: if len(result) > offset and limit > 0:
for user in result[offset:offset+limit]: for user in result[offset:offset + limit]:
entry = {} entry = {}
for attr, values in user.items(): for attr, values in user.items():
try: try:
@ -88,11 +88,11 @@ def user_list(auth, fields=None, filter=None, limit=None, offset=None):
pass pass
uid = entry[user_attrs['uid']] uid = entry[user_attrs['uid']]
users[uid] = entry users[uid] = entry
return { 'users' : users } return {'users': users}
def user_create(auth, username, firstname, lastname, mail, password, def user_create(auth, username, firstname, lastname, mail, password,
mailbox_quota=0): mailbox_quota="0"):
""" """
Create user Create user
@ -112,8 +112,8 @@ def user_create(auth, username, firstname, lastname, mail, password,
# Validate uniqueness of username and mail in LDAP # Validate uniqueness of username and mail in LDAP
auth.validate_uniqueness({ auth.validate_uniqueness({
'uid' : username, 'uid': username,
'mail' : mail 'mail': mail
}) })
# Validate uniqueness of username in system users # 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')) raise MoulinetteError(errno.EEXIST, m18n.n('system_username_exists'))
# Check that the mail domain 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, raise MoulinetteError(errno.EINVAL,
m18n.n('mail_domain_unknown', m18n.n('mail_domain_unknown',
domain=mail[mail.find('@')+1:])) domain=mail[mail.find('@') + 1:]))
# Get random UID/GID # Get random UID/GID
uid_check = gid_check = 0 uid_check = gid_check = 0
@ -141,24 +141,24 @@ def user_create(auth, username, firstname, lastname, mail, password,
fullname = '%s %s' % (firstname, lastname) fullname = '%s %s' % (firstname, lastname)
rdn = 'uid=%s,ou=users' % username rdn = 'uid=%s,ou=users' % username
char_set = string.ascii_uppercase + string.digits char_set = string.ascii_uppercase + string.digits
salt = ''.join(random.sample(char_set,8)) salt = ''.join(random.sample(char_set, 8))
salt = '$1$' + salt + '$' salt = '$1$' + salt + '$'
user_pwd = '{CRYPT}' + crypt.crypt(str(password), salt) user_pwd = '{CRYPT}' + crypt.crypt(str(password), salt)
attr_dict = { attr_dict = {
'objectClass' : ['mailAccount', 'inetOrgPerson', 'posixAccount'], 'objectClass': ['mailAccount', 'inetOrgPerson', 'posixAccount'],
'givenName' : firstname, 'givenName': firstname,
'sn' : lastname, 'sn': lastname,
'displayName' : fullname, 'displayName': fullname,
'cn' : fullname, 'cn': fullname,
'uid' : username, 'uid': username,
'mail' : mail, 'mail': mail,
'maildrop' : username, 'maildrop': username,
'mailuserquota' : mailbox_quota, 'mailuserquota': mailbox_quota,
'userPassword' : user_pwd, 'userPassword': user_pwd,
'gidNumber' : uid, 'gidNumber': uid,
'uidNumber' : uid, 'uidNumber': uid,
'homeDirectory' : '/home/' + username, 'homeDirectory': '/home/' + username,
'loginShell' : '/bin/false' 'loginShell': '/bin/false'
} }
# If it is the first user, add some aliases # 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: with open('/etc/yunohost/current_host') as f:
main_domain = f.readline().rstrip() main_domain = f.readline().rstrip()
aliases = [ aliases = [
'root@'+ main_domain, 'root@' + main_domain,
'admin@'+ main_domain, 'admin@' + main_domain,
'webmaster@'+ main_domain, 'webmaster@' + main_domain,
'postmaster@'+ 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 # If exists, remove the redirection from the SSO
try: try:
with open('/etc/ssowat/conf.json.persistent') as json_conf: with open('/etc/ssowat/conf.json.persistent') as json_conf:
ssowat_conf = json.loads(str(json_conf.read())) 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']: if 'redirected_urls' in ssowat_conf and '/' in ssowat_conf['redirected_urls']:
del ssowat_conf['redirected_urls']['/'] del ssowat_conf['redirected_urls']['/']
try:
with open('/etc/ssowat/conf.json.persistent', 'w+') as f: with open('/etc/ssowat/conf.json.persistent', 'w+') as f:
json.dump(ssowat_conf, f, sort_keys=True, indent=4) json.dump(ssowat_conf, f, sort_keys=True, indent=4)
except IOError as e:
except IOError: pass raise MoulinetteError(errno.EPERM,
m18n.n('ssowat_persistent_conf_write_error', error=e.strerror))
if auth.add(rdn, attr_dict): if auth.add(rdn, attr_dict):
# Invalidate passwd to take user creation into account # 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 # Update SFTP user group
memberlist = auth.search(filter='cn=sftpusers', attrs=['memberUid'])[0]['memberUid'] memberlist = auth.search(filter='cn=sftpusers', attrs=['memberUid'])[0]['memberUid']
memberlist.append(username) memberlist.append(username)
if auth.update('cn=sftpusers,ou=groups', { 'memberUid': memberlist }): if auth.update('cn=sftpusers,ou=groups', {'memberUid': memberlist}):
try: try:
# Attempt to create user home folder # Attempt to create user home folder
subprocess.check_call( subprocess.check_call(
@ -204,12 +209,12 @@ def user_create(auth, username, firstname, lastname, mail, password,
logger.warning(m18n.n('user_home_creation_failed'), logger.warning(m18n.n('user_home_creation_failed'),
exc_info=1) exc_info=1)
app_ssowatconf(auth) app_ssowatconf(auth)
#TODO: Send a welcome mail to user # TODO: Send a welcome mail to user
logger.success(m18n.n('user_created')) logger.success(m18n.n('user_created'))
hook_callback('post_user_create', hook_callback('post_user_create',
args=[username, mail, password, firstname, lastname]) 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')) raise MoulinetteError(169, m18n.n('user_creation_failed'))
@ -232,9 +237,11 @@ def user_delete(auth, username, purge=False):
# Update SFTP user group # Update SFTP user group
memberlist = auth.search(filter='cn=sftpusers', attrs=['memberUid'])[0]['memberUid'] memberlist = auth.search(filter='cn=sftpusers', attrs=['memberUid'])[0]['memberUid']
try: memberlist.remove(username) try:
except: pass memberlist.remove(username)
if auth.update('cn=sftpusers,ou=groups', { 'memberUid': memberlist }): except:
pass
if auth.update('cn=sftpusers,ou=groups', {'memberUid': memberlist}):
if purge: if purge:
subprocess.call(['rm', '-rf', '/home/{0}'.format(username)]) subprocess.call(['rm', '-rf', '/home/{0}'.format(username)])
else: else:
@ -280,11 +287,11 @@ def user_update(auth, username, firstname=None, lastname=None, mail=None,
# Get modifications from arguments # Get modifications from arguments
if firstname: 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] new_attr_dict['cn'] = new_attr_dict['displayName'] = firstname + ' ' + user['sn'][0]
if lastname: 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 new_attr_dict['cn'] = new_attr_dict['displayName'] = user['givenName'][0] + ' ' + lastname
if lastname and firstname: if lastname and firstname:
@ -292,34 +299,34 @@ def user_update(auth, username, firstname=None, lastname=None, mail=None,
if change_password: if change_password:
char_set = string.ascii_uppercase + string.digits char_set = string.ascii_uppercase + string.digits
salt = ''.join(random.sample(char_set,8)) salt = ''.join(random.sample(char_set, 8))
salt = '$1$' + salt + '$' salt = '$1$' + salt + '$'
new_attr_dict['userPassword'] = '{CRYPT}' + crypt.crypt(str(change_password), salt) new_attr_dict['userPassword'] = '{CRYPT}' + crypt.crypt(str(change_password), salt)
if mail: if mail:
auth.validate_uniqueness({ 'mail': mail }) auth.validate_uniqueness({'mail': mail})
if mail[mail.find('@')+1:] not in domains: if mail[mail.find('@') + 1:] not in domains:
raise MoulinetteError(errno.EINVAL, raise MoulinetteError(errno.EINVAL,
m18n.n('mail_domain_unknown', m18n.n('mail_domain_unknown',
domain=mail[mail.find('@')+1:])) domain=mail[mail.find('@') + 1:]))
del user['mail'][0] del user['mail'][0]
new_attr_dict['mail'] = [mail] + user['mail'] new_attr_dict['mail'] = [mail] + user['mail']
if add_mailalias: if add_mailalias:
if not isinstance(add_mailalias, list): if not isinstance(add_mailalias, list):
add_mailalias = [ add_mailalias ] add_mailalias = [add_mailalias]
for mail in add_mailalias: for mail in add_mailalias:
auth.validate_uniqueness({ 'mail': mail }) auth.validate_uniqueness({'mail': mail})
if mail[mail.find('@')+1:] not in domains: if mail[mail.find('@') + 1:] not in domains:
raise MoulinetteError(errno.EINVAL, raise MoulinetteError(errno.EINVAL,
m18n.n('mail_domain_unknown', m18n.n('mail_domain_unknown',
domain=mail[mail.find('@')+1:])) domain=mail[mail.find('@') + 1:]))
user['mail'].append(mail) user['mail'].append(mail)
new_attr_dict['mail'] = user['mail'] new_attr_dict['mail'] = user['mail']
if remove_mailalias: if remove_mailalias:
if not isinstance(remove_mailalias, list): if not isinstance(remove_mailalias, list):
remove_mailalias = [ remove_mailalias ] remove_mailalias = [remove_mailalias]
for mail in remove_mailalias: for mail in remove_mailalias:
if len(user['mail']) > 1 and mail in user['mail'][1:]: if len(user['mail']) > 1 and mail in user['mail'][1:]:
user['mail'].remove(mail) user['mail'].remove(mail)
@ -330,7 +337,7 @@ def user_update(auth, username, firstname=None, lastname=None, mail=None,
if add_mailforward: if add_mailforward:
if not isinstance(add_mailforward, list): if not isinstance(add_mailforward, list):
add_mailforward = [ add_mailforward ] add_mailforward = [add_mailforward]
for mail in add_mailforward: for mail in add_mailforward:
if mail in user['maildrop'][1:]: if mail in user['maildrop'][1:]:
continue continue
@ -339,7 +346,7 @@ def user_update(auth, username, firstname=None, lastname=None, mail=None,
if remove_mailforward: if remove_mailforward:
if not isinstance(remove_mailforward, list): if not isinstance(remove_mailforward, list):
remove_mailforward = [ remove_mailforward ] remove_mailforward = [remove_mailforward]
for mail in remove_mailforward: for mail in remove_mailforward:
if len(user['maildrop']) > 1 and mail in user['maildrop'][1:]: if len(user['maildrop']) > 1 and mail in user['maildrop'][1:]:
user['maildrop'].remove(mail) 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 new_attr_dict['mailuserquota'] = mailbox_quota
if auth.update('uid=%s,ou=users' % username, new_attr_dict): if auth.update('uid=%s,ou=users' % username, new_attr_dict):
logger.success(m18n.n('user_updated')) logger.success(m18n.n('user_updated'))
app_ssowatconf(auth) app_ssowatconf(auth)
return user_info(auth, username) return user_info(auth, username)
else: else:
raise MoulinetteError(169, m18n.n('user_update_failed')) raise MoulinetteError(169, m18n.n('user_update_failed'))
def user_info(auth, username): def user_info(auth, username):
@ -372,9 +379,9 @@ def user_info(auth, username):
] ]
if len(username.split('@')) is 2: if len(username.split('@')) is 2:
filter = 'mail='+ username filter = 'mail=' + username
else: else:
filter = 'uid='+ username filter = 'uid=' + username
result = auth.search('ou=users,dc=yunohost,dc=org', filter, user_attrs) 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:] result_dict['mail-forward'] = user['maildrop'][1:]
if 'mailuserquota' in user: if 'mailuserquota' in user:
if user['mailuserquota'][0] != '0': userquota = user['mailuserquota'][0]
cmd = 'doveadm -f flow quota get -u %s' % user['uid'][0]
userquota = subprocess.check_output(cmd,stderr=subprocess.STDOUT, if isinstance(userquota, int):
shell=True) userquota = str(userquota)
quotavalue = re.findall(r'\d+', userquota)
result = '%s (%s%s)' % ( _convertSize(eval(quotavalue[0])), # Test if userquota is '0' or '0M' ( quota pattern is ^(\d+[bkMGT])|0$ )
quotavalue[2], '%') is_limited = not re.match('0[bkMGT]?', userquota)
result_dict['mailbox-quota'] = { storage_use = '?'
'limit' : user['mailuserquota'][0],
'use' : result if service_status("dovecot")["status"] != "running":
} logger.warning(m18n.n('mailbox_used_space_dovecot_down'))
else: 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: if result:
return result_dict return result_dict
else: else:
raise MoulinetteError(167, m18n.n('user_info_failed')) raise MoulinetteError(167, m18n.n('user_info_failed'))
def _convertSize(num, suffix=''): 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: if abs(num) < 1024.0:
return "%3.1f%s%s" % (num, unit, suffix) return "%3.1f%s%s" % (num, unit, suffix)
num /= 1024.0 num /= 1024.0

View file

@ -424,6 +424,7 @@ def get_installed_version(*pkgnames, **kwargs):
return versions[pkgnames[0]] return versions[pkgnames[0]]
return versions return versions
def meets_version_specifier(pkgname, specifier): def meets_version_specifier(pkgname, specifier):
"""Check if a package installed version meets specifier""" """Check if a package installed version meets specifier"""
spec = SpecifierSet(specifier) spec = SpecifierSet(specifier)

0
src/yunohost/vendor/__init__.py vendored Normal file
View file

View file

View file

@ -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:])

4
tests/test_actionmap.py Normal file
View file

@ -0,0 +1,4 @@
import yaml
def test_yaml_syntax():
yaml.load(open("data/actionsmap/yunohost.yml"))