diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 000000000..c3b460087
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,4 @@
+# These are supported funding model platforms
+
+custom: https://donate.yunohost.org
+liberapay: YunoHost
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 000000000..05aafe43b
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,101 @@
+stages:
+ - postinstall
+ - tests
+
+.tests:
+ image: after-postinstall
+ before_script:
+ - apt-get install python-pip -y
+ - mkdir -p .pip
+ - pip install -U pip
+ - hash -d pip
+ - pip --cache-dir=.pip install pytest pytest-sugar pytest-mock requests-mock mock
+ - pushd src/yunohost/tests
+ - >
+ if [ ! -d "./apps" ]; then
+ git clone https://github.com/YunoHost/test_apps ./apps
+ fi
+ - cd apps
+ - git pull > /dev/null 2>&1
+ - popd
+ - export PYTEST_ADDOPTS="--color=yes"
+ cache:
+ paths:
+ - .pip
+ - src/yunohost/tests/apps
+ key: "$CI_JOB_STAGE-$CI_COMMIT_REF_SLUG"
+
+postinstall:
+ image: before-postinstall
+ stage: postinstall
+ script:
+ - yunohost tools postinstall -d domain.tld -p the_password --ignore-dyndns
+
+root-tests:
+ extends: .tests
+ stage: tests
+ script:
+ - py.test tests
+
+test-apps:
+ extends: .tests
+ stage: tests
+ script:
+ - cd src/yunohost
+ - py.test tests/test_apps.py
+
+test-appscatalog:
+ extends: .tests
+ stage: tests
+ script:
+ - cd src/yunohost
+ - py.test tests/test_appscatalog.py
+
+test-appurl:
+ extends: .tests
+ stage: tests
+ script:
+ - cd src/yunohost
+ - py.test tests/test_appurl.py
+
+test-backuprestore:
+ extends: .tests
+ stage: tests
+ script:
+ - cd src/yunohost
+ - py.test tests/test_backuprestore.py
+
+test-changeurl:
+ extends: .tests
+ stage: tests
+ script:
+ - cd src/yunohost
+ - py.test tests/test_changeurl.py
+
+test-permission:
+ extends: .tests
+ stage: tests
+ script:
+ - cd src/yunohost
+ - py.test tests/test_permission.py
+
+test-settings:
+ extends: .tests
+ stage: tests
+ script:
+ - cd src/yunohost
+ - py.test tests/test_settings.py
+
+test-user-group:
+ extends: .tests
+ stage: tests
+ script:
+ - cd src/yunohost
+ - py.test tests/test_user-group.py
+
+test-regenconf:
+ extends: .tests
+ stage: tests
+ script:
+ - cd src/yunohost
+ - py.test tests/test_regenconf.py
diff --git a/README.md b/README.md
index b860c27d7..aec5300e2 100644
--- a/README.md
+++ b/README.md
@@ -1,48 +1,43 @@
+
+
+
+
+YunoHost
+
+
+
[](https://travis-ci.org/YunoHost/yunohost)
[](https://github.com/YunoHost/yunohost/blob/stretch-unstable/LICENSE)
+[](https://mastodon.social/@yunohost)
-# YunoHost core
+
-This repository is the core of YunoHost code.
+YunoHost is an operating system aiming to simplify as much as possible the administration of a server.
+This repository corresponds to the core code of YunoHost, mainly written in Python and Bash.
+
+- [Project features](https://yunohost.org/#/whatsyunohost)
- [Project website](https://yunohost.org)
-- [Bugtracker](https://github.com/YunoHost/issues).
+- [Install documentation](https://yunohost.org/install)
+- [Issue tracker](https://github.com/YunoHost/issues)
+
+# Screenshots
+
+Webadmin ([Yunohost-Admin](https://github.com/YunoHost/yunohost-admin)) | Single sign-on user portal ([SSOwat](https://github.com/YunoHost/ssowat))
+--- | ---
+ | 
+
## Contributing
-- You can develop on this repository using [ynh-dev](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 ← unstable ← your_branch`.
-- Note: If you modify Python scripts, you will have to modifiy the actions map.
+- You can learn how to get started with developing on YunoHost by reading [this piece of documentation](https://yunohost.org/dev).
+- Come chat with us on the [dev chatroom](https://yunohost.org/#/chat_rooms) !
- You can help translate YunoHost on our [translation platform](https://translate.yunohost.org/engage/yunohost/?utm_source=widget)
+
-
-
-## Repository content
-
-- [YunoHost core Python 2.7 scripts](./src/yunohost).
-- [An actionsmap](./data/actionsmap/yunohost.yml) used by moulinette.
-- [Services configuration templates](./data/templates).
-- [Hooks](./data/hooks).
-- [Locales](./locales) for translations of `yunohost` command.
-- [Shell helpers](./helpers.d) for [application packaging](https://yunohost.org/#/packaging_apps_helpers_en).
-- [Modules for the XMPP server Metronome](./lib/metronome/modules).
-- [Debian files](./debian) for package creation.
-
-## How does it work?
-
-- 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 Stretch](https://www.debian.org/releases/stretch)
+
## License
-As [other components of YunoHost core code](https://yunohost.org/#/faq_en), this repository is licensed GNU AGPL v3.
+As [other components of YunoHost](https://yunohost.org/#/faq_en), this repository is licensed under GNU AGPL v3.
diff --git a/bin/yunohost b/bin/yunohost
index 10a21a9da..b640c8c52 100755
--- a/bin/yunohost
+++ b/bin/yunohost
@@ -179,6 +179,10 @@ def _retrieve_namespaces():
ret.append(n)
return ret
+# Stupid PATH management because sometimes (e.g. some cron job) PATH is only /usr/bin:/bin ...
+default_path = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
+if os.environ["PATH"] != default_path:
+ os.environ["PATH"] = default_path + ":" + os.environ["PATH"]
# Main action ----------------------------------------------------------
diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml
index cbb7756b0..0ad1268f2 100644
--- a/data/actionsmap/yunohost.yml
+++ b/data/actionsmap/yunohost.yml
@@ -201,24 +201,34 @@ user:
subcategories:
group:
- subcategory_help: Manage group
+ subcategory_help: Manage user groups
actions:
### user_group_list()
list:
- action_help: List group
+ action_help: List existing groups
api: GET /users/groups
arguments:
- --fields:
- help: fields to fetch
- nargs: "+"
+ -s:
+ full: --short
+ help: List only the names of groups
+ action: store_true
+ -f:
+ full: --full
+ help: Display all informations known about each groups
+ action: store_true
+ -p:
+ full: --include-primary-groups
+ help: Also display primary groups (each user has an eponym group that only contains itself)
+ action: store_true
+ default: false
- ### user_group_add()
- add:
+ ### user_group_create()
+ create:
action_help: Create group
api: POST /users/groups
arguments:
groupname:
- help: The unique group name to add
+ help: Name of the group to be created
extra:
pattern: &pattern_groupname
- !!str ^[a-z0-9_]+$
@@ -230,7 +240,7 @@ user:
api: DELETE /users/groups/
arguments:
groupname:
- help: Username to delete
+ help: Name of the group to be deleted
extra:
pattern: *pattern_groupname
@@ -240,19 +250,19 @@ user:
api: PUT /users/groups/
arguments:
groupname:
- help: Username to update
+ help: Name of the group to be updated
extra:
pattern: *pattern_groupname
-a:
- full: --add-user
- help: User to add in group
+ full: --add
+ help: User(s) to add in the group
nargs: "*"
metavar: USERNAME
extra:
pattern: *pattern_username
-r:
- full: --remove-user
- help: User to remove in group
+ full: --remove
+ help: User(s) to remove in the group
nargs: "*"
metavar: USERNAME
extra:
@@ -260,112 +270,69 @@ user:
### user_group_info()
info:
- action_help: Get group information
+ action_help: Get information about a specific group
api: GET /users/groups/
arguments:
groupname:
- help: Groupname to get information
+ help: Name of the group to fetch info about
extra:
pattern: *pattern_username
permission:
- subcategory_help: Manage user permission
+ subcategory_help: Manage permissions
actions:
+
### user_permission_list()
list:
- action_help: List access to user and group
- api: GET /users/permissions/
+ action_help: List permissions and corresponding accesses
+ api: GET /users/permissions
arguments:
+ -s:
+ full: --short
+ help: Only list permission names
+ action: store_true
+ -f:
+ full: --full
+ help: Display all info known about each permission, including the full user list of each group it is granted to.
+ action: store_true
+
+ ### user_permission_info()
+ info:
+ action_help: Get information about a specific permission
+ api: GET /users/permissions/
+ arguments:
+ permission:
+ help: Name of the permission to fetch info about
+
+ ### user_permission_update()
+ update:
+ action_help: Manage group or user permissions
+ api: PUT /users/permissions/
+ arguments:
+ permission:
+ help: Permission to manage (e.g. mail or nextcloud or wordpress.editors)
-a:
- full: --app
- help: Application to manage the permission
+ full: --add
+ help: Group or usernames to grant this permission to
nargs: "*"
- metavar: APP
- -p:
- full: --permission
- help: Name of permission (main by default)
- nargs: "*"
- metavar: PERMISSION
- -u:
- full: --username
- help: Username
- nargs: "*"
- metavar: USER
- -g:
- full: --group
- help: Group name
- nargs: "*"
- metavar: GROUP
-
- ### user_permission_add()
- add:
- action_help: Grant access right to users and group
- api: POST /users/permissions/
- arguments:
- app:
- help: Application to manage the permission
- nargs: "+"
- -p:
- full: --permission
- help: Name of permission (main by default)
- nargs: "*"
- metavar: PERMISSION
- -u:
- full: --username
- help: Username
- nargs: "*"
- metavar: USER
+ metavar: GROUP_OR_USER
extra:
pattern: *pattern_username
- -g:
- full: --group
- help: Group name
+ -r:
+ full: --remove
+ help: Group or usernames revoke this permission from
nargs: "*"
- metavar: GROUP
+ metavar: GROUP_OR_USER
extra:
pattern: *pattern_username
- ### user_permission_remove()
- remove:
- action_help: Revoke access right to users and group
- api: PUT /users/permissions/
- arguments:
- app:
- help: Application to manage the permission
- nargs: "+"
- -p:
- full: --permission
- help: Name of permission (main by default)
- nargs: "*"
- metavar: PERMISSION
- -u:
- full: --username
- help: Username
- nargs: "*"
- metavar: USER
- extra:
- pattern: *pattern_username
- -g:
- full: --group
- help: Group name
- nargs: "*"
- metavar: GROUP
- extra:
- pattern: *pattern_username
-
- ## user_permission_clear()
- clear:
- action_help: Reset access rights for the app
+ ## user_permission_reset()
+ reset:
+ action_help: Reset allowed groups to the default (all_users) for a given permission
api: DELETE /users/permissions/
arguments:
- app:
- help: Application to manage the permission
- nargs: "+"
- -p:
- full: --permission
- help: Name of permission (main by default)
- nargs: "*"
- metavar: PERMISSION
+ permission:
+ help: Permission to manage (e.g. mail or nextcloud or wordpress.editors)
ssh:
subcategory_help: Manage ssh access
@@ -439,6 +406,10 @@ domain:
list:
action_help: List domains
api: GET /domains
+ arguments:
+ --exclude-subdomains:
+ help: Filter out domains that are obviously subdomains of other declared domains
+ action: store_true
### domain_add()
add:
@@ -481,6 +452,21 @@ domain:
- !!str ^[0-9]+$
- "pattern_positive_number"
+ ### domain_maindomain()
+ main-domain:
+ action_help: Check the current main domain, or change it
+ deprecated_alias:
+ - maindomain
+ api:
+ - GET /domains/main
+ - PUT /domains/main
+ arguments:
+ -n:
+ full: --new-main-domain
+ help: Change the current main domain
+ extra:
+ pattern: *pattern_domain
+
### certificate_status()
cert-status:
action_help: List status of current certificates (all by default).
@@ -568,78 +554,53 @@ app:
category_help: Manage apps
actions:
- ### app_fetchlist()
+ catalog:
+ action_help: Show the catalog of installable application
+ api: GET /appscatalog
+ arguments:
+ -f:
+ full: --full
+ help: Display all details, including the app manifest and various other infos
+ action: store_true
+ -c:
+ full: --with-categories
+ help: Also return a list of app categories
+ action: store_true
+
fetchlist:
- action_help: Fetch application lists from app servers, or register a new one.
- api: PUT /appslists
- arguments:
- -n:
- full: --name
- help: Name of the list to fetch (fetches all registered lists if empty)
- extra:
- pattern: &pattern_listname
- - !!str ^[a-z0-9_]+$
- - "pattern_listname"
- -u:
- full: --url
- help: URL of a new application list to register. To be specified with -n.
-
- ### app_listlists()
- listlists:
- action_help: List registered application lists
- api: GET /appslists
-
- ### app_removelist()
- removelist:
- action_help: Remove and forget about a given application list
- api: DELETE /appslists
- arguments:
- name:
- help: Name of the list to remove
- extra:
- ask: ask_list_to_remove
- pattern: *pattern_listname
+ deprecated: true
### app_list()
list:
- action_help: List apps
+ action_help: List installed apps
api: GET /apps
arguments:
-f:
- full: --filter
- help: Name filter of app_id or app_name
- -r:
- full: --raw
- help: Return the full app_dict
+ full: --full
+ help: Display all details, including the app manifest and various other infos
action: store_true
-i:
full: --installed
- help: Return only installed apps
- action: store_true
- -b:
- full: --with-backup
- help: Return only apps with backup feature (force --installed filter)
+ help: Dummy argument, does nothing anymore (still there only for backward compatibility)
action: store_true
+ filter:
+ nargs: '?'
### app_info()
info:
- action_help: Get information about an installed app
+ action_help: Show infos about a specific installed app
api: GET /apps/
arguments:
app:
help: Specific app ID
- -s:
- full: --show-status
- help: Show app installation status
- action: store_true
- -r:
- full: --raw
- help: Return the full app_dict
+ -f:
+ full: --full
+ help: Display all details, including the app manifest and various other infos
action: store_true
### app_map()
map:
- action_help: List apps by domain
+ action_help: Show the mapping between urls and apps
api: GET /appsmap
arguments:
-a:
@@ -738,30 +699,6 @@ app:
help: Delete the key
action: store_true
- ### app_checkport()
- checkport:
- action_help: Check availability of a local port
- api: GET /tools/checkport
- deprecated: true
- arguments:
- port:
- help: Port to check
- extra:
- pattern: &pattern_port
- - !!str ^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$
- - "pattern_port"
-
- ### app_checkurl()
- checkurl:
- action_help: Check availability of a web path
- api: GET /tools/checkurl
- deprecated: True
- arguments:
- url:
- help: Url to check
- -a:
- full: --app
- help: Write domain & path to app settings for further checks
### app_register_url()
register-url:
@@ -776,32 +713,6 @@ app:
help: The path to be registered (e.g. /coffee)
- ### app_initdb()
- initdb:
- action_help: Create database and initialize it with optionnal attached script
- api: POST /tools/initdb
- deprecated: true
- arguments:
- user:
- help: Name of the DB user
- -p:
- full: --password
- help: Password of the DB (generated unless set)
- -d:
- full: --db
- help: DB name (user unless set)
- -s:
- full: --sql
- help: Initial SQL file
-
- ### app_debug()
- debug:
- action_help: Display all debug informations for an application
- api: GET /apps//debug
- arguments:
- app:
- help: App name
-
### app_makedefault()
makedefault:
action_help: Redirect domain root to an app
@@ -832,6 +743,7 @@ app:
addaccess:
action_help: Grant access right to users (everyone by default)
api: PUT /access
+ deprecated: true
arguments:
apps:
nargs: "+"
@@ -843,6 +755,7 @@ app:
removeaccess:
action_help: Revoke access right to users (everyone by default)
api: DELETE /access
+ deprecated: true
arguments:
apps:
nargs: "+"
@@ -854,6 +767,7 @@ app:
clearaccess:
action_help: Reset access rights for the app
api: POST /access
+ deprecated: true
arguments:
apps:
nargs: "+"
@@ -1005,147 +919,6 @@ backup:
pattern: *pattern_backup_archive_name
-#############################
-# Monitor #
-#############################
-monitor:
- category_help: Monitor the server
- actions:
-
- ### monitor_disk()
- disk:
- action_help: Monitor disk space and usage
- api: GET /monitor/disk
- arguments:
- -f:
- full: --filesystem
- help: Show filesystem disk space
- action: append_const
- const: filesystem
- dest: units
- -t:
- full: --io
- help: Show I/O throughput
- action: append_const
- const: io
- dest: units
- -m:
- full: --mountpoint
- help: Monitor only the device mounted on MOUNTPOINT
- action: store
- -H:
- full: --human-readable
- help: Print sizes in human readable format
- action: store_true
-
- ### monitor_network()
- network:
- action_help: Monitor network interfaces
- api: GET /monitor/network
- arguments:
- -u:
- full: --usage
- help: Show interfaces bit rates
- action: append_const
- const: usage
- dest: units
- -i:
- full: --infos
- help: Show network informations
- action: append_const
- const: infos
- dest: units
- -c:
- full: --check
- help: Check network configuration
- action: append_const
- const: check
- dest: units
- -H:
- full: --human-readable
- help: Print sizes in human readable format
- action: store_true
-
- ### monitor_system()
- system:
- action_help: Monitor system informations and usage
- api: GET /monitor/system
- arguments:
- -m:
- full: --memory
- help: Show memory usage
- action: append_const
- const: memory
- dest: units
- -c:
- full: --cpu
- help: Show CPU usage and load
- action: append_const
- const: cpu
- dest: units
- -p:
- full: --process
- help: Show processes summary
- action: append_const
- const: process
- dest: units
- -u:
- full: --uptime
- help: Show the system uptime
- action: append_const
- const: uptime
- dest: units
- -i:
- full: --infos
- help: Show system informations
- action: append_const
- const: infos
- dest: units
- -H:
- full: --human-readable
- help: Print sizes in human readable format
- action: store_true
-
- ### monitor_updatestats()
- update-stats:
- action_help: Update monitoring statistics
- api: POST /monitor/stats
- arguments:
- period:
- help: Time period to update
- choices:
- - day
- - week
- - month
-
- ### monitor_showstats()
- show-stats:
- action_help: Show monitoring statistics
- api: GET /monitor/stats
- arguments:
- period:
- help: Time period to show
- choices:
- - day
- - week
- - month
-
- ### monitor_enable()
- enable:
- action_help: Enable server monitoring
- api: PUT /monitor
- arguments:
- -s:
- full: --with-stats
- help: Enable monitoring statistics
- action: store_true
-
- ### monitor_disable()
- disable:
- api: DELETE /monitor
- action_help: Disable server monitoring
-
-
#############################
# Settings #
#############################
@@ -1209,24 +982,13 @@ service:
arguments:
name:
help: Service name to add
- -s:
- full: --status
- help: Custom status command
+ -d:
+ full: --description
+ help: Description of the service
-l:
full: --log
help: Absolute path to log file to display
nargs: "+"
- -r:
- full: --runlevel
- help: Runlevel priority of the service
- type: int
- -n:
- full: --need_lock
- help: Use this option to prevent deadlocks if the service does invoke yunohost commands.
- action: store_true
- -d:
- full: --description
- help: Description of the service
-t:
full: --log_type
help: Type of the log (file or systemd)
@@ -1235,6 +997,22 @@ service:
- file
- systemd
default: file
+ --test_status:
+ help: Specify a custom bash command to check the status of the service. Note that it only makes sense to specify this if the corresponding systemd service does not return the proper information already.
+ --test_conf:
+ help: Specify a custom bash command to check if the configuration of the service is valid or broken, similar to nginx -t.
+ --needs_exposed_ports:
+ help: A list of ports that needs to be publicly exposed for the service to work as intended.
+ nargs: "+"
+ type: int
+ metavar: PORT
+ -n:
+ full: --need_lock
+ help: Use this option to prevent deadlocks if the service does invoke yunohost commands.
+ action: store_true
+ -s:
+ full: --status
+ help: Deprecated, old option. Does nothing anymore. Possibly check the --test_status option.
### service_remove()
remove:
@@ -1276,6 +1054,7 @@ service:
### service_restart()
restart:
action_help: Restart one or more services. If the services are not running yet, they will be started.
+ api: PUT /services//restart
arguments:
names:
help: Service name to restart
@@ -1579,12 +1358,9 @@ tools:
### tools_maindomain()
maindomain:
action_help: Check the current main domain, or change it
- api:
- - GET /domains/main
- - PUT /domains/main
arguments:
-n:
- full: --new-domain
+ full: --new-main-domain
help: Change the current main domain
extra:
pattern: *pattern_domain
@@ -1643,26 +1419,6 @@ tools:
help: Upgrade only the system packages
action: store_true
- ### tools_diagnosis()
- diagnosis:
- action_help: YunoHost diagnosis
- api: GET /diagnosis
- arguments:
- -p:
- full: --private
- help: Show private data (domain, IP)
- action: store_true
-
- ### tools_port_available()
- port-available:
- action_help: Check availability of a local port
- api: GET /tools/portavailable
- arguments:
- port:
- help: Port to check
- extra:
- pattern: *pattern_port
-
### tools_shell()
shell:
action_help: Launch a development shell
@@ -1717,6 +1473,11 @@ tools:
help: List pending configuration files and exit
action: store_true
+ ### tools_versions()
+ versions:
+ action_help: Display YunoHost's packages versions
+ api: GET /versions
+
subcategories:
migrations:
@@ -1872,7 +1633,7 @@ log:
api: GET /logs
arguments:
category:
- help: Log category to display (default operations), could be operation, history, package, system, access, service or app
+ help: Log category to display (default operations), could be operation, history, package, system, access, service or app
nargs: "*"
-l:
full: --limit
@@ -1898,3 +1659,73 @@ log:
--share:
help: Share the full log using yunopaste
action: store_true
+
+
+#############################
+# Diagnosis #
+#############################
+diagnosis:
+ category_help: Look for possible issues on the server
+ actions:
+
+ list:
+ action_help: List diagnosis categories
+ api: GET /diagnosis/list
+
+ show:
+ action_help: Show most recents diagnosis results
+ api: GET /diagnosis/show
+ arguments:
+ categories:
+ help: Diagnosis categories to display (all by default)
+ nargs: "*"
+ --full:
+ help: Display additional information
+ action: store_true
+ --issues:
+ help: Only display issues
+ action: store_true
+ --share:
+ help: Share the logs using yunopaste
+ action: store_true
+
+ run:
+ action_help: Run diagnosis
+ api: POST /diagnosis/run
+ arguments:
+ categories:
+ help: Diagnosis categories to run (all by default)
+ nargs: "*"
+ --force:
+ help: Ignore the cached report even if it is still 'fresh'
+ action: store_true
+ --except-if-never-ran-yet:
+ help: Don't run anything if diagnosis never ran yet ... (this is meant to be used by the webadmin)
+ action: store_true
+
+ ignore:
+ action_help: Configure some diagnosis results to be ignored and therefore not considered as actual issues
+ api: POST /diagnosis/ignore
+ arguments:
+ --add-filter:
+ help: "Add a filter. The first element should be a diagnosis category, and other criterias can be provided using the infos from the 'meta' sections in 'yunohost diagnosis show'. For example: 'dnsrecords domain=yolo.test category=xmpp'"
+ nargs: "*"
+ metavar: CRITERIA
+ --remove-filter:
+ help: Remove a filter (it should be an existing filter as listed with --list)
+ nargs: "*"
+ metavar: CRITERIA
+ --list:
+ help: List active ignore filters
+ action: store_true
+
+ get:
+ action_help: Low-level command to fetch raw data and status about a specific diagnosis test
+ api: GET /diagnosis/item/
+ arguments:
+ category:
+ help: Diagnosis category to fetch results from
+ item:
+ help: "List of criteria describing the test. Must correspond exactly to the 'meta' infos in 'yunohost diagnosis show'"
+ metavar: CRITERIA
+ nargs: "*"
diff --git a/data/actionsmap/yunohost_completion.py b/data/actionsmap/yunohost_completion.py
index a4c17c4d6..45d15f16c 100644
--- a/data/actionsmap/yunohost_completion.py
+++ b/data/actionsmap/yunohost_completion.py
@@ -3,7 +3,7 @@ Simple automated generation of a bash_completion file
for yunohost command from the actionsmap.
Generates a bash completion file assuming the structure
-`yunohost domain action`
+`yunohost category action`
adds `--help` at the end if one presses [tab] again.
author: Christophe Vuillot
@@ -15,18 +15,39 @@ THIS_SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
ACTIONSMAP_FILE = THIS_SCRIPT_DIR + '/yunohost.yml'
BASH_COMPLETION_FILE = THIS_SCRIPT_DIR + '/../bash-completion.d/yunohost'
+def get_dict_actions(OPTION_SUBTREE, category):
+ ACTIONS = [action for action in OPTION_SUBTREE[category]["actions"].keys()
+ if not action.startswith('_')]
+ ACTIONS_STR = '{}'.format(' '.join(ACTIONS))
+
+ DICT = { "actions_str": ACTIONS_STR }
+
+ return DICT
+
with open(ACTIONSMAP_FILE, 'r') as stream:
- # Getting the dictionary containning what actions are possible per domain
+ # Getting the dictionary containning what actions are possible per category
OPTION_TREE = yaml.load(stream)
- DOMAINS = [str for str in OPTION_TREE.keys() if not str.startswith('_')]
- DOMAINS_STR = '"{}"'.format(' '.join(DOMAINS))
+
+ CATEGORY = [category for category in OPTION_TREE.keys() if not category.startswith('_')]
+
+ CATEGORY_STR = '{}'.format(' '.join(CATEGORY))
ACTIONS_DICT = {}
- for domain in DOMAINS:
- ACTIONS = [str for str in OPTION_TREE[domain]['actions'].keys()
- if not str.startswith('_')]
- ACTIONS_STR = '"{}"'.format(' '.join(ACTIONS))
- ACTIONS_DICT[domain] = ACTIONS_STR
+ for category in CATEGORY:
+ ACTIONS_DICT[category] = get_dict_actions(OPTION_TREE, category)
+
+ ACTIONS_DICT[category]["subcategories"] = {}
+ ACTIONS_DICT[category]["subcategories_str"] = ""
+
+ if "subcategories" in OPTION_TREE[category].keys():
+ SUBCATEGORIES = [ subcategory for subcategory in OPTION_TREE[category]["subcategories"].keys() ]
+
+ SUBCATEGORIES_STR = '{}'.format(' '.join(SUBCATEGORIES))
+
+ ACTIONS_DICT[category]["subcategories_str"] = SUBCATEGORIES_STR
+
+ for subcategory in SUBCATEGORIES:
+ ACTIONS_DICT[category]["subcategories"][subcategory] = get_dict_actions(OPTION_TREE[category]["subcategories"], subcategory)
with open(BASH_COMPLETION_FILE, 'w') as generated_file:
@@ -47,31 +68,49 @@ with open(ACTIONSMAP_FILE, 'r') as stream:
generated_file.write('\tnarg=${#COMP_WORDS[@]}\n\n')
generated_file.write('\t# the current word being typed\n')
generated_file.write('\tcur="${COMP_WORDS[COMP_CWORD]}"\n\n')
- generated_file.write('\t# the last typed word\n')
- generated_file.write('\tprev="${COMP_WORDS[COMP_CWORD-1]}"\n\n')
- # If one is currently typing a domain then match with the domain list
- generated_file.write('\t# If one is currently typing a domain,\n')
- generated_file.write('\t# match with domains\n')
+ # If one is currently typing a category then match with the category list
+ generated_file.write('\t# If one is currently typing a category,\n')
+ generated_file.write('\t# match with categorys\n')
generated_file.write('\tif [[ $narg == 2 ]]; then\n')
- generated_file.write('\t\topts={}\n'.format(DOMAINS_STR))
+ generated_file.write('\t\topts="{}"\n'.format(CATEGORY_STR))
generated_file.write('\tfi\n\n')
# If one is currently typing an action then match with the action list
- # of the previously typed domain
- generated_file.write('\t# If one already typed a domain,\n')
- generated_file.write('\t# match the actions of that domain\n')
+ # of the previously typed category
+ generated_file.write('\t# If one already typed a category,\n')
+ generated_file.write('\t# match the actions or the subcategories of that category\n')
generated_file.write('\tif [[ $narg == 3 ]]; then\n')
- for domain in DOMAINS:
- generated_file.write('\t\tif [[ $prev == "{}" ]]; then\n'.format(domain))
- generated_file.write('\t\t\topts={}\n'.format(ACTIONS_DICT[domain]))
+ generated_file.write('\t\t# the category typed\n')
+ generated_file.write('\t\tcategory="${COMP_WORDS[1]}"\n\n')
+ for category in CATEGORY:
+ generated_file.write('\t\tif [[ $category == "{}" ]]; then\n'.format(category))
+ generated_file.write('\t\t\topts="{} {}"\n'.format(ACTIONS_DICT[category]["actions_str"], ACTIONS_DICT[category]["subcategories_str"]))
generated_file.write('\t\tfi\n')
generated_file.write('\tfi\n\n')
- # If both domain and action have been typed or the domain
+ generated_file.write('\t# If one already typed an action or a subcategory,\n')
+ generated_file.write('\t# match the actions of that subcategory\n')
+ generated_file.write('\tif [[ $narg == 4 ]]; then\n')
+ generated_file.write('\t\t# the category typed\n')
+ generated_file.write('\t\tcategory="${COMP_WORDS[1]}"\n\n')
+ generated_file.write('\t\t# the action or the subcategory typed\n')
+ generated_file.write('\t\taction_or_subcategory="${COMP_WORDS[2]}"\n\n')
+ for category in CATEGORY:
+ if len(ACTIONS_DICT[category]["subcategories"]):
+ generated_file.write('\t\tif [[ $category == "{}" ]]; then\n'.format(category))
+ for subcategory in ACTIONS_DICT[category]["subcategories"]:
+ generated_file.write('\t\t\tif [[ $action_or_subcategory == "{}" ]]; then\n'.format(subcategory))
+ generated_file.write('\t\t\t\topts="{}"\n'.format(ACTIONS_DICT[category]["subcategories"][subcategory]["actions_str"]))
+ generated_file.write('\t\t\tfi\n')
+ generated_file.write('\t\tfi\n')
+ generated_file.write('\tfi\n\n')
+
+ # If both category and action have been typed or the category
# was not recognized propose --help (only once)
generated_file.write('\t# If no options were found propose --help\n')
generated_file.write('\tif [ -z "$opts" ]; then\n')
+ generated_file.write('\t\tprev="${COMP_WORDS[COMP_CWORD-1]}"\n\n')
generated_file.write('\t\tif [[ $prev != "--help" ]]; then\n')
generated_file.write('\t\t\topts=( --help )\n')
generated_file.write('\t\tfi\n')
diff --git a/data/helpers.d/apt b/data/helpers.d/apt
index b4bf60c1f..9e3f26b90 100644
--- a/data/helpers.d/apt
+++ b/data/helpers.d/apt
@@ -5,6 +5,7 @@
# [internal]
#
# usage: ynh_wait_dpkg_free
+# | exit: Return 1 if dpkg is broken
#
# Requires YunoHost version 3.3.1 or higher.
ynh_wait_dpkg_free() {
@@ -13,7 +14,7 @@ ynh_wait_dpkg_free() {
for try in `seq 1 17`
do
# Check if /var/lib/dpkg/lock is used by another process
- if sudo lsof /var/lib/dpkg/lock > /dev/null
+ if lsof /var/lib/dpkg/lock > /dev/null
then
echo "apt is already in use..."
# Sleep an exponential time at each round
@@ -27,7 +28,7 @@ ynh_wait_dpkg_free() {
while read dpkg_file <&9
do
# Check if the name of this file contains only numbers.
- if echo "$dpkg_file" | grep -Pq "^[[:digit:]]+$"
+ if echo "$dpkg_file" | grep --perl-regexp --quiet "^[[:digit:]]+$"
then
# If so, that a remaining of dpkg.
ynh_print_err "E: dpkg was interrupted, you must manually run 'sudo dpkg --configure -a' to correct the problem."
@@ -45,20 +46,20 @@ ynh_wait_dpkg_free() {
# example: ynh_package_is_installed --package=yunohost && echo "ok"
#
# usage: ynh_package_is_installed --package=name
-# | arg: -p, --package - the package name to check
+# | arg: -p, --package= - the package name to check
#
# Requires YunoHost version 2.2.4 or higher.
ynh_package_is_installed() {
# Declare an array to define the options of this helper.
local legacy_args=p
- declare -Ar args_array=( [p]=package= )
+ local -A args_array=( [p]=package= )
local package
# Manage arguments with getopts
ynh_handle_getopts_args "$@"
ynh_wait_dpkg_free
- dpkg-query -W -f '${Status}' "$package" 2>/dev/null \
- | grep -c "ok installed" &>/dev/null
+ dpkg-query --show --showformat='${Status}' "$package" 2>/dev/null \
+ | grep --count "ok installed" &>/dev/null
}
# Get the version of an installed package
@@ -66,20 +67,21 @@ ynh_package_is_installed() {
# example: version=$(ynh_package_version --package=yunohost)
#
# usage: ynh_package_version --package=name
-# | arg: -p, --package - the package name to get version
+# | arg: -p, --package= - the package name to get version
# | ret: the version or an empty string
#
# Requires YunoHost version 2.2.4 or higher.
ynh_package_version() {
# Declare an array to define the options of this helper.
local legacy_args=p
- declare -Ar args_array=( [p]=package= )
+ local -A args_array=( [p]=package= )
local package
# Manage arguments with getopts
ynh_handle_getopts_args "$@"
- if ynh_package_is_installed "$package"; then
- dpkg-query -W -f '${Version}' "$package" 2>/dev/null
+ if ynh_package_is_installed "$package"
+ then
+ dpkg-query --show --showformat='${Version}' "$package" 2>/dev/null
else
echo ''
fi
@@ -94,7 +96,7 @@ ynh_package_version() {
# Requires YunoHost version 2.4.0.3 or higher.
ynh_apt() {
ynh_wait_dpkg_free
- DEBIAN_FRONTEND=noninteractive apt-get -y $@
+ LC_ALL=C DEBIAN_FRONTEND=noninteractive apt-get --assume-yes $@
}
# Update package index files
@@ -113,8 +115,8 @@ ynh_package_update() {
#
# Requires YunoHost version 2.2.4 or higher.
ynh_package_install() {
- ynh_apt --no-remove -o Dpkg::Options::=--force-confdef \
- -o Dpkg::Options::=--force-confold install $@
+ ynh_apt --no-remove --option Dpkg::Options::=--force-confdef \
+ --option Dpkg::Options::=--force-confold install $@
}
# Remove package(s)
@@ -163,8 +165,8 @@ ynh_package_install_from_equivs () {
local controlfile=$1
# retrieve package information
- local pkgname=$(grep '^Package: ' $controlfile | cut -d' ' -f 2) # Retrieve the name of the debian package
- local pkgversion=$(grep '^Version: ' $controlfile | cut -d' ' -f 2) # And its version number
+ local pkgname=$(grep '^Package: ' $controlfile | cut --delimiter=' ' --fields=2) # Retrieve the name of the debian package
+ local pkgversion=$(grep '^Version: ' $controlfile | cut --delimiter=' ' --fields=2) # And its version number
[[ -z "$pkgname" || -z "$pkgversion" ]] \
&& ynh_die --message="Invalid control file" # Check if this 2 variables aren't empty.
@@ -172,7 +174,7 @@ ynh_package_install_from_equivs () {
ynh_package_update
# Build and install the package
- local TMPDIR=$(mktemp -d)
+ local TMPDIR=$(mktemp --directory)
# Force the compatibility level at 10, levels below are deprecated
echo 10 > /usr/share/equivs/template/debian/compat
@@ -184,10 +186,22 @@ ynh_package_install_from_equivs () {
ynh_wait_dpkg_free
cp "$controlfile" "${TMPDIR}/control"
(cd "$TMPDIR"
- equivs-build ./control 1> /dev/null
- dpkg --force-depends -i "./${pkgname}_${pkgversion}_all.deb" 2>&1)
- ynh_package_install -f || ynh_die --message="Unable to install dependencies"
- [[ -n "$TMPDIR" ]] && rm -rf $TMPDIR # Remove the temp dir.
+ LC_ALL=C equivs-build ./control 1> /dev/null
+ dpkg --force-depends --install "./${pkgname}_${pkgversion}_all.deb" 2>&1)
+ # If install fails we use "apt-get check" to try to debug and diagnose possible unmet dependencies
+ # Note the use of { } which allows to group commands without starting a subshell (otherwise the ynh_die wouldn't exit the current shell).
+ # Be careful with the syntax : the semicolon + space at the end is important!
+
+ ynh_package_install --fix-broken || \
+ { # If the installation failed
+ # Get the list of dependencies from the deb
+ local dependencies="$(dpkg --info "$TMPDIR/${pkgname}_${pkgversion}_all.deb" | grep Depends | \
+ sed 's/^ Depends: //' | sed 's/,//g')"
+ # Fake an install of those dependencies to see the errors
+ # The sed command here is, Print only from '--fix-broken' to the end.
+ ynh_package_install $dependencies --dry-run | sed --quiet '/--fix-broken/,$p' >&2
+ ynh_die --message="Unable to install dependencies"; }
+ [[ -n "$TMPDIR" ]] && rm --recursive --force $TMPDIR # Remove the temp dir.
# check if the package is actually installed
ynh_package_is_installed "$pkgname"
@@ -205,19 +219,58 @@ ynh_package_install_from_equivs () {
# Requires YunoHost version 2.6.4 or higher.
ynh_install_app_dependencies () {
local dependencies=$@
- local dependencies=${dependencies// /, }
+ # Add a comma for each space between packages. But not add a comma if the space separate a version specification. (See below)
+ dependencies="$(echo "$dependencies" | sed 's/\([^\<=\>]\)\ \([^(]\)/\1, \2/g')"
local dependencies=${dependencies//|/ | }
local manifest_path="../manifest.json"
if [ ! -e "$manifest_path" ]; then
- manifest_path="../settings/manifest.json" # Into the restore script, the manifest is not at the same place
+ manifest_path="../settings/manifest.json" # Into the restore script, the manifest is not at the same place
fi
- local version=$(grep '\"version\": ' "$manifest_path" | cut -d '"' -f 4) # Retrieve the version number in the manifest file.
+ local version=$(grep '\"version\": ' "$manifest_path" | cut --delimiter='"' --fields=4) # Retrieve the version number in the manifest file.
if [ ${#version} -eq 0 ]; then
version="1.0"
fi
local dep_app=${app//_/-} # Replace all '_' by '-'
+ # Handle specific versions
+ if [[ "$dependencies" =~ [\<=\>] ]]
+ then
+ # Replace version specifications by relationships syntax
+ # https://www.debian.org/doc/debian-policy/ch-relationships.html
+ # Sed clarification
+ # [^(\<=\>] ignore if it begins by ( or < = >. To not apply twice.
+ # [\<=\>] matches < = or >
+ # \+ matches one or more occurence of the previous characters, for >= or >>.
+ # [^,]\+ matches all characters except ','
+ # Ex: 'package>=1.0' will be replaced by 'package (>= 1.0)'
+ dependencies="$(echo "$dependencies" | sed 's/\([^(\<=\>]\)\([\<=\>]\+\)\([^,]\+\)/\1 (\2 \3)/g')"
+ fi
+
+ #
+ # Epic ugly hack to fix the goddamn dependency nightmare of sury
+ # Sponsored by the "Djeezusse Fokin Kraiste Why Do Adminsys Has To Be So Fucking Complicated I Should Go Grow Potatoes Instead Of This Shit" collective
+ # https://github.com/YunoHost/issues/issues/1407
+ #
+ # If we require to install php dependency
+ if echo $dependencies | grep --quiet 'php'
+ then
+ # And we have packages from sury installed (7.0.33-10+weirdshiftafter instead of 7.0.33-0 on debian)
+ if dpkg --list | grep "php7.0" | grep --quiet --invert-match "7.0.33-0+deb9"
+ then
+ # And sury ain't already installed
+ if ! grep --line-number --recursive --quiet "sury" /etc/apt/sources.list*
+ then
+ # Re-add sury
+ ynh_install_extra_repo --repo="https://packages.sury.org/php/ $(ynh_get_debian_release) main" --key="https://packages.sury.org/php/apt.gpg" --name=extra_php_version
+
+ # Pin this sury repository to prevent sury of doing shit
+ ynh_pin_repo --package="*" --pin="origin \"packages.sury.org\"" --priority=200 --name=extra_php_version
+ ynh_pin_repo --package="php${$YNH_DEFAULT_PHP_VERSION}*" --pin="origin \"packages.sury.org\"" --priority=600 --name=extra_php_version --append
+ fi
+ fi
+ fi
+
cat > /tmp/${dep_app}-ynh-deps.control << EOF # Make a control file for equivs-build
Section: misc
Priority: optional
@@ -234,6 +287,38 @@ EOF
ynh_app_setting_set --app=$app --key=apt_dependencies --value="$dependencies"
}
+# Add dependencies to install with ynh_install_app_dependencies
+#
+# usage: ynh_add_app_dependencies --package=phpversion [--replace]
+# | arg: -p, --package= - Packages to add as dependencies for the app.
+# | arg: -r, --replace - Replace dependencies instead of adding to existing ones.
+#
+# Requires YunoHost version 3.8.1 or higher.
+ynh_add_app_dependencies () {
+ # Declare an array to define the options of this helper.
+ local legacy_args=pr
+ local -A args_array=( [p]=package= [r]=replace)
+ local package
+ local replace
+ # Manage arguments with getopts
+ ynh_handle_getopts_args "$@"
+ replace=${replace:-0}
+
+ local current_dependencies=""
+ if [ $replace -eq 0 ]
+ then
+ local dep_app=${app//_/-} # Replace all '_' by '-'
+ if ynh_package_is_installed --package="${dep_app}-ynh-deps"
+ then
+ current_dependencies="$(dpkg-query --show --showformat='${Depends}' ${dep_app}-ynh-deps) "
+ fi
+
+ current_dependencies=${current_dependencies// | /|}
+ fi
+
+ ynh_install_app_dependencies "${current_dependencies}${package}"
+}
+
# Remove fake package and its dependencies
#
# Dependencies will removed only if no other package need them.
@@ -245,3 +330,231 @@ ynh_remove_app_dependencies () {
local dep_app=${app//_/-} # Replace all '_' by '-'
ynh_package_autopurge ${dep_app}-ynh-deps # Remove the fake package and its dependencies if they not still used.
}
+
+#=================================================
+
+# Install packages from an extra repository properly.
+#
+# usage: ynh_install_extra_app_dependencies --repo="repo" --package="dep1 dep2" [--key=key_url] [--name=name]
+# | arg: -r, --repo= - Complete url of the extra repository.
+# | arg: -p, --package= - The packages to install from this extra repository
+# | arg: -k, --key= - url to get the public key.
+# | arg: -n, --name= - Name for the files for this repo, $app as default value.
+#
+# Requires YunoHost version 3.8.1 or higher.
+ynh_install_extra_app_dependencies () {
+ # Declare an array to define the options of this helper.
+ local legacy_args=rpkn
+ local -A args_array=( [r]=repo= [p]=package= [k]=key= [n]=name= )
+ local repo
+ local package
+ local key
+ local name
+ # Manage arguments with getopts
+ ynh_handle_getopts_args "$@"
+ name="${name:-$app}"
+ key=${key:-}
+
+ # Set a key only if asked
+ if [ -n "$key" ]
+ then
+ key="--key=$key"
+ fi
+ # Add an extra repository for those packages
+ ynh_install_extra_repo --repo="$repo" $key --priority=995 --name=$name
+
+ # Install requested dependencies from this extra repository.
+ ynh_add_app_dependencies --package="$package"
+
+ # Remove this extra repository after packages are installed
+ ynh_remove_extra_repo --name=$app
+}
+
+# Add an extra repository correctly, pin it and get the key.
+#
+# [internal]
+#
+# usage: ynh_install_extra_repo --repo="repo" [--key=key_url] [--priority=priority_value] [--name=name] [--append]
+# | arg: -r, --repo= - Complete url of the extra repository.
+# | arg: -k, --key= - url to get the public key.
+# | arg: -p, --priority= - Priority for the pin
+# | arg: -n, --name= - Name for the files for this repo, $app as default value.
+# | arg: -a, --append - Do not overwrite existing files.
+#
+# Requires YunoHost version 3.8.1 or higher.
+ynh_install_extra_repo () {
+ # Declare an array to define the options of this helper.
+ local legacy_args=rkpna
+ local -A args_array=( [r]=repo= [k]=key= [p]=priority= [n]=name= [a]=append )
+ local repo
+ local key
+ local priority
+ local name
+ local append
+ # Manage arguments with getopts
+ ynh_handle_getopts_args "$@"
+ name="${name:-$app}"
+ append=${append:-0}
+ key=${key:-}
+ priority=${priority:-}
+
+ if [ $append -eq 1 ]
+ then
+ append="--append"
+ wget_append="tee --append"
+ else
+ append=""
+ wget_append="tee"
+ fi
+
+ # Split the repository into uri, suite and components.
+ # Remove "deb " at the beginning of the repo.
+ repo="${repo#deb }"
+
+ # Get the uri
+ local uri="$(echo "$repo" | awk '{ print $1 }')"
+
+ # Get the suite
+ local suite="$(echo "$repo" | awk '{ print $2 }')"
+
+ # Get the components
+ local component="${repo##$uri $suite }"
+
+ # Add the repository into sources.list.d
+ ynh_add_repo --uri="$uri" --suite="$suite" --component="$component" --name="$name" $append
+
+ # Pin the new repo with the default priority, so it won't be used for upgrades.
+ # Build $pin from the uri without http and any sub path
+ local pin="${uri#*://}"
+ pin="${pin%%/*}"
+ # Set a priority only if asked
+ if [ -n "$priority" ]
+ then
+ priority="--priority=$priority"
+ fi
+ ynh_pin_repo --package="*" --pin="origin \"$pin\"" $priority --name="$name" $append
+
+ # Get the public key for the repo
+ if [ -n "$key" ]
+ then
+ mkdir --parents "/etc/apt/trusted.gpg.d"
+ wget --quiet "$key" --output-document=- | gpg --dearmor | $wget_append /etc/apt/trusted.gpg.d/$name.gpg > /dev/null
+ fi
+
+ # Update the list of package with the new repo
+ ynh_package_update
+}
+
+# Remove an extra repository and the assiociated configuration.
+#
+# [internal]
+#
+# usage: ynh_remove_extra_repo [--name=name]
+# | arg: -n, --name= - Name for the files for this repo, $app as default value.
+#
+# Requires YunoHost version 3.8.1 or higher.
+ynh_remove_extra_repo () {
+ # Declare an array to define the options of this helper.
+ local legacy_args=n
+ local -A args_array=( [n]=name= )
+ local name
+ # Manage arguments with getopts
+ ynh_handle_getopts_args "$@"
+ name="${name:-$app}"
+
+ ynh_secure_remove "/etc/apt/sources.list.d/$name.list"
+ ynh_secure_remove "/etc/apt/preferences.d/$name"
+ ynh_secure_remove "/etc/apt/trusted.gpg.d/$name.gpg"
+ ynh_secure_remove "/etc/apt/trusted.gpg.d/$name.asc"
+
+ # Update the list of package to exclude the old repo
+ ynh_package_update
+}
+
+# Add a repository.
+#
+# [internal]
+#
+# usage: ynh_add_repo --uri=uri --suite=suite --component=component [--name=name] [--append]
+# | arg: -u, --uri= - Uri of the repository.
+# | arg: -s, --suite= - Suite of the repository.
+# | arg: -c, --component= - Component of the repository.
+# | arg: -n, --name= - Name for the files for this repo, $app as default value.
+# | arg: -a, --append - Do not overwrite existing files.
+#
+# Example for a repo like deb http://forge.yunohost.org/debian/ stretch stable
+# uri suite component
+# ynh_add_repo --uri=http://forge.yunohost.org/debian/ --suite=stretch --component=stable
+#
+# Requires YunoHost version 3.8.1 or higher.
+ynh_add_repo () {
+ # Declare an array to define the options of this helper.
+ local legacy_args=uscna
+ local -A args_array=( [u]=uri= [s]=suite= [c]=component= [n]=name= [a]=append )
+ local uri
+ local suite
+ local component
+ local name
+ local append
+ # Manage arguments with getopts
+ ynh_handle_getopts_args "$@"
+ name="${name:-$app}"
+ append=${append:-0}
+
+ if [ $append -eq 1 ]
+ then
+ append="tee --append"
+ else
+ append="tee"
+ fi
+
+ mkdir --parents "/etc/apt/sources.list.d"
+ # Add the new repo in sources.list.d
+ echo "deb $uri $suite $component" \
+ | $append "/etc/apt/sources.list.d/$name.list"
+}
+
+# Pin a repository.
+#
+# [internal]
+#
+# usage: ynh_pin_repo --package=packages --pin=pin_filter [--priority=priority_value] [--name=name] [--append]
+# | arg: -p, --package= - Packages concerned by the pin. Or all, *.
+# | arg: -i, --pin= - Filter for the pin.
+# | arg: -p, --priority= - Priority for the pin
+# | arg: -n, --name= - Name for the files for this repo, $app as default value.
+# | arg: -a, --append - Do not overwrite existing files.
+#
+# See https://manpages.debian.org/stretch/apt/apt_preferences.5.en.html#How_APT_Interprets_Priorities for information about pinning.
+#
+# Requires YunoHost version 3.8.1 or higher.
+ynh_pin_repo () {
+ # Declare an array to define the options of this helper.
+ local legacy_args=pirna
+ local -A args_array=( [p]=package= [i]=pin= [r]=priority= [n]=name= [a]=append )
+ local package
+ local pin
+ local priority
+ local name
+ local append
+ # Manage arguments with getopts
+ ynh_handle_getopts_args "$@"
+ package="${package:-*}"
+ priority=${priority:-50}
+ name="${name:-$app}"
+ append=${append:-0}
+
+ if [ $append -eq 1 ]
+ then
+ append="tee --append"
+ else
+ append="tee"
+ fi
+
+ mkdir --parents "/etc/apt/preferences.d"
+ echo "Package: $package
+Pin: $pin
+Pin-Priority: $priority
+" \
+ | $append "/etc/apt/preferences.d/$name"
+}
diff --git a/data/helpers.d/backup b/data/helpers.d/backup
index d3ffffcd3..a62f6c104 100644
--- a/data/helpers.d/backup
+++ b/data/helpers.d/backup
@@ -4,6 +4,13 @@ CAN_BIND=${CAN_BIND:-1}
# Add a file or a directory to the list of paths to backup
#
+# usage: ynh_backup --src_path=src_path [--dest_path=dest_path] [--is_big] [--not_mandatory]
+# | arg: -s, --src_path= - file or directory to bind or symlink or copy. it shouldn't be in the backup dir.
+# | arg: -d, --dest_path= - destination file or directory inside the backup dir
+# | arg: -b, --is_big - Indicate data are big (mail, video, image ...)
+# | arg: -m, --not_mandatory - Indicate that if the file is missing, the backup can ignore it.
+# | arg: arg - Deprecated arg
+#
# This helper can be used both in a system backup hook, and in an app backup script
#
# Details: ynh_backup writes SRC and the relative DEST into a CSV file. And it
@@ -11,13 +18,6 @@ CAN_BIND=${CAN_BIND:-1}
#
# If DEST is ended by a slash it complete this path with the basename of SRC.
#
-# usage: ynh_backup --src_path=src_path [--dest_path=dest_path] [--is_big] [--not_mandatory]
-# | arg: -s, --src_path - file or directory to bind or symlink or copy. it shouldn't be in the backup dir.
-# | arg: -d, --dest_path - destination file or directory inside the backup dir
-# | arg: -b, --is_big - Indicate data are big (mail, video, image ...)
-# | arg: -m, --not_mandatory - Indicate that if the file is missing, the backup can ignore it.
-# | arg: arg - Deprecated arg
-#
# Example in the context of a wordpress app
#
# ynh_backup "/etc/nginx/conf.d/$domain.d/$app.conf"
@@ -41,21 +41,22 @@ CAN_BIND=${CAN_BIND:-1}
# # => "/etc/nginx/conf.d/$domain.d/$app.conf","apps/wordpress/conf/$app.conf"
#
# Requires YunoHost version 2.4.0 or higher.
+# Requires YunoHost version 3.5.0 or higher for the argument --not_mandatory
ynh_backup() {
# TODO find a way to avoid injection by file strange naming !
# Declare an array to define the options of this helper.
local legacy_args=sdbm
- declare -Ar args_array=( [s]=src_path= [d]=dest_path= [b]=is_big [m]=not_mandatory )
+ local -A args_array=( [s]=src_path= [d]=dest_path= [b]=is_big [m]=not_mandatory )
local src_path
local dest_path
local is_big
local not_mandatory
# Manage arguments with getopts
ynh_handle_getopts_args "$@"
- local dest_path="${dest_path:-}"
- local is_big="${is_big:-0}"
- local not_mandatory="${not_mandatory:-0}"
+ dest_path="${dest_path:-}"
+ is_big="${is_big:-0}"
+ not_mandatory="${not_mandatory:-0}"
BACKUP_CORE_ONLY=${BACKUP_CORE_ONLY:-0}
test -n "${app:-}" && do_not_backup_data=$(ynh_app_setting_get --app=$app --key=do_not_backup_data)
@@ -64,7 +65,8 @@ ynh_backup() {
# don't backup big data items
if [ $is_big -eq 1 ] && ( [ ${do_not_backup_data:-0} -eq 1 ] || [ $BACKUP_CORE_ONLY -eq 1 ] )
then
- if [ $BACKUP_CORE_ONLY -eq 1 ]; then
+ if [ $BACKUP_CORE_ONLY -eq 1 ]
+ then
ynh_print_warn --message="$src_path will not be saved, because 'BACKUP_CORE_ONLY' is set."
else
ynh_print_warn --message="$src_path will not be saved, because 'do_not_backup_data' is set."
@@ -76,22 +78,23 @@ ynh_backup() {
# Format correctly source and destination paths
# ==============================================================================
# Be sure the source path is not empty
- [[ -e "${src_path}" ]] || {
+ if [ ! -e "$src_path" ]
+ then
ynh_print_warn --message="Source path '${src_path}' does not exist"
if [ "$not_mandatory" == "0" ]
then
- # This is a temporary fix for fail2ban config files missing after the migration to stretch.
- if echo "${src_path}" | grep --quiet "/etc/fail2ban"
- then
- touch "${src_path}"
- ynh_print_info --message="The missing file will be replaced by a dummy one for the backup !!!"
- else
- return 1
- fi
+ # This is a temporary fix for fail2ban config files missing after the migration to stretch.
+ if echo "${src_path}" | grep --quiet "/etc/fail2ban"
+ then
+ touch "${src_path}"
+ ynh_print_info --message="The missing file will be replaced by a dummy one for the backup !!!"
+ else
+ return 1
+ fi
else
- return 0
+ return 0
fi
- }
+ fi
# Transform the source path as an absolute path
# If it's a dir remove the ending /
@@ -100,12 +103,13 @@ ynh_backup() {
# If there is no destination path, initialize it with the source path
# relative to "/".
# eg: src_path=/etc/yunohost -> dest_path=etc/yunohost
- if [[ -z "$dest_path" ]]; then
-
+ if [[ -z "$dest_path" ]]
+ then
dest_path="${src_path#/}"
else
- if [[ "${dest_path:0:1}" == "/" ]]; then
+ if [[ "${dest_path:0:1}" == "/" ]]
+ then
# If the destination path is an absolute path, transform it as a path
# relative to the current working directory ($YNH_CWD)
@@ -117,20 +121,23 @@ ynh_backup() {
dest_path="${dest_path#$YNH_CWD/}"
# Case where $2 is an absolute dir but doesn't begin with $YNH_CWD
- [[ "${dest_path:0:1}" == "/" ]] \
- && dest_path="${dest_path#/}"
+ if [[ "${dest_path:0:1}" == "/" ]]; then
+ dest_path="${dest_path#/}"
+ fi
fi
# Complete dest_path if ended by a /
- [[ "${dest_path: -1}" == "/" ]] \
- && dest_path="${dest_path}/$(basename $src_path)"
+ if [[ "${dest_path: -1}" == "/" ]]; then
+ dest_path="${dest_path}/$(basename $src_path)"
+ fi
fi
# Check if dest_path already exists in tmp archive
- [[ ! -e "${dest_path}" ]] || {
+ if [[ -e "${dest_path}" ]]
+ then
ynh_print_err --message="Destination path '${dest_path}' already exist"
return 1
- }
+ fi
# Add the relative current working directory to the destination path
local rel_dir="${YNH_CWD#$YNH_BACKUP_DIR}"
@@ -142,15 +149,15 @@ ynh_backup() {
# ==============================================================================
# Write file to backup into backup_list
# ==============================================================================
- local src=$(echo "${src_path}" | sed -r 's/"/\"\"/g')
- local dest=$(echo "${dest_path}" | sed -r 's/"/\"\"/g')
+ local src=$(echo "${src_path}" | sed --regexp-extended 's/"/\"\"/g')
+ local dest=$(echo "${dest_path}" | sed --regexp-extended 's/"/\"\"/g')
echo "\"${src}\",\"${dest}\"" >> "${YNH_BACKUP_CSV}"
# ==============================================================================
# Create the parent dir of the destination path
# It's for retro compatibility, some script consider ynh_backup creates this dir
- mkdir -p $(dirname "$YNH_BACKUP_DIR/${dest_path}")
+ mkdir --parents $(dirname "$YNH_BACKUP_DIR/${dest_path}")
}
# Restore all files that were previously backuped in a core backup script or app backup script
@@ -164,10 +171,11 @@ ynh_restore () {
REL_DIR="${REL_DIR%/}/"
# For each destination path begining by $REL_DIR
- cat ${YNH_BACKUP_CSV} | tr -d $'\r' | grep -ohP "^\".*\",\"$REL_DIR.*\"$" | \
- while read line; do
- local ORIGIN_PATH=$(echo "$line" | grep -ohP "^\"\K.*(?=\",\".*\"$)")
- local ARCHIVE_PATH=$(echo "$line" | grep -ohP "^\".*\",\"$REL_DIR\K.*(?=\"$)")
+ cat ${YNH_BACKUP_CSV} | tr --delete $'\r' | grep --only-matching --no-filename --perl-regexp "^\".*\",\"$REL_DIR.*\"$" | \
+ while read line
+ do
+ local ORIGIN_PATH=$(echo "$line" | grep --only-matching --no-filename --perl-regexp "^\"\K.*(?=\",\".*\"$)")
+ local ARCHIVE_PATH=$(echo "$line" | grep --only-matching --no-filename --perl-regexp "^\".*\",\"$REL_DIR\K.*(?=\"$)")
ynh_restore_file --origin_path="$ARCHIVE_PATH" --dest_path="$ORIGIN_PATH"
done
}
@@ -179,7 +187,7 @@ ynh_restore () {
# usage: _get_archive_path ORIGIN_PATH
_get_archive_path () {
# For security reasons we use csv python library to read the CSV
- sudo python -c "
+ python -c "
import sys
import csv
with open(sys.argv[1], 'r') as backup_file:
@@ -199,9 +207,9 @@ with open(sys.argv[1], 'r') as backup_file:
# the right place.
#
# usage: ynh_restore_file --origin_path=origin_path [--dest_path=dest_path] [--not_mandatory]
-# | arg: -o, --origin_path - Path where was located the file or the directory before to be backuped or relative path to $YNH_CWD where it is located in the backup archive
-# | arg: -d, --dest_path - Path where restore the file or the dir, if unspecified, the destination will be ORIGIN_PATH or if the ORIGIN_PATH doesn't exist in the archive, the destination will be searched into backup.csv
-# | arg: -m, --not_mandatory - Indicate that if the file is missing, the restore process can ignore it.
+# | arg: -o, --origin_path= - Path where was located the file or the directory before to be backuped or relative path to $YNH_CWD where it is located in the backup archive
+# | arg: -d, --dest_path= - Path where restore the file or the dir, if unspecified, the destination will be ORIGIN_PATH or if the ORIGIN_PATH doesn't exist in the archive, the destination will be searched into backup.csv
+# | arg: -m, --not_mandatory - Indicate that if the file is missing, the restore process can ignore it.
#
# examples:
# ynh_restore_file "/etc/nginx/conf.d/$domain.d/$app.conf"
@@ -217,24 +225,25 @@ with open(sys.argv[1], 'r') as backup_file:
# /etc/nginx/conf.d/$domain.d/$app.conf
#
# Requires YunoHost version 2.6.4 or higher.
+# Requires YunoHost version 3.5.0 or higher for the argument --not_mandatory
ynh_restore_file () {
# Declare an array to define the options of this helper.
local legacy_args=odm
- declare -Ar args_array=( [o]=origin_path= [d]=dest_path= [m]=not_mandatory )
+ local -A args_array=( [o]=origin_path= [d]=dest_path= [m]=not_mandatory )
local origin_path
- local archive_path
local dest_path
local not_mandatory
# Manage arguments with getopts
ynh_handle_getopts_args "$@"
- local origin_path="/${origin_path#/}"
- local archive_path="$YNH_CWD${origin_path}"
+ origin_path="/${origin_path#/}"
# Default value for dest_path = /$origin_path
- local dest_path="${dest_path:-$origin_path}"
- local not_mandatory="${not_mandatory:-0}"
+ dest_path="${dest_path:-$origin_path}"
+ not_mandatory="${not_mandatory:-0}"
+ local archive_path="$YNH_CWD${origin_path}"
# If archive_path doesn't exist, search for a corresponding path in CSV
- if [ ! -d "$archive_path" ] && [ ! -f "$archive_path" ] && [ ! -L "$archive_path" ]; then
+ if [ ! -d "$archive_path" ] && [ ! -f "$archive_path" ] && [ ! -L "$archive_path" ]
+ then
if [ "$not_mandatory" == "0" ]
then
archive_path="$YNH_BACKUP_DIR/$(_get_archive_path \"$origin_path\")"
@@ -247,10 +256,10 @@ ynh_restore_file () {
if [[ -e "${dest_path}" ]]
then
# Check if the file/dir size is less than 500 Mo
- if [[ $(du -sb ${dest_path} | cut -d"/" -f1) -le "500000000" ]]
+ if [[ $(du --summarize --bytes ${dest_path} | cut --delimiter="/" --fields=1) -le "500000000" ]]
then
local backup_file="/home/yunohost.conf/backup/${dest_path}.backup.$(date '+%Y%m%d.%H%M%S')"
- mkdir -p "$(dirname "$backup_file")"
+ mkdir --parents "$(dirname "$backup_file")"
mv "${dest_path}" "$backup_file" # Move the current file or directory
else
ynh_secure_remove --file=${dest_path}
@@ -258,15 +267,17 @@ ynh_restore_file () {
fi
# Restore origin_path into dest_path
- mkdir -p $(dirname "$dest_path")
+ mkdir --parents $(dirname "$dest_path")
# Do a copy if it's just a mounting point
- if mountpoint -q $YNH_BACKUP_DIR; then
- if [[ -d "${archive_path}" ]]; then
+ if mountpoint --quiet $YNH_BACKUP_DIR
+ then
+ if [[ -d "${archive_path}" ]]
+ then
archive_path="${archive_path}/."
- mkdir -p "$dest_path"
+ mkdir --parents "$dest_path"
fi
- cp -a "$archive_path" "${dest_path}"
+ cp --archive "$archive_path" "${dest_path}"
# Do a move if YNH_BACKUP_DIR is already a copy
else
mv "$archive_path" "${dest_path}"
@@ -287,22 +298,22 @@ ynh_bind_or_cp() {
# Calculate and store a file checksum into the app settings
#
-# $app should be defined when calling this helper
-#
# usage: ynh_store_file_checksum --file=file
-# | arg: -f, --file - The file on which the checksum will performed, then stored.
+# | arg: -f, --file= - The file on which the checksum will performed, then stored.
+#
+# $app should be defined when calling this helper
#
# Requires YunoHost version 2.6.4 or higher.
ynh_store_file_checksum () {
# Declare an array to define the options of this helper.
local legacy_args=f
- declare -Ar args_array=( [f]=file= )
+ local -A args_array=( [f]=file= )
local file
# Manage arguments with getopts
ynh_handle_getopts_args "$@"
local checksum_setting_name=checksum_${file//[\/ ]/_} # Replace all '/' and ' ' by '_'
- ynh_app_setting_set --app=$app --key=$checksum_setting_name --value=$(sudo md5sum "$file" | cut -d' ' -f1)
+ ynh_app_setting_set --app=$app --key=$checksum_setting_name --value=$(md5sum "$file" | cut --delimiter=' ' --fields=1)
# If backup_file_checksum isn't empty, ynh_backup_if_checksum_is_different has made a backup
if [ -n "${backup_file_checksum-}" ]
@@ -321,14 +332,14 @@ ynh_store_file_checksum () {
# modified config files.
#
# usage: ynh_backup_if_checksum_is_different --file=file
-# | arg: -f, --file - The file on which the checksum test will be perfomed.
+# | arg: -f, --file= - The file on which the checksum test will be perfomed.
# | ret: the name of a backup file, or nothing
#
# Requires YunoHost version 2.6.4 or higher.
ynh_backup_if_checksum_is_different () {
# Declare an array to define the options of this helper.
local legacy_args=f
- declare -Ar args_array=( [f]=file= )
+ local -A args_array=( [f]=file= )
local file
# Manage arguments with getopts
ynh_handle_getopts_args "$@"
@@ -339,11 +350,11 @@ ynh_backup_if_checksum_is_different () {
backup_file_checksum=""
if [ -n "$checksum_value" ]
then # Proceed only if a value was stored into the app settings
- if [ -e $file ] && ! echo "$checksum_value $file" | sudo md5sum -c --status
+ if [ -e $file ] && ! echo "$checksum_value $file" | md5sum --check --status
then # If the checksum is now different
backup_file_checksum="/home/yunohost.conf/backup/$file.backup.$(date '+%Y%m%d.%H%M%S')"
- sudo mkdir -p "$(dirname "$backup_file_checksum")"
- sudo cp -a "$file" "$backup_file_checksum" # Backup the current file
+ mkdir --parents "$(dirname "$backup_file_checksum")"
+ cp --archive "$file" "$backup_file_checksum" # Backup the current file
ynh_print_warn "File $file has been manually modified since the installation or last upgrade. So it has been duplicated in $backup_file_checksum"
echo "$backup_file_checksum" # Return the name of the backup file
fi
@@ -352,16 +363,16 @@ ynh_backup_if_checksum_is_different () {
# Delete a file checksum from the app settings
#
-# $app should be defined when calling this helper
+# usage: ynh_delete_file_checksum --file=file
+# | arg: -f, --file= - The file for which the checksum will be deleted
#
-# usage: ynh_remove_file_checksum file
-# | arg: -f, --file= - The file for which the checksum will be deleted
+# $app should be defined when calling this helper
#
# Requires YunoHost version 3.3.1 or higher.
ynh_delete_file_checksum () {
# Declare an array to define the options of this helper.
local legacy_args=f
- declare -Ar args_array=( [f]=file= )
+ local -A args_array=( [f]=file= )
local file
# Manage arguments with getopts
ynh_handle_getopts_args "$@"
@@ -373,11 +384,11 @@ ynh_delete_file_checksum () {
# Make a backup in case of failed upgrade
#
# usage:
-# ynh_backup_before_upgrade
-# ynh_clean_setup () {
-# ynh_restore_upgradebackup
-# }
-# ynh_abort_if_errors
+# ynh_backup_before_upgrade
+# ynh_clean_setup () {
+# ynh_restore_upgradebackup
+# }
+# ynh_abort_if_errors
#
# Requires YunoHost version 2.7.2 or higher.
ynh_backup_before_upgrade () {
@@ -394,7 +405,7 @@ ynh_backup_before_upgrade () {
if [ "$NO_BACKUP_UPGRADE" -eq 0 ]
then
# Check if a backup already exists with the prefix 1
- if sudo yunohost backup list | grep -q $app_bck-pre-upgrade1
+ if yunohost backup list | grep --quiet $app_bck-pre-upgrade1
then
# Prefix becomes 2 to preserve the previous backup
backup_number=2
@@ -402,14 +413,14 @@ ynh_backup_before_upgrade () {
fi
# Create backup
- sudo BACKUP_CORE_ONLY=1 yunohost backup create --apps $app --name $app_bck-pre-upgrade$backup_number --debug
+ BACKUP_CORE_ONLY=1 yunohost backup create --apps $app --name $app_bck-pre-upgrade$backup_number --debug
if [ "$?" -eq 0 ]
then
# If the backup succeeded, remove the previous backup
- if sudo yunohost backup list | grep -q $app_bck-pre-upgrade$old_backup_number
+ if yunohost backup list | grep --quiet $app_bck-pre-upgrade$old_backup_number
then
# Remove the previous backup only if it exists
- sudo yunohost backup delete $app_bck-pre-upgrade$old_backup_number > /dev/null
+ yunohost backup delete $app_bck-pre-upgrade$old_backup_number > /dev/null
fi
else
ynh_die --message="Backup failed, the upgrade process was aborted."
@@ -422,11 +433,11 @@ ynh_backup_before_upgrade () {
# Restore a previous backup if the upgrade process failed
#
# usage:
-# ynh_backup_before_upgrade
-# ynh_clean_setup () {
-# ynh_restore_upgradebackup
-# }
-# ynh_abort_if_errors
+# ynh_backup_before_upgrade
+# ynh_clean_setup () {
+# ynh_restore_upgradebackup
+# }
+# ynh_abort_if_errors
#
# Requires YunoHost version 2.7.2 or higher.
ynh_restore_upgradebackup () {
@@ -438,12 +449,12 @@ ynh_restore_upgradebackup () {
if [ "$NO_BACKUP_UPGRADE" -eq 0 ]
then
# Check if an existing backup can be found before removing and restoring the application.
- if sudo yunohost backup list | grep -q $app_bck-pre-upgrade$backup_number
+ if yunohost backup list | grep --quiet $app_bck-pre-upgrade$backup_number
then
# Remove the application then restore it
- sudo yunohost app remove $app
+ yunohost app remove $app
# Restore the backup
- sudo yunohost backup restore $app_bck-pre-upgrade$backup_number --apps $app --force --debug
+ yunohost backup restore $app_bck-pre-upgrade$backup_number --apps $app --force --debug
ynh_die --message="The app was restored to the way it was before the failed upgrade."
fi
else
diff --git a/data/helpers.d/fail2ban b/data/helpers.d/fail2ban
index 58af9ec0b..d8777a16d 100644
--- a/data/helpers.d/fail2ban
+++ b/data/helpers.d/fail2ban
@@ -63,52 +63,54 @@
#
# Requires YunoHost version 3.5.0 or higher.
ynh_add_fail2ban_config () {
- # Declare an array to define the options of this helper.
- local legacy_args=lrmptv
- declare -Ar args_array=( [l]=logpath= [r]=failregex= [m]=max_retry= [p]=ports= [t]=use_template [v]=others_var=)
- local logpath
- local failregex
- local max_retry
- local ports
- local others_var
- local use_template
- # Manage arguments with getopts
- ynh_handle_getopts_args "$@"
- use_template="${use_template:-0}"
- max_retry=${max_retry:-3}
- ports=${ports:-http,https}
+ # Declare an array to define the options of this helper.
+ local legacy_args=lrmptv
+ local -A args_array=( [l]=logpath= [r]=failregex= [m]=max_retry= [p]=ports= [t]=use_template [v]=others_var=)
+ local logpath
+ local failregex
+ local max_retry
+ local ports
+ local others_var
+ local use_template
+ # Manage arguments with getopts
+ ynh_handle_getopts_args "$@"
+ max_retry=${max_retry:-3}
+ ports=${ports:-http,https}
+ others_var=${others_var:-}
+ use_template="${use_template:-0}"
- finalfail2banjailconf="/etc/fail2ban/jail.d/$app.conf"
- finalfail2banfilterconf="/etc/fail2ban/filter.d/$app.conf"
- ynh_backup_if_checksum_is_different "$finalfail2banjailconf"
- ynh_backup_if_checksum_is_different "$finalfail2banfilterconf"
+ finalfail2banjailconf="/etc/fail2ban/jail.d/$app.conf"
+ finalfail2banfilterconf="/etc/fail2ban/filter.d/$app.conf"
+ ynh_backup_if_checksum_is_different "$finalfail2banjailconf"
+ ynh_backup_if_checksum_is_different "$finalfail2banfilterconf"
- if [ $use_template -eq 1 ]
- then
- # Usage 2, templates
- cp ../conf/f2b_jail.conf $finalfail2banjailconf
- cp ../conf/f2b_filter.conf $finalfail2banfilterconf
-
- if [ -n "${app:-}" ]
+ if [ $use_template -eq 1 ]
then
- ynh_replace_string "__APP__" "$app" "$finalfail2banjailconf"
- ynh_replace_string "__APP__" "$app" "$finalfail2banfilterconf"
- fi
+ # Usage 2, templates
+ cp ../conf/f2b_jail.conf $finalfail2banjailconf
+ cp ../conf/f2b_filter.conf $finalfail2banfilterconf
- # Replace all other variable given as arguments
- for var_to_replace in ${others_var:-}; do
- # ${var_to_replace^^} make the content of the variable on upper-cases
- # ${!var_to_replace} get the content of the variable named $var_to_replace
- ynh_replace_string --match_string="__${var_to_replace^^}__" --replace_string="${!var_to_replace}" --target_file="$finalfail2banjailconf"
- ynh_replace_string --match_string="__${var_to_replace^^}__" --replace_string="${!var_to_replace}" --target_file="$finalfail2banfilterconf"
- done
+ if [ -n "${app:-}" ]
+ then
+ ynh_replace_string "__APP__" "$app" "$finalfail2banjailconf"
+ ynh_replace_string "__APP__" "$app" "$finalfail2banfilterconf"
+ fi
- else
- # Usage 1, no template. Build a config file from scratch.
- test -n "$logpath" || ynh_die "ynh_add_fail2ban_config expects a logfile path as first argument and received nothing."
- test -n "$failregex" || ynh_die "ynh_add_fail2ban_config expects a failure regex as second argument and received nothing."
+ # Replace all other variable given as arguments
+ for var_to_replace in $others_var
+ do
+ # ${var_to_replace^^} make the content of the variable on upper-cases
+ # ${!var_to_replace} get the content of the variable named $var_to_replace
+ ynh_replace_string --match_string="__${var_to_replace^^}__" --replace_string="${!var_to_replace}" --target_file="$finalfail2banjailconf"
+ ynh_replace_string --match_string="__${var_to_replace^^}__" --replace_string="${!var_to_replace}" --target_file="$finalfail2banfilterconf"
+ done
- tee $finalfail2banjailconf < /dev/null; set +x
- local i
- for i in `seq 0 $(( ${#arguments[@]} -1 ))`
- do
- # Try to use legacy_args as a list of option_flag of the array args_array
- # Otherwise, fallback to getopts_parameters to get the option_flag. But an associative arrays isn't always sorted in the correct order...
- # Remove all ':' in getopts_parameters
- getopts_parameters=${legacy_args:-${getopts_parameters//:}}
- # Get the option_flag from getopts_parameters, by using the option_flag according to the position of the argument.
- option_flag=${getopts_parameters:$i:1}
- if [ -z "$option_flag" ]; then
- ynh_print_warn --message="Too many arguments ! \"${arguments[$i]}\" will be ignored."
- continue
- fi
- # Use the long option, corresponding to the option_flag, as a variable
- # (e.g. for [u]=user, 'user' will be used as a variable)
- # Also, remove '=' at the end of the long option
- # The variable name will be stored in 'option_var'
- local option_var="${args_array[$option_flag]%=}"
+ # LEGACY MODE
+ # Check if there's getopts arguments
+ if [ "${arguments[0]:0:1}" != "-" ]
+ then
+ # If not, enter in legacy mode and manage the arguments as positionnal ones..
+ # Dot not echo, to prevent to go through a helper output. But print only in the log.
+ set -x; echo "! Helper used in legacy mode !" > /dev/null; set +x
+ local i
+ for i in `seq 0 $(( ${#arguments[@]} -1 ))`
+ do
+ # Try to use legacy_args as a list of option_flag of the array args_array
+ # Otherwise, fallback to getopts_parameters to get the option_flag. But an associative arrays isn't always sorted in the correct order...
+ # Remove all ':' in getopts_parameters
+ getopts_parameters=${legacy_args:-${getopts_parameters//:}}
+ # Get the option_flag from getopts_parameters, by using the option_flag according to the position of the argument.
+ option_flag=${getopts_parameters:$i:1}
+ if [ -z "$option_flag" ]
+ then
+ ynh_print_warn --message="Too many arguments ! \"${arguments[$i]}\" will be ignored."
+ continue
+ fi
+ # Use the long option, corresponding to the option_flag, as a variable
+ # (e.g. for [u]=user, 'user' will be used as a variable)
+ # Also, remove '=' at the end of the long option
+ # The variable name will be stored in 'option_var'
+ local option_var="${args_array[$option_flag]%=}"
- # Store each value given as argument in the corresponding variable
- # The values will be stored in the same order than $args_array
- eval ${option_var}+='"${arguments[$i]}"'
- done
- unset legacy_args
- else
- # END LEGACY MODE
- # Call parse_arg and pass the modified list of args as an array of arguments.
- parse_arg "${arguments[@]}"
- fi
- fi
- set -x
+ # Store each value given as argument in the corresponding variable
+ # The values will be stored in the same order than $args_array
+ eval ${option_var}+='"${arguments[$i]}"'
+ done
+ unset legacy_args
+ else
+ # END LEGACY MODE
+ # Call parse_arg and pass the modified list of args as an array of arguments.
+ parse_arg "${arguments[@]}"
+ fi
+ fi
+ set -o xtrace # set -x
}
diff --git a/data/helpers.d/hardware b/data/helpers.d/hardware
new file mode 100644
index 000000000..6702a8548
--- /dev/null
+++ b/data/helpers.d/hardware
@@ -0,0 +1,108 @@
+#!/bin/bash
+
+# Get the total or free amount of RAM+swap on the system
+#
+# usage: ynh_get_ram [--free|--total] [--ignore_swap|--only_swap]
+# | arg: -f, --free - Count free RAM+swap
+# | arg: -t, --total - Count total RAM+swap
+# | arg: -s, --ignore_swap - Ignore swap, consider only real RAM
+# | arg: -o, --only_swap - Ignore real RAM, consider only swap
+# | ret: the amount of free ram
+#
+# Requires YunoHost version 3.8.1 or higher.
+ynh_get_ram () {
+ # Declare an array to define the options of this helper.
+ local legacy_args=ftso
+ local -A args_array=( [f]=free [t]=total [s]=ignore_swap [o]=only_swap )
+ local free
+ local total
+ local ignore_swap
+ local only_swap
+ # Manage arguments with getopts
+ ynh_handle_getopts_args "$@"
+ ignore_swap=${ignore_swap:-0}
+ only_swap=${only_swap:-0}
+ free=${free:-0}
+ total=${total:-0}
+
+ local total_ram=$(vmstat --stats --unit M | grep "total memory" | awk '{print $1}')
+ local total_swap=$(vmstat --stats --unit M | grep "total swap" | awk '{print $1}')
+ local total_ram_swap=$(( total_ram + total_swap ))
+
+ local free_ram=$(vmstat --stats --unit M | grep "free memory" | awk '{print $1}')
+ local free_swap=$(vmstat --stats --unit M | grep "free swap" | awk '{print $1}')
+ local free_ram_swap=$(( free_ram + free_swap ))
+
+ # Use the total amount of ram
+ if [ $free -eq 1 ]
+ then
+ # Use the total amount of free ram
+ local ram=$free_ram_swap
+ if [ $ignore_swap -eq 1 ]
+ then
+ # Use only the amount of free ram
+ ram=$free_ram
+ elif [ $only_swap -eq 1 ]
+ then
+ # Use only the amount of free swap
+ ram=$free_swap
+ fi
+ elif [ $total -eq 1 ]
+ then
+ local ram=$total_ram_swap
+ if [ $ignore_swap -eq 1 ]
+ then
+ # Use only the amount of free ram
+ ram=$total_ram
+ elif [ $only_swap -eq 1 ]
+ then
+ # Use only the amount of free swap
+ ram=$total_swap
+ fi
+ else
+ ynh_print_warn --message="You have to choose --free or --total when using ynh_get_ram"
+ ram=0
+ fi
+
+ echo $ram
+}
+
+# Return 0 or 1 depending if the system has a given amount of RAM+swap free or total
+#
+# usage: ynh_require_ram --required=RAM required in Mb [--free|--total] [--ignore_swap|--only_swap]
+# | arg: -r, --required= - The amount to require, in Mb
+# | arg: -f, --free - Count free RAM+swap
+# | arg: -t, --total - Count total RAM+swap
+# | arg: -s, --ignore_swap - Ignore swap, consider only real RAM
+# | arg: -o, --only_swap - Ignore real RAM, consider only swap
+# | exit: Return 1 if the ram is under the requirement, 0 otherwise.
+#
+# Requires YunoHost version 3.8.1 or higher.
+ynh_require_ram () {
+ # Declare an array to define the options of this helper.
+ local legacy_args=rftso
+ local -A args_array=( [r]=required= [f]=free [t]=total [s]=ignore_swap [o]=only_swap )
+ local required
+ local free
+ local total
+ local ignore_swap
+ local only_swap
+ # Manage arguments with getopts
+ ynh_handle_getopts_args "$@"
+ # Dunno if that's the right way to do, but that's some black magic to be able to
+ # forward the bool args to ynh_get_ram easily?
+ # If the variable $free is not empty, set it to '--free'
+ free=${free:+--free}
+ total=${total:+--total}
+ ignore_swap=${ignore_swap:+--ignore_swap}
+ only_swap=${only_swap:+--only_swap}
+
+ local ram=$(ynh_get_ram $free $total $ignore_swap $only_swap)
+
+ if [ $ram -lt $required ]
+ then
+ return 1
+ else
+ return 0
+ fi
+}
diff --git a/data/helpers.d/logging b/data/helpers.d/logging
index be33b75a5..c79090e25 100644
--- a/data/helpers.d/logging
+++ b/data/helpers.d/logging
@@ -3,30 +3,34 @@
# Print a message to stderr and exit
#
# usage: ynh_die --message=MSG [--ret_code=RETCODE]
+# | arg: -m, --message= - Message to display
+# | arg: -c, --ret_code= - Exit code to exit with
#
# Requires YunoHost version 2.4.0 or higher.
ynh_die() {
- # Declare an array to define the options of this helper.
- local legacy_args=mc
- declare -Ar args_array=( [m]=message= [c]=ret_code= )
- local message
- local ret_code
- # Manage arguments with getopts
- ynh_handle_getopts_args "$@"
+ # Declare an array to define the options of this helper.
+ local legacy_args=mc
+ local -A args_array=( [m]=message= [c]=ret_code= )
+ local message
+ local ret_code
+ # Manage arguments with getopts
+ ynh_handle_getopts_args "$@"
+ ret_code=${ret_code:-1}
- echo "$message" 1>&2
- exit "${ret_code:-1}"
+ echo "$message" 1>&2
+ exit "$ret_code"
}
# Display a message in the 'INFO' logging category
#
# usage: ynh_print_info --message="Some message"
+# | arg: -m, --message= - Message to display
#
# Requires YunoHost version 3.2.0 or higher.
ynh_print_info() {
# Declare an array to define the options of this helper.
local legacy_args=m
- declare -Ar args_array=( [m]=message= )
+ local -A args_array=( [m]=message= )
local message
# Manage arguments with getopts
ynh_handle_getopts_args "$@"
@@ -45,12 +49,12 @@ ynh_print_info() {
#
# Requires YunoHost version 2.6.4 or higher.
ynh_no_log() {
- local ynh_cli_log=/var/log/yunohost/yunohost-cli.log
- sudo cp -a ${ynh_cli_log} ${ynh_cli_log}-move
- eval $@
- local exit_code=$?
- sudo mv ${ynh_cli_log}-move ${ynh_cli_log}
- return $?
+ local ynh_cli_log=/var/log/yunohost/yunohost-cli.log
+ cp --archive ${ynh_cli_log} ${ynh_cli_log}-move
+ eval $@
+ local exit_code=$?
+ mv ${ynh_cli_log}-move ${ynh_cli_log}
+ return $exit_code
}
# Main printer, just in case in the future we have to change anything about that.
@@ -59,121 +63,116 @@ ynh_no_log() {
#
# Requires YunoHost version 3.2.0 or higher.
ynh_print_log () {
- echo -e "${1}"
+ echo -e "${1}"
}
# Print a warning on stderr
#
# usage: ynh_print_warn --message="Text to print"
-# | arg: -m, --message - The text to print
+# | arg: -m, --message= - The text to print
#
# Requires YunoHost version 3.2.0 or higher.
ynh_print_warn () {
- # Declare an array to define the options of this helper.
- local legacy_args=m
- declare -Ar args_array=( [m]=message= )
- local message
- # Manage arguments with getopts
- ynh_handle_getopts_args "$@"
+ # Declare an array to define the options of this helper.
+ local legacy_args=m
+ local -A args_array=( [m]=message= )
+ local message
+ # Manage arguments with getopts
+ ynh_handle_getopts_args "$@"
- ynh_print_log "\e[93m\e[1m[WARN]\e[0m ${message}" >&2
+ ynh_print_log "\e[93m\e[1m[WARN]\e[0m ${message}" >&2
}
# Print an error on stderr
#
# usage: ynh_print_err --message="Text to print"
-# | arg: -m, --message - The text to print
+# | arg: -m, --message= - The text to print
#
# Requires YunoHost version 3.2.0 or higher.
ynh_print_err () {
- # Declare an array to define the options of this helper.
- local legacy_args=m
- declare -Ar args_array=( [m]=message= )
- local message
- # Manage arguments with getopts
- ynh_handle_getopts_args "$@"
+ # Declare an array to define the options of this helper.
+ local legacy_args=m
+ local -A args_array=( [m]=message= )
+ local message
+ # Manage arguments with getopts
+ ynh_handle_getopts_args "$@"
- ynh_print_log "\e[91m\e[1m[ERR]\e[0m ${message}" >&2
+ ynh_print_log "\e[91m\e[1m[ERR]\e[0m ${message}" >&2
}
# Execute a command and print the result as an error
#
# usage: ynh_exec_err your_command
# usage: ynh_exec_err "your_command | other_command"
+# | arg: command - command to execute
#
# When using pipes, double quotes are required - otherwise, this helper will run the first command, and the whole output will be sent through the next pipe.
#
# If the command to execute uses double quotes, they have to be escaped or they will be interpreted and removed.
#
-# | arg: command - command to execute
-#
# Requires YunoHost version 3.2.0 or higher.
ynh_exec_err () {
- ynh_print_err "$(eval $@)"
+ ynh_print_err "$(eval $@)"
}
# Execute a command and print the result as a warning
#
# usage: ynh_exec_warn your_command
# usage: ynh_exec_warn "your_command | other_command"
+# | arg: command - command to execute
#
# When using pipes, double quotes are required - otherwise, this helper will run the first command, and the whole output will be sent through the next pipe.
#
# If the command to execute uses double quotes, they have to be escaped or they will be interpreted and removed.
#
-# | arg: command - command to execute
-#
# Requires YunoHost version 3.2.0 or higher.
ynh_exec_warn () {
- ynh_print_warn "$(eval $@)"
+ ynh_print_warn "$(eval $@)"
}
# Execute a command and force the result to be printed on stdout
#
# usage: ynh_exec_warn_less your_command
# usage: ynh_exec_warn_less "your_command | other_command"
+# | arg: command - command to execute
#
# When using pipes, double quotes are required - otherwise, this helper will run the first command, and the whole output will be sent through the next pipe.
#
# If the command to execute uses double quotes, they have to be escaped or they will be interpreted and removed.
#
-# | arg: command - command to execute
-#
# Requires YunoHost version 3.2.0 or higher.
ynh_exec_warn_less () {
- eval $@ 2>&1
+ eval $@ 2>&1
}
# Execute a command and redirect stdout in /dev/null
#
# usage: ynh_exec_quiet your_command
# usage: ynh_exec_quiet "your_command | other_command"
+# | arg: command - command to execute
#
# When using pipes, double quotes are required - otherwise, this helper will run the first command, and the whole output will be sent through the next pipe.
#
# If the command to execute uses double quotes, they have to be escaped or they will be interpreted and removed.
#
-# | arg: command - command to execute
-#
# Requires YunoHost version 3.2.0 or higher.
ynh_exec_quiet () {
- eval $@ > /dev/null
+ eval $@ > /dev/null
}
# Execute a command and redirect stdout and stderr in /dev/null
#
# usage: ynh_exec_fully_quiet your_command
# usage: ynh_exec_fully_quiet "your_command | other_command"
+# | arg: command - command to execute
#
# When using pipes, double quotes are required - otherwise, this helper will run the first command, and the whole output will be sent through the next pipe.
#
# If the command to execute uses double quotes, they have to be escaped or they will be interpreted and removed.
#
-# | arg: command - command to execute
-#
# Requires YunoHost version 3.2.0 or higher.
ynh_exec_fully_quiet () {
- eval $@ > /dev/null 2>&1
+ eval $@ > /dev/null 2>&1
}
# Remove any logs for all the following commands.
@@ -184,7 +183,7 @@ ynh_exec_fully_quiet () {
#
# Requires YunoHost version 3.2.0 or higher.
ynh_print_OFF () {
- exec {BASH_XTRACEFD}>/dev/null
+ exec {BASH_XTRACEFD}>/dev/null
}
# Restore the logging after ynh_print_OFF
@@ -193,9 +192,9 @@ ynh_print_OFF () {
#
# Requires YunoHost version 3.2.0 or higher.
ynh_print_ON () {
- exec {BASH_XTRACEFD}>&1
- # Print an echo only for the log, to be able to know that ynh_print_ON has been called.
- echo ynh_print_ON > /dev/null
+ exec {BASH_XTRACEFD}>&1
+ # Print an echo only for the log, to be able to know that ynh_print_ON has been called.
+ echo ynh_print_ON > /dev/null
}
# Initial definitions for ynh_script_progression
@@ -216,86 +215,87 @@ base_time=$(date +%s)
# usage: ynh_script_progression --message=message [--weight=weight] [--time]
# | arg: -m, --message= - The text to print
# | arg: -w, --weight= - The weight for this progression. This value is 1 by default. Use a bigger value for a longer part of the script.
-# | arg: -t, --time= - Print the execution time since the last call to this helper. Especially usefull to define weights. The execution time is given for the duration since the previous call. So the weight should be applied to this previous call.
-# | arg: -l, --last= - Use for the last call of the helper, to fill te progression bar.
+# | arg: -t, --time - Print the execution time since the last call to this helper. Especially usefull to define weights. The execution time is given for the duration since the previous call. So the weight should be applied to this previous call.
+# | arg: -l, --last - Use for the last call of the helper, to fill te progression bar.
#
# Requires YunoHost version 3.5.0 or higher.
ynh_script_progression () {
- set +x
- # Declare an array to define the options of this helper.
- local legacy_args=mwtl
- declare -Ar args_array=( [m]=message= [w]=weight= [t]=time [l]=last )
- local message
- local weight
- local time
- local last
- # Manage arguments with getopts
- ynh_handle_getopts_args "$@"
- set +x
- weight=${weight:-1}
- time=${time:-0}
- last=${last:-0}
+ set +o xtrace # set +x
+ # Declare an array to define the options of this helper.
+ local legacy_args=mwtl
+ local -A args_array=( [m]=message= [w]=weight= [t]=time [l]=last )
+ local message
+ local weight
+ local time
+ local last
+ # Manage arguments with getopts
+ ynh_handle_getopts_args "$@"
+ # Re-disable xtrace, ynh_handle_getopts_args set it back
+ set +o xtrace # set +x
+ weight=${weight:-1}
+ time=${time:-0}
+ last=${last:-0}
- # Get execution time since the last $base_time
- local exec_time=$(( $(date +%s) - $base_time ))
- base_time=$(date +%s)
+ # Get execution time since the last $base_time
+ local exec_time=$(( $(date +%s) - $base_time ))
+ base_time=$(date +%s)
- # Compute $max_progression (if we didn't already)
- if [ "$max_progression" = -1 ]
- then
- # Get the number of occurrences of 'ynh_script_progression' in the script. Except those are commented.
- local helper_calls="$(grep --count "^[^#]*ynh_script_progression" $0)"
- # Get the number of call with a weight value
- local weight_calls=$(grep --perl-regexp --count "^[^#]*ynh_script_progression.*(--weight|-w )" $0)
+ # Compute $max_progression (if we didn't already)
+ if [ "$max_progression" = -1 ]
+ then
+ # Get the number of occurrences of 'ynh_script_progression' in the script. Except those are commented.
+ local helper_calls="$(grep --count "^[^#]*ynh_script_progression" $0)"
+ # Get the number of call with a weight value
+ local weight_calls=$(grep --perl-regexp --count "^[^#]*ynh_script_progression.*(--weight|-w )" $0)
- # Get the weight of each occurrences of 'ynh_script_progression' in the script using --weight
- local weight_valuesA="$(grep --perl-regexp "^[^#]*ynh_script_progression.*--weight" $0 | sed 's/.*--weight[= ]\([[:digit:]]*\).*/\1/g')"
- # Get the weight of each occurrences of 'ynh_script_progression' in the script using -w
- local weight_valuesB="$(grep --perl-regexp "^[^#]*ynh_script_progression.*-w " $0 | sed 's/.*-w[= ]\([[:digit:]]*\).*/\1/g')"
- # Each value will be on a different line.
- # Remove each 'end of line' and replace it by a '+' to sum the values.
- local weight_values=$(( $(echo "$weight_valuesA" | tr '\n' '+') + $(echo "$weight_valuesB" | tr '\n' '+') 0 ))
+ # Get the weight of each occurrences of 'ynh_script_progression' in the script using --weight
+ local weight_valuesA="$(grep --perl-regexp "^[^#]*ynh_script_progression.*--weight" $0 | sed 's/.*--weight[= ]\([[:digit:]]*\).*/\1/g')"
+ # Get the weight of each occurrences of 'ynh_script_progression' in the script using -w
+ local weight_valuesB="$(grep --perl-regexp "^[^#]*ynh_script_progression.*-w " $0 | sed 's/.*-w[= ]\([[:digit:]]*\).*/\1/g')"
+ # Each value will be on a different line.
+ # Remove each 'end of line' and replace it by a '+' to sum the values.
+ local weight_values=$(( $(echo "$weight_valuesA" | tr '\n' '+') + $(echo "$weight_valuesB" | tr '\n' '+') 0 ))
- # max_progression is a total number of calls to this helper.
- # Less the number of calls with a weight value.
- # Plus the total of weight values
- max_progression=$(( $helper_calls - $weight_calls + $weight_values ))
- fi
+ # max_progression is a total number of calls to this helper.
+ # Less the number of calls with a weight value.
+ # Plus the total of weight values
+ max_progression=$(( $helper_calls - $weight_calls + $weight_values ))
+ fi
- # Increment each execution of ynh_script_progression in this script by the weight of the previous call.
- increment_progression=$(( $increment_progression + $previous_weight ))
- # Store the weight of the current call in $previous_weight for next call
- previous_weight=$weight
+ # Increment each execution of ynh_script_progression in this script by the weight of the previous call.
+ increment_progression=$(( $increment_progression + $previous_weight ))
+ # Store the weight of the current call in $previous_weight for next call
+ previous_weight=$weight
- # Reduce $increment_progression to the size of the scale
- if [ $last -eq 0 ]
- then
- local effective_progression=$(( $increment_progression * $progress_scale / $max_progression ))
- # If last is specified, fill immediately the progression_bar
- else
- local effective_progression=$progress_scale
- fi
+ # Reduce $increment_progression to the size of the scale
+ if [ $last -eq 0 ]
+ then
+ local effective_progression=$(( $increment_progression * $progress_scale / $max_progression ))
+ # If last is specified, fill immediately the progression_bar
+ else
+ local effective_progression=$progress_scale
+ fi
- # Build $progression_bar from progress_string(0,1,2) according to $effective_progression and the weight of the current task
- # expected_progression is the progression expected after the current task
- local expected_progression="$(( ( $increment_progression + $weight ) * $progress_scale / $max_progression - $effective_progression ))"
- if [ $last -eq 1 ]
- then
- expected_progression=0
- fi
- # left_progression is the progression not yet done
- local left_progression="$(( $progress_scale - $effective_progression - $expected_progression ))"
- # Build the progression bar with $effective_progression, work done, $expected_progression, current work and $left_progression, work to be done.
- local progression_bar="${progress_string2:0:$effective_progression}${progress_string1:0:$expected_progression}${progress_string0:0:$left_progression}"
+ # Build $progression_bar from progress_string(0,1,2) according to $effective_progression and the weight of the current task
+ # expected_progression is the progression expected after the current task
+ local expected_progression="$(( ( $increment_progression + $weight ) * $progress_scale / $max_progression - $effective_progression ))"
+ if [ $last -eq 1 ]
+ then
+ expected_progression=0
+ fi
+ # left_progression is the progression not yet done
+ local left_progression="$(( $progress_scale - $effective_progression - $expected_progression ))"
+ # Build the progression bar with $effective_progression, work done, $expected_progression, current work and $left_progression, work to be done.
+ local progression_bar="${progress_string2:0:$effective_progression}${progress_string1:0:$expected_progression}${progress_string0:0:$left_progression}"
- local print_exec_time=""
- if [ $time -eq 1 ]
- then
- print_exec_time=" [$(date +%Hh%Mm,%Ss --date="0 + $exec_time sec")]"
- fi
+ local print_exec_time=""
+ if [ $time -eq 1 ]
+ then
+ print_exec_time=" [$(date +%Hh%Mm,%Ss --date="0 + $exec_time sec")]"
+ fi
- ynh_print_info "[$progression_bar] > ${message}${print_exec_time}"
- set -x
+ ynh_print_info "[$progression_bar] > ${message}${print_exec_time}"
+ set -o xtrace # set -x
}
# Return data to the Yunohost core for later processing
@@ -316,63 +316,62 @@ ynh_return () {
#
# Requires YunoHost version 3.5.0 or higher.
ynh_debug () {
- # Disable set xtrace for the helper itself, to not pollute the debug log
- set +x
- # Declare an array to define the options of this helper.
- local legacy_args=mt
- declare -Ar args_array=( [m]=message= [t]=trace= )
- local message
- local trace
- # Manage arguments with getopts
- ynh_handle_getopts_args "$@"
- # Redisable xtrace, ynh_handle_getopts_args set it back
- set +x
- message=${message:-}
- trace=${trace:-}
+ # Disable set xtrace for the helper itself, to not pollute the debug log
+ set +o xtrace # set +x
+ # Declare an array to define the options of this helper.
+ local legacy_args=mt
+ local -A args_array=( [m]=message= [t]=trace= )
+ local message
+ local trace
+ # Manage arguments with getopts
+ ynh_handle_getopts_args "$@"
+ # Re-disable xtrace, ynh_handle_getopts_args set it back
+ set +o xtrace # set +x
+ message=${message:-}
+ trace=${trace:-}
- if [ -n "$message" ]
- then
- ynh_print_log "\e[34m\e[1m[DEBUG]\e[0m ${message}" >&2
- fi
+ if [ -n "$message" ]
+ then
+ ynh_print_log "\e[34m\e[1m[DEBUG]\e[0m ${message}" >&2
+ fi
- if [ "$trace" == "1" ]
- then
- ynh_debug --message="Enable debugging"
- set +x
- # Get the current file descriptor of xtrace
- old_bash_xtracefd=$BASH_XTRACEFD
- # Add the current file name and the line number of any command currently running while tracing.
- PS4='$(basename ${BASH_SOURCE[0]})-L${LINENO}: '
- # Force xtrace to stderr
- BASH_XTRACEFD=2
- # Force stdout to stderr
- exec 1>&2
- fi
- if [ "$trace" == "0" ]
- then
- ynh_debug --message="Disable debugging"
- set +x
- # Put xtrace back to its original fild descriptor
- BASH_XTRACEFD=$old_bash_xtracefd
- # Restore stdout
- exec 1>&1
- fi
- # Renable set xtrace
- set -x
+ if [ "$trace" == "1" ]
+ then
+ ynh_debug --message="Enable debugging"
+ set +o xtrace # set +x
+ # Get the current file descriptor of xtrace
+ old_bash_xtracefd=$BASH_XTRACEFD
+ # Add the current file name and the line number of any command currently running while tracing.
+ PS4='$(basename ${BASH_SOURCE[0]})-L${LINENO}: '
+ # Force xtrace to stderr
+ BASH_XTRACEFD=2
+ # Force stdout to stderr
+ exec 1>&2
+ fi
+ if [ "$trace" == "0" ]
+ then
+ ynh_debug --message="Disable debugging"
+ set +o xtrace # set +x
+ # Put xtrace back to its original fild descriptor
+ BASH_XTRACEFD=$old_bash_xtracefd
+ # Restore stdout
+ exec 1>&1
+ fi
+ # Renable set xtrace
+ set -o xtrace # set -x
}
# Execute a command and print the result as debug
#
# usage: ynh_debug_exec your_command
# usage: ynh_debug_exec "your_command | other_command"
+# | arg: command - command to execute
#
# When using pipes, double quotes are required - otherwise, this helper will run the first command, and the whole output will be sent through the next pipe.
#
# If the command to execute uses double quotes, they have to be escaped or they will be interpreted and removed.
#
-# | arg: command - command to execute
-#
# Requires YunoHost version 3.5.0 or higher.
ynh_debug_exec () {
- ynh_debug --message="$(eval $@)"
+ ynh_debug --message="$(eval $@)"
}
diff --git a/data/helpers.d/logrotate b/data/helpers.d/logrotate
index 47ce46cf6..d5384264c 100644
--- a/data/helpers.d/logrotate
+++ b/data/helpers.d/logrotate
@@ -3,9 +3,9 @@
# Use logrotate to manage the logfile
#
# usage: ynh_use_logrotate [--logfile=/log/file] [--nonappend] [--specific_user=user/group]
-# | arg: -l, --logfile - absolute path of logfile
-# | arg: -n, --nonappend - (optional) Replace the config file instead of appending this new config.
-# | arg: -u, --specific_user : run logrotate as the specified user and group. If not specified logrotate is runned as root.
+# | arg: -l, --logfile= - absolute path of logfile
+# | arg: -n, --nonappend - (optional) Replace the config file instead of appending this new config.
+# | arg: -u, --specific_user= - run logrotate as the specified user and group. If not specified logrotate is runned as root.
#
# If no --logfile is provided, /var/log/${app} will be used as default.
# logfile can be just a directory, or a full path to a logfile :
@@ -16,79 +16,88 @@
# the same logrotate config file. Unless you use the option --non-append
#
# Requires YunoHost version 2.6.4 or higher.
+# Requires YunoHost version 3.2.0 or higher for the argument --specific_user
ynh_use_logrotate () {
- # Declare an array to define the options of this helper.
- local legacy_args=lnuya
- declare -Ar args_array=( [l]=logfile= [n]=nonappend [u]=specific_user= [y]=non [a]=append )
- # [y]=non [a]=append are only for legacy purpose, to not fail on the old option '--non-append'
- local logfile
- local nonappend
- local specific_user
- # Manage arguments with getopts
- ynh_handle_getopts_args "$@"
- local logfile="${logfile:-}"
- local nonappend="${nonappend:-0}"
- local specific_user="${specific_user:-}"
+ # Declare an array to define the options of this helper.
+ local legacy_args=lnuya
+ local -A args_array=( [l]=logfile= [n]=nonappend [u]=specific_user= [y]=non [a]=append )
+ # [y]=non [a]=append are only for legacy purpose, to not fail on the old option '--non-append'
+ local logfile
+ local nonappend
+ local specific_user
+ # Manage arguments with getopts
+ ynh_handle_getopts_args "$@"
+ logfile="${logfile:-}"
+ nonappend="${nonappend:-0}"
+ specific_user="${specific_user:-}"
- # LEGACY CODE - PRE GETOPTS
- if [ $# -gt 0 ] && [ "$1" == "--non-append" ]; then
- nonappend=1
- # Destroy this argument for the next command.
- shift
- elif [ $# -gt 1 ] && [ "$2" == "--non-append" ]; then
- nonappend=1
- fi
+ # LEGACY CODE - PRE GETOPTS
+ if [ $# -gt 0 ] && [ "$1" == "--non-append" ]
+ then
+ nonappend=1
+ # Destroy this argument for the next command.
+ shift
+ elif [ $# -gt 1 ] && [ "$2" == "--non-append" ]
+ then
+ nonappend=1
+ fi
- if [ $# -gt 0 ] && [ "$(echo ${1:0:1})" != "-" ]; then
- if [ "$(echo ${1##*.})" == "log" ]; then # Keep only the extension to check if it's a logfile
- local logfile=$1 # In this case, focus logrotate on the logfile
- else
- local logfile=$1/*.log # Else, uses the directory and all logfile into it.
- fi
- fi
- # LEGACY CODE
+ if [ $# -gt 0 ] && [ "$(echo ${1:0:1})" != "-" ]
+ then
+ # If the given logfile parameter already exists as a file, or if it ends up with ".log",
+ # we just want to manage a single file
+ if [ -f "$1" ] || [ "$(echo ${1##*.})" == "log" ]
+ then
+ local logfile=$1
+ # Otherwise we assume we want to manage a directory and all its .log file inside
+ else
+ local logfile=$1/*.log
+ fi
+ fi
+ # LEGACY CODE
- local customtee="tee -a"
- if [ "$nonappend" -eq 1 ]; then
- customtee="tee"
- fi
- if [ -n "$logfile" ]
- then
- if [ "$(echo ${logfile##*.})" != "log" ]; then # Keep only the extension to check if it's a logfile
- local logfile="$logfile/*.log" # Else, uses the directory and all logfile into it.
- fi
- else
- logfile="/var/log/${app}/*.log" # Without argument, use a defaut directory in /var/log
- fi
- local su_directive=""
- if [[ -n $specific_user ]]; then
- su_directive=" # Run logorotate as specific user - group
- su ${specific_user%/*} ${specific_user#*/}"
- fi
+ local customtee="tee --append"
+ if [ "$nonappend" -eq 1 ]; then
+ customtee="tee"
+ fi
+ if [ -n "$logfile" ]
+ then
+ if [ ! -f "$1" ] && [ "$(echo ${logfile##*.})" != "log" ]; then # Keep only the extension to check if it's a logfile
+ local logfile="$logfile/*.log" # Else, uses the directory and all logfile into it.
+ fi
+ else
+ logfile="/var/log/${app}/*.log" # Without argument, use a defaut directory in /var/log
+ fi
+ local su_directive=""
+ if [[ -n $specific_user ]]
+ then
+ su_directive=" # Run logorotate as specific user - group
+ su ${specific_user%/*} ${specific_user#*/}"
+ fi
- cat > ./${app}-logrotate << EOF # Build a config file for logrotate
+ cat > ./${app}-logrotate << EOF # Build a config file for logrotate
$logfile {
- # Rotate if the logfile exceeds 100Mo
- size 100M
- # Keep 12 old log maximum
- rotate 12
- # Compress the logs with gzip
- compress
- # Compress the log at the next cycle. So keep always 2 non compressed logs
- delaycompress
- # Copy and truncate the log to allow to continue write on it. Instead of move the log.
- copytruncate
- # Do not do an error if the log is missing
- missingok
- # Not rotate if the log is empty
- notifempty
- # Keep old logs in the same dir
- noolddir
- $su_directive
+ # Rotate if the logfile exceeds 100Mo
+ size 100M
+ # Keep 12 old log maximum
+ rotate 12
+ # Compress the logs with gzip
+ compress
+ # Compress the log at the next cycle. So keep always 2 non compressed logs
+ delaycompress
+ # Copy and truncate the log to allow to continue write on it. Instead of move the log.
+ copytruncate
+ # Do not do an error if the log is missing
+ missingok
+ # Not rotate if the log is empty
+ notifempty
+ # Keep old logs in the same dir
+ noolddir
+ $su_directive
}
EOF
- sudo mkdir -p $(dirname "$logfile") # Create the log directory, if not exist
- cat ${app}-logrotate | sudo $customtee /etc/logrotate.d/$app > /dev/null # Append this config to the existing config file, or replace the whole config file (depending on $customtee)
+ mkdir --parents $(dirname "$logfile") # Create the log directory, if not exist
+ cat ${app}-logrotate | $customtee /etc/logrotate.d/$app > /dev/null # Append this config to the existing config file, or replace the whole config file (depending on $customtee)
}
# Remove the app's logrotate config.
@@ -97,7 +106,7 @@ EOF
#
# Requires YunoHost version 2.6.4 or higher.
ynh_remove_logrotate () {
- if [ -e "/etc/logrotate.d/$app" ]; then
- sudo rm "/etc/logrotate.d/$app"
- fi
+ if [ -e "/etc/logrotate.d/$app" ]; then
+ rm "/etc/logrotate.d/$app"
+ fi
}
diff --git a/data/helpers.d/mysql b/data/helpers.d/mysql
index e9cf59b3c..84acc1029 100644
--- a/data/helpers.d/mysql
+++ b/data/helpers.d/mysql
@@ -4,19 +4,19 @@ MYSQL_ROOT_PWD_FILE=/etc/yunohost/mysql
# Open a connection as a user
#
-# example: ynh_mysql_connect_as 'user' 'pass' <<< "UPDATE ...;"
-# example: ynh_mysql_connect_as 'user' 'pass' < /path/to/file.sql
+# example: ynh_mysql_connect_as --user="user" --password="pass" <<< "UPDATE ...;"
+# example: ynh_mysql_connect_as --user="user" --password="pass" < /path/to/file.sql
#
# usage: ynh_mysql_connect_as --user=user --password=password [--database=database]
-# | arg: -u, --user - the user name to connect as
-# | arg: -p, --password - the user password
-# | arg: -d, --database - the database to connect to
+# | arg: -u, --user= - the user name to connect as
+# | arg: -p, --password= - the user password
+# | arg: -d, --database= - the database to connect to
#
# Requires YunoHost version 2.2.4 or higher.
ynh_mysql_connect_as() {
# Declare an array to define the options of this helper.
local legacy_args=upd
- declare -Ar args_array=( [u]=user= [p]=password= [d]=database= )
+ local -A args_array=( [u]=user= [p]=password= [d]=database= )
local user
local password
local database
@@ -24,48 +24,48 @@ ynh_mysql_connect_as() {
ynh_handle_getopts_args "$@"
database="${database:-}"
- mysql -u "$user" --password="$password" -B "$database"
+ mysql --user="$user" --password="$password" --batch "$database"
}
# Execute a command as root user
#
# usage: ynh_mysql_execute_as_root --sql=sql [--database=database]
-# | arg: -s, --sql - the SQL command to execute
-# | arg: -d, --database - the database to connect to
+# | arg: -s, --sql= - the SQL command to execute
+# | arg: -d, --database= - the database to connect to
#
# Requires YunoHost version 2.2.4 or higher.
ynh_mysql_execute_as_root() {
# Declare an array to define the options of this helper.
local legacy_args=sd
- declare -Ar args_array=( [s]=sql= [d]=database= )
+ local -A args_array=( [s]=sql= [d]=database= )
local sql
local database
# Manage arguments with getopts
ynh_handle_getopts_args "$@"
database="${database:-}"
- ynh_mysql_connect_as --user="root" --password="$(sudo cat $MYSQL_ROOT_PWD_FILE)" \
+ ynh_mysql_connect_as --user="root" --password="$(cat $MYSQL_ROOT_PWD_FILE)" \
--database="$database" <<< "$sql"
}
# Execute a command from a file as root user
#
# usage: ynh_mysql_execute_file_as_root --file=file [--database=database]
-# | arg: -f, --file - the file containing SQL commands
-# | arg: -d, --database - the database to connect to
+# | arg: -f, --file= - the file containing SQL commands
+# | arg: -d, --database= - the database to connect to
#
# Requires YunoHost version 2.2.4 or higher.
ynh_mysql_execute_file_as_root() {
# Declare an array to define the options of this helper.
local legacy_args=fd
- declare -Ar args_array=( [f]=file= [d]=database= )
+ local -A args_array=( [f]=file= [d]=database= )
local file
local database
# Manage arguments with getopts
ynh_handle_getopts_args "$@"
database="${database:-}"
- ynh_mysql_connect_as --user="root" --password="$(sudo cat $MYSQL_ROOT_PWD_FILE)" \
+ ynh_mysql_connect_as --user="root" --password="$(cat $MYSQL_ROOT_PWD_FILE)" \
--database="$database" < "$file"
}
@@ -85,9 +85,12 @@ ynh_mysql_create_db() {
local sql="CREATE DATABASE ${db};"
# grant all privilegies to user
- if [[ $# -gt 1 ]]; then
+ if [[ $# -gt 1 ]]
+ then
sql+=" GRANT ALL PRIVILEGES ON ${db}.* TO '${2}'@'localhost'"
- [[ -n ${3:-} ]] && sql+=" IDENTIFIED BY '${3}'"
+ if [[ -n ${3:-} ]]; then
+ sql+=" IDENTIFIED BY '${3}'"
+ fi
sql+=" WITH GRANT OPTION;"
fi
@@ -111,22 +114,22 @@ ynh_mysql_drop_db() {
# Dump a database
#
-# example: ynh_mysql_dump_db 'roundcube' > ./dump.sql
+# example: ynh_mysql_dump_db --database=roundcube > ./dump.sql
#
# usage: ynh_mysql_dump_db --database=database
-# | arg: -d, --database - the database name to dump
+# | arg: -d, --database= - the database name to dump
# | ret: the mysqldump output
#
# Requires YunoHost version 2.2.4 or higher.
ynh_mysql_dump_db() {
# Declare an array to define the options of this helper.
local legacy_args=d
- declare -Ar args_array=( [d]=database= )
+ local -A args_array=( [d]=database= )
local database
# Manage arguments with getopts
ynh_handle_getopts_args "$@"
- mysqldump -u "root" -p"$(sudo cat $MYSQL_ROOT_PWD_FILE)" --single-transaction --skip-dump-date "$database"
+ mysqldump --user="root" --password="$(cat $MYSQL_ROOT_PWD_FILE)" --single-transaction --skip-dump-date "$database"
}
# Create a user
@@ -146,24 +149,25 @@ ynh_mysql_create_user() {
# Check if a mysql user exists
#
# usage: ynh_mysql_user_exists --user=user
-# | arg: -u, --user - the user for which to check existence
+# | arg: -u, --user= - the user for which to check existence
+# | exit: Return 1 if the user doesn't exist, 0 otherwise.
#
# Requires YunoHost version 2.2.4 or higher.
ynh_mysql_user_exists()
{
- # Declare an array to define the options of this helper.
- local legacy_args=u
- declare -Ar args_array=( [u]=user= )
- local user
- # Manage arguments with getopts
- ynh_handle_getopts_args "$@"
+ # Declare an array to define the options of this helper.
+ local legacy_args=u
+ local -A args_array=( [u]=user= )
+ local user
+ # Manage arguments with getopts
+ ynh_handle_getopts_args "$@"
- if [[ -z $(ynh_mysql_execute_as_root --sql="SELECT User from mysql.user WHERE User = '$user';") ]]
- then
- return 1
- else
- return 0
- fi
+ if [[ -z $(ynh_mysql_execute_as_root --sql="SELECT User from mysql.user WHERE User = '$user';") ]]
+ then
+ return 1
+ else
+ return 0
+ fi
}
# Drop a user
@@ -180,59 +184,59 @@ ynh_mysql_drop_user() {
# Create a database, an user and its password. Then store the password in the app's config
#
+# usage: ynh_mysql_setup_db --db_user=user --db_name=name [--db_pwd=pwd]
+# | arg: -u, --db_user= - Owner of the database
+# | arg: -n, --db_name= - Name of the database
+# | arg: -p, --db_pwd= - Password of the database. If not provided, a password will be generated
+#
# After executing this helper, the password of the created database will be available in $db_pwd
# It will also be stored as "mysqlpwd" into the app settings.
#
-# usage: ynh_mysql_setup_db --db_user=user --db_name=name [--db_pwd=pwd]
-# | arg: -u, --db_user - Owner of the database
-# | arg: -n, --db_name - Name of the database
-# | arg: -p, --db_pwd - Password of the database. If not provided, a password will be generated
-#
# Requires YunoHost version 2.6.4 or higher.
ynh_mysql_setup_db () {
- # Declare an array to define the options of this helper.
- local legacy_args=unp
- declare -Ar args_array=( [u]=db_user= [n]=db_name= [p]=db_pwd= )
- local db_user
- local db_name
- db_pwd=""
- # Manage arguments with getopts
- ynh_handle_getopts_args "$@"
+ # Declare an array to define the options of this helper.
+ local legacy_args=unp
+ local -A args_array=( [u]=db_user= [n]=db_name= [p]=db_pwd= )
+ local db_user
+ local db_name
+ db_pwd=""
+ # Manage arguments with getopts
+ ynh_handle_getopts_args "$@"
- local new_db_pwd=$(ynh_string_random) # Generate a random password
- # If $db_pwd is not provided, use new_db_pwd instead for db_pwd
- db_pwd="${db_pwd:-$new_db_pwd}"
+ local new_db_pwd=$(ynh_string_random) # Generate a random password
+ # If $db_pwd is not provided, use new_db_pwd instead for db_pwd
+ db_pwd="${db_pwd:-$new_db_pwd}"
- ynh_mysql_create_db "$db_name" "$db_user" "$db_pwd" # Create the database
- ynh_app_setting_set --app=$app --key=mysqlpwd --value=$db_pwd # Store the password in the app's config
+ ynh_mysql_create_db "$db_name" "$db_user" "$db_pwd" # Create the database
+ ynh_app_setting_set --app=$app --key=mysqlpwd --value=$db_pwd # Store the password in the app's config
}
# Remove a database if it exists, and the associated user
#
# usage: ynh_mysql_remove_db --db_user=user --db_name=name
-# | arg: -u, --db_user - Owner of the database
-# | arg: -n, --db_name - Name of the database
+# | arg: -u, --db_user= - Owner of the database
+# | arg: -n, --db_name= - Name of the database
#
# Requires YunoHost version 2.6.4 or higher.
ynh_mysql_remove_db () {
- # Declare an array to define the options of this helper.
- local legacy_args=un
- declare -Ar args_array=( [u]=db_user= [n]=db_name= )
- local db_user
- local db_name
- # Manage arguments with getopts
- ynh_handle_getopts_args "$@"
+ # Declare an array to define the options of this helper.
+ local legacy_args=un
+ local -A args_array=( [u]=db_user= [n]=db_name= )
+ local db_user
+ local db_name
+ # Manage arguments with getopts
+ ynh_handle_getopts_args "$@"
- local mysql_root_password=$(sudo cat $MYSQL_ROOT_PWD_FILE)
- if mysqlshow -u root -p$mysql_root_password | grep -q "^| $db_name"; then # Check if the database exists
- ynh_mysql_drop_db $db_name # Remove the database
- else
- ynh_print_warn --message="Database $db_name not found"
- fi
+ local mysql_root_password=$(cat $MYSQL_ROOT_PWD_FILE)
+ if mysqlshow --user=root --password=$mysql_root_password | grep --quiet "^| $db_name"
+ then # Check if the database exists
+ ynh_mysql_drop_db $db_name # Remove the database
+ else
+ ynh_print_warn --message="Database $db_name not found"
+ fi
- # Remove mysql user if it exists
- if ynh_mysql_user_exists --user=$db_user; then
- ynh_mysql_drop_user $db_user
- fi
+ # Remove mysql user if it exists
+ if ynh_mysql_user_exists --user=$db_user; then
+ ynh_mysql_drop_user $db_user
+ fi
}
-
diff --git a/data/helpers.d/network b/data/helpers.d/network
index 0f75cb165..4f108422b 100644
--- a/data/helpers.d/network
+++ b/data/helpers.d/network
@@ -5,27 +5,56 @@
# example: port=$(ynh_find_port --port=8080)
#
# usage: ynh_find_port --port=begin_port
-# | arg: -p, --port - port to start to search
+# | arg: -p, --port= - port to start to search
+# | ret: the port number
#
# Requires YunoHost version 2.6.4 or higher.
ynh_find_port () {
- # Declare an array to define the options of this helper.
- local legacy_args=p
- declare -Ar args_array=( [p]=port= )
- local port
- # Manage arguments with getopts
- ynh_handle_getopts_args "$@"
+ # Declare an array to define the options of this helper.
+ local legacy_args=p
+ local -A args_array=( [p]=port= )
+ local port
+ # Manage arguments with getopts
+ ynh_handle_getopts_args "$@"
- test -n "$port" || ynh_die --message="The argument of ynh_find_port must be a valid port."
- while netcat -z 127.0.0.1 $port # Check if the port is free
- do
- port=$((port+1)) # Else, pass to next port
- done
- echo $port
+ test -n "$port" || ynh_die --message="The argument of ynh_find_port must be a valid port."
+ while ! ynh_port_available --port=$port
+ do
+ port=$((port+1)) # Else, pass to next port
+ done
+ echo $port
}
+# Test if a port is available
+#
+# example: ynh_port_available --port=1234 || ynh_die "Port 1234 is needs to be available for this app"
+#
+# usage: ynh_find_port --port=XYZ
+# | arg: -p, --port= - port to check
+# | exit: Return 1 if the port is already used by another process.
+#
+# Requires YunoHost version 3.8.0 or higher.
+ynh_port_available () {
+ # Declare an array to define the options of this helper.
+ local legacy_args=p
+ local -A args_array=( [p]=port= )
+ local port
+ # Manage arguments with getopts
+ ynh_handle_getopts_args "$@"
+
+ if ss --numeric --listening --tcp --udp | awk '{print$5}' | grep --quiet --extended-regexp ":$port$" # Check if the port is free
+ then
+ return 1
+ else
+ return 0
+ fi
+}
+
+
# Validate an IP address
#
+# [internal]
+#
# usage: ynh_validate_ip --family=family --ip_address=ip_address
# | ret: 0 for valid ip addresses, 1 otherwise
#
@@ -34,17 +63,17 @@ ynh_find_port () {
# Requires YunoHost version 2.2.4 or higher.
ynh_validate_ip()
{
- # http://stackoverflow.com/questions/319279/how-to-validate-ip-address-in-python#319298
+ # http://stackoverflow.com/questions/319279/how-to-validate-ip-address-in-python#319298
- # Declare an array to define the options of this helper.
- local legacy_args=fi
- declare -Ar args_array=( [f]=family= [i]=ip_address= )
- local family
- local ip_address
- # Manage arguments with getopts
- ynh_handle_getopts_args "$@"
+ # Declare an array to define the options of this helper.
+ local legacy_args=fi
+ local -A args_array=( [f]=family= [i]=ip_address= )
+ local family
+ local ip_address
+ # Manage arguments with getopts
+ ynh_handle_getopts_args "$@"
- [ "$family" == "4" ] || [ "$family" == "6" ] || return 1
+ [ "$family" == "4" ] || [ "$family" == "6" ] || return 1
python /dev/stdin << EOF
import socket
@@ -63,19 +92,20 @@ EOF
# example: ynh_validate_ip4 111.222.333.444
#
# usage: ynh_validate_ip4 --ip_address=ip_address
+# | arg: -i, --ip_address= - the ipv4 address to check
# | ret: 0 for valid ipv4 addresses, 1 otherwise
#
# Requires YunoHost version 2.2.4 or higher.
ynh_validate_ip4()
{
- # Declare an array to define the options of this helper.
- local legacy_args=i
- declare -Ar args_array=( [i]=ip_address= )
- local ip_address
- # Manage arguments with getopts
- ynh_handle_getopts_args "$@"
+ # Declare an array to define the options of this helper.
+ local legacy_args=i
+ local -A args_array=( [i]=ip_address= )
+ local ip_address
+ # Manage arguments with getopts
+ ynh_handle_getopts_args "$@"
- ynh_validate_ip 4 $ip_address
+ ynh_validate_ip --family=4 --ip_address=$ip_address
}
@@ -84,17 +114,18 @@ ynh_validate_ip4()
# example: ynh_validate_ip6 2000:dead:beef::1
#
# usage: ynh_validate_ip6 --ip_address=ip_address
+# | arg: -i, --ip_address= - the ipv6 address to check
# | ret: 0 for valid ipv6 addresses, 1 otherwise
#
# Requires YunoHost version 2.2.4 or higher.
ynh_validate_ip6()
{
- # Declare an array to define the options of this helper.
- local legacy_args=i
- declare -Ar args_array=( [i]=ip_address= )
- local ip_address
- # Manage arguments with getopts
- ynh_handle_getopts_args "$@"
+ # Declare an array to define the options of this helper.
+ local legacy_args=i
+ local -A args_array=( [i]=ip_address= )
+ local ip_address
+ # Manage arguments with getopts
+ ynh_handle_getopts_args "$@"
- ynh_validate_ip 6 $ip_address
+ ynh_validate_ip --family=6 --ip_address=$ip_address
}
diff --git a/data/helpers.d/nginx b/data/helpers.d/nginx
index ce6b61d3c..cd4380f16 100644
--- a/data/helpers.d/nginx
+++ b/data/helpers.d/nginx
@@ -12,57 +12,61 @@
# __PORT__ by $port
# __NAME__ by $app
# __FINALPATH__ by $final_path
+# __PHPVERSION__ by $YNH_PHP_VERSION ($YNH_PHP_VERSION is either the default php version or the version defined for the app)
#
# And dynamic variables (from the last example) :
# __PATH_2__ by $path_2
# __PORT_2__ by $port_2
#
# Requires YunoHost version 2.7.2 or higher.
+# Requires YunoHost version 2.7.13 or higher for dynamic variables
ynh_add_nginx_config () {
- finalnginxconf="/etc/nginx/conf.d/$domain.d/$app.conf"
- local others_var=${1:-}
- ynh_backup_if_checksum_is_different --file="$finalnginxconf"
- sudo cp ../conf/nginx.conf "$finalnginxconf"
+ finalnginxconf="/etc/nginx/conf.d/$domain.d/$app.conf"
+ local others_var=${1:-}
+ ynh_backup_if_checksum_is_different --file="$finalnginxconf"
+ cp ../conf/nginx.conf "$finalnginxconf"
- # To avoid a break by set -u, use a void substitution ${var:-}. If the variable is not set, it's simply set with an empty variable.
- # Substitute in a nginx config file only if the variable is not empty
- if test -n "${path_url:-}"; then
- # path_url_slash_less is path_url, or a blank value if path_url is only '/'
- local path_url_slash_less=${path_url%/}
- ynh_replace_string --match_string="__PATH__/" --replace_string="$path_url_slash_less/" --target_file="$finalnginxconf"
- ynh_replace_string --match_string="__PATH__" --replace_string="$path_url" --target_file="$finalnginxconf"
- fi
- if test -n "${domain:-}"; then
- ynh_replace_string --match_string="__DOMAIN__" --replace_string="$domain" --target_file="$finalnginxconf"
- fi
- if test -n "${port:-}"; then
- ynh_replace_string --match_string="__PORT__" --replace_string="$port" --target_file="$finalnginxconf"
- fi
- if test -n "${app:-}"; then
- ynh_replace_string --match_string="__NAME__" --replace_string="$app" --target_file="$finalnginxconf"
- fi
- if test -n "${final_path:-}"; then
- ynh_replace_string --match_string="__FINALPATH__" --replace_string="$final_path" --target_file="$finalnginxconf"
- fi
+ # To avoid a break by set -u, use a void substitution ${var:-}. If the variable is not set, it's simply set with an empty variable.
+ # Substitute in a nginx config file only if the variable is not empty
+ if test -n "${path_url:-}"
+ then
+ # path_url_slash_less is path_url, or a blank value if path_url is only '/'
+ local path_url_slash_less=${path_url%/}
+ ynh_replace_string --match_string="__PATH__/" --replace_string="$path_url_slash_less/" --target_file="$finalnginxconf"
+ ynh_replace_string --match_string="__PATH__" --replace_string="$path_url" --target_file="$finalnginxconf"
+ fi
+ if test -n "${domain:-}"; then
+ ynh_replace_string --match_string="__DOMAIN__" --replace_string="$domain" --target_file="$finalnginxconf"
+ fi
+ if test -n "${port:-}"; then
+ ynh_replace_string --match_string="__PORT__" --replace_string="$port" --target_file="$finalnginxconf"
+ fi
+ if test -n "${app:-}"; then
+ ynh_replace_string --match_string="__NAME__" --replace_string="$app" --target_file="$finalnginxconf"
+ fi
+ if test -n "${final_path:-}"; then
+ ynh_replace_string --match_string="__FINALPATH__" --replace_string="$final_path" --target_file="$finalnginxconf"
+ fi
+ ynh_replace_string --match_string="__PHPVERSION__" --replace_string="$YNH_PHP_VERSION" --target_file="$finalnginxconf"
- # Replace all other variable given as arguments
- for var_to_replace in $others_var
- do
- # ${var_to_replace^^} make the content of the variable on upper-cases
- # ${!var_to_replace} get the content of the variable named $var_to_replace
- ynh_replace_string --match_string="__${var_to_replace^^}__" --replace_string="${!var_to_replace}" --target_file="$finalnginxconf"
- done
-
- if [ "${path_url:-}" != "/" ]
- then
- ynh_replace_string --match_string="^#sub_path_only" --replace_string="" --target_file="$finalnginxconf"
- else
- ynh_replace_string --match_string="^#root_path_only" --replace_string="" --target_file="$finalnginxconf"
- fi
+ # Replace all other variable given as arguments
+ for var_to_replace in $others_var
+ do
+ # ${var_to_replace^^} make the content of the variable on upper-cases
+ # ${!var_to_replace} get the content of the variable named $var_to_replace
+ ynh_replace_string --match_string="__${var_to_replace^^}__" --replace_string="${!var_to_replace}" --target_file="$finalnginxconf"
+ done
- ynh_store_file_checksum --file="$finalnginxconf"
+ if [ "${path_url:-}" != "/" ]
+ then
+ ynh_replace_string --match_string="^#sub_path_only" --replace_string="" --target_file="$finalnginxconf"
+ else
+ ynh_replace_string --match_string="^#root_path_only" --replace_string="" --target_file="$finalnginxconf"
+ fi
- ynh_systemd_action --service_name=nginx --action=reload
+ ynh_store_file_checksum --file="$finalnginxconf"
+
+ ynh_systemd_action --service_name=nginx --action=reload
}
# Remove the dedicated nginx config
@@ -71,6 +75,6 @@ ynh_add_nginx_config () {
#
# Requires YunoHost version 2.7.2 or higher.
ynh_remove_nginx_config () {
- ynh_secure_remove --file="/etc/nginx/conf.d/$domain.d/$app.conf"
- ynh_systemd_action --service_name=nginx --action=reload
+ ynh_secure_remove --file="/etc/nginx/conf.d/$domain.d/$app.conf"
+ ynh_systemd_action --service_name=nginx --action=reload
}
diff --git a/data/helpers.d/nodejs b/data/helpers.d/nodejs
index 288240b1b..efb50ae37 100644
--- a/data/helpers.d/nodejs
+++ b/data/helpers.d/nodejs
@@ -13,16 +13,16 @@ export N_PREFIX="$n_install_dir"
#
# Requires YunoHost version 2.7.12 or higher.
ynh_install_n () {
- ynh_print_info --message="Installation of N - Node.js version management"
- # Build an app.src for n
- mkdir -p "../conf"
- echo "SOURCE_URL=https://github.com/tj/n/archive/v4.1.0.tar.gz
+ ynh_print_info --message="Installation of N - Node.js version management"
+ # Build an app.src for n
+ mkdir --parents "../conf"
+ echo "SOURCE_URL=https://github.com/tj/n/archive/v4.1.0.tar.gz
SOURCE_SUM=3983fa3f00d4bf85ba8e21f1a590f6e28938093abe0bb950aeea52b1717471fc" > "../conf/n.src"
- # Download and extract n
- ynh_setup_source --dest_dir="$n_install_dir/git" --source_id=n
- # Install n
- (cd "$n_install_dir/git"
- PREFIX=$N_PREFIX make install 2>&1)
+ # Download and extract n
+ ynh_setup_source --dest_dir="$n_install_dir/git" --source_id=n
+ # Install n
+ (cd "$n_install_dir/git"
+ PREFIX=$N_PREFIX make install 2>&1)
}
# Load the version of node for an app, and set variables.
@@ -41,95 +41,97 @@ SOURCE_SUM=3983fa3f00d4bf85ba8e21f1a590f6e28938093abe0bb950aeea52b1717471fc" > "
#
# Requires YunoHost version 2.7.12 or higher.
ynh_use_nodejs () {
- nodejs_version=$(ynh_app_setting_get --app=$app --key=nodejs_version)
+ nodejs_version=$(ynh_app_setting_get --app=$app --key=nodejs_version)
- nodejs_use_version="echo \"Deprecated command, should be removed\""
+ nodejs_use_version="echo \"Deprecated command, should be removed\""
- # Get the absolute path of this version of node
- nodejs_path="$node_version_path/$nodejs_version/bin"
+ # Get the absolute path of this version of node
+ nodejs_path="$node_version_path/$nodejs_version/bin"
- # Load the path of this version of node in $PATH
- [[ :$PATH: == *":$nodejs_path"* ]] || PATH="$nodejs_path:$PATH"
+ # Load the path of this version of node in $PATH
+ if [[ :$PATH: != *":$nodejs_path"* ]]; then
+ PATH="$nodejs_path:$PATH"
+ fi
}
# Install a specific version of nodejs
#
-# n (Node version management) uses the PATH variable to store the path of the version of node it is going to use.
-# That's how it changes the version
-#
# ynh_install_nodejs will install the version of node provided as argument by using n.
#
# usage: ynh_install_nodejs --nodejs_version=nodejs_version
-# | arg: -n, --nodejs_version - Version of node to install. When possible, your should prefer to use major version number (e.g. 8 instead of 8.10.0). The crontab will then handle the update of minor versions when needed.
+# | arg: -n, --nodejs_version= - Version of node to install. When possible, your should prefer to use major version number (e.g. 8 instead of 8.10.0). The crontab will then handle the update of minor versions when needed.
+#
+# n (Node version management) uses the PATH variable to store the path of the version of node it is going to use.
+# That's how it changes the version
#
# Requires YunoHost version 2.7.12 or higher.
ynh_install_nodejs () {
- # Use n, https://github.com/tj/n to manage the nodejs versions
+ # Use n, https://github.com/tj/n to manage the nodejs versions
- # Declare an array to define the options of this helper.
- local legacy_args=n
- declare -Ar args_array=( [n]=nodejs_version= )
- local nodejs_version
- # Manage arguments with getopts
- ynh_handle_getopts_args "$@"
+ # Declare an array to define the options of this helper.
+ local legacy_args=n
+ local -A args_array=( [n]=nodejs_version= )
+ local nodejs_version
+ # Manage arguments with getopts
+ ynh_handle_getopts_args "$@"
- # Create $n_install_dir
- mkdir -p "$n_install_dir"
+ # Create $n_install_dir
+ mkdir --parents "$n_install_dir"
- # Load n path in PATH
- CLEAR_PATH="$n_install_dir/bin:$PATH"
- # Remove /usr/local/bin in PATH in case of node prior installation
- PATH=$(echo $CLEAR_PATH | sed 's@/usr/local/bin:@@')
+ # Load n path in PATH
+ CLEAR_PATH="$n_install_dir/bin:$PATH"
+ # Remove /usr/local/bin in PATH in case of node prior installation
+ PATH=$(echo $CLEAR_PATH | sed 's@/usr/local/bin:@@')
- # Move an existing node binary, to avoid to block n.
- test -x /usr/bin/node && mv /usr/bin/node /usr/bin/node_n
- test -x /usr/bin/npm && mv /usr/bin/npm /usr/bin/npm_n
+ # Move an existing node binary, to avoid to block n.
+ test -x /usr/bin/node && mv /usr/bin/node /usr/bin/node_n
+ test -x /usr/bin/npm && mv /usr/bin/npm /usr/bin/npm_n
- # If n is not previously setup, install it
- if ! test $(n --version > /dev/null 2>&1)
- then
- ynh_install_n
- fi
+ # If n is not previously setup, install it
+ if ! test $(n --version > /dev/null 2>&1)
+ then
+ ynh_install_n
+ fi
- # Modify the default N_PREFIX in n script
- ynh_replace_string --match_string="^N_PREFIX=\${N_PREFIX-.*}$" --replace_string="N_PREFIX=\${N_PREFIX-$N_PREFIX}" --target_file="$n_install_dir/bin/n"
+ # Modify the default N_PREFIX in n script
+ ynh_replace_string --match_string="^N_PREFIX=\${N_PREFIX-.*}$" --replace_string="N_PREFIX=\${N_PREFIX-$N_PREFIX}" --target_file="$n_install_dir/bin/n"
- # Restore /usr/local/bin in PATH
- PATH=$CLEAR_PATH
+ # Restore /usr/local/bin in PATH
+ PATH=$CLEAR_PATH
- # And replace the old node binary.
- test -x /usr/bin/node_n && mv /usr/bin/node_n /usr/bin/node
- test -x /usr/bin/npm_n && mv /usr/bin/npm_n /usr/bin/npm
+ # And replace the old node binary.
+ test -x /usr/bin/node_n && mv /usr/bin/node_n /usr/bin/node
+ test -x /usr/bin/npm_n && mv /usr/bin/npm_n /usr/bin/npm
- # Install the requested version of nodejs
- uname=$(uname -m)
- if [[ $uname =~ aarch64 || $uname =~ arm64 ]]
- then
- n $nodejs_version --arch=arm64
- else
- n $nodejs_version
- fi
+ # Install the requested version of nodejs
+ uname=$(uname --machine)
+ if [[ $uname =~ aarch64 || $uname =~ arm64 ]]
+ then
+ n $nodejs_version --arch=arm64
+ else
+ n $nodejs_version
+ fi
- # Find the last "real" version for this major version of node.
- real_nodejs_version=$(find $node_version_path/$nodejs_version* -maxdepth 0 | sort --version-sort | tail --lines=1)
- real_nodejs_version=$(basename $real_nodejs_version)
+ # Find the last "real" version for this major version of node.
+ real_nodejs_version=$(find $node_version_path/$nodejs_version* -maxdepth 0 | sort --version-sort | tail --lines=1)
+ real_nodejs_version=$(basename $real_nodejs_version)
- # Create a symbolic link for this major version if the file doesn't already exist
- if [ ! -e "$node_version_path/$nodejs_version" ]
- then
- ln --symbolic --force --no-target-directory $node_version_path/$real_nodejs_version $node_version_path/$nodejs_version
- fi
+ # Create a symbolic link for this major version if the file doesn't already exist
+ if [ ! -e "$node_version_path/$nodejs_version" ]
+ then
+ ln --symbolic --force --no-target-directory $node_version_path/$real_nodejs_version $node_version_path/$nodejs_version
+ fi
- # Store the ID of this app and the version of node requested for it
- echo "$YNH_APP_INSTANCE_NAME:$nodejs_version" | tee --append "$n_install_dir/ynh_app_version"
+ # Store the ID of this app and the version of node requested for it
+ echo "$YNH_APP_INSTANCE_NAME:$nodejs_version" | tee --append "$n_install_dir/ynh_app_version"
- # Store nodejs_version into the config of this app
- ynh_app_setting_set --app=$app --key=nodejs_version --value=$nodejs_version
+ # Store nodejs_version into the config of this app
+ ynh_app_setting_set --app=$app --key=nodejs_version --value=$nodejs_version
- # Build the update script and set the cronjob
- ynh_cron_upgrade_node
+ # Build the update script and set the cronjob
+ ynh_cron_upgrade_node
- ynh_use_nodejs
+ ynh_use_nodejs
}
# Remove the version of node used by the app.
@@ -142,25 +144,25 @@ ynh_install_nodejs () {
#
# Requires YunoHost version 2.7.12 or higher.
ynh_remove_nodejs () {
- nodejs_version=$(ynh_app_setting_get --app=$app --key=nodejs_version)
+ nodejs_version=$(ynh_app_setting_get --app=$app --key=nodejs_version)
- # Remove the line for this app
- sed --in-place "/$YNH_APP_INSTANCE_NAME:$nodejs_version/d" "$n_install_dir/ynh_app_version"
+ # Remove the line for this app
+ sed --in-place "/$YNH_APP_INSTANCE_NAME:$nodejs_version/d" "$n_install_dir/ynh_app_version"
- # If no other app uses this version of nodejs, remove it.
- if ! grep --quiet "$nodejs_version" "$n_install_dir/ynh_app_version"
- then
- $n_install_dir/bin/n rm $nodejs_version
- fi
+ # If no other app uses this version of nodejs, remove it.
+ if ! grep --quiet "$nodejs_version" "$n_install_dir/ynh_app_version"
+ then
+ $n_install_dir/bin/n rm $nodejs_version
+ fi
- # If no other app uses n, remove n
- if [ ! -s "$n_install_dir/ynh_app_version" ]
- then
- ynh_secure_remove --file="$n_install_dir"
- ynh_secure_remove --file="/usr/local/n"
- sed --in-place "/N_PREFIX/d" /root/.bashrc
- rm -f /etc/cron.daily/node_update
- fi
+ # If no other app uses n, remove n
+ if [ ! -s "$n_install_dir/ynh_app_version" ]
+ then
+ ynh_secure_remove --file="$n_install_dir"
+ ynh_secure_remove --file="/usr/local/n"
+ sed --in-place "/N_PREFIX/d" /root/.bashrc
+ rm --force /etc/cron.daily/node_update
+ fi
}
# Set a cron design to update your node versions
@@ -173,8 +175,8 @@ ynh_remove_nodejs () {
#
# Requires YunoHost version 2.7.12 or higher.
ynh_cron_upgrade_node () {
- # Build the update script
- cat > "$n_install_dir/node_update.sh" << EOF
+ # Build the update script
+ cat > "$n_install_dir/node_update.sh" << EOF
#!/bin/bash
version_path="$node_version_path"
@@ -195,26 +197,26 @@ all_real_version=\$(echo "\$all_real_version" | sort --unique)
# Read each major version
while read version
do
- echo "Update of the version \$version"
- sudo \$n_install_dir/bin/n \$version
+ echo "Update of the version \$version"
+ sudo \$n_install_dir/bin/n \$version
- # Find the last "real" version for this major version of node.
- real_nodejs_version=\$(find \$version_path/\$version* -maxdepth 0 | sort --version-sort | tail --lines=1)
- real_nodejs_version=\$(basename \$real_nodejs_version)
+ # Find the last "real" version for this major version of node.
+ real_nodejs_version=\$(find \$version_path/\$version* -maxdepth 0 | sort --version-sort | tail --lines=1)
+ real_nodejs_version=\$(basename \$real_nodejs_version)
- # Update the symbolic link for this version
- sudo ln --symbolic --force --no-target-directory \$version_path/\$real_nodejs_version \$version_path/\$version
+ # Update the symbolic link for this version
+ sudo ln --symbolic --force --no-target-directory \$version_path/\$real_nodejs_version \$version_path/\$version
done <<< "\$(echo "\$all_real_version")"
EOF
- chmod +x "$n_install_dir/node_update.sh"
+ chmod +x "$n_install_dir/node_update.sh"
- # Build the cronjob
- cat > "/etc/cron.daily/node_update" << EOF
+ # Build the cronjob
+ cat > "/etc/cron.daily/node_update" << EOF
#!/bin/bash
$n_install_dir/node_update.sh >> $n_install_dir/node_update.log
EOF
- chmod +x "/etc/cron.daily/node_update"
+ chmod +x "/etc/cron.daily/node_update"
}
diff --git a/data/helpers.d/php b/data/helpers.d/php
index c9e3ba9ed..e8de6d9ff 100644
--- a/data/helpers.d/php
+++ b/data/helpers.d/php
@@ -1,51 +1,264 @@
#!/bin/bash
+readonly YNH_DEFAULT_PHP_VERSION=7.0
+# Declare the actual php version to use.
+# A packager willing to use another version of php can override the variable into its _common.sh.
+YNH_PHP_VERSION=${YNH_PHP_VERSION:-$YNH_DEFAULT_PHP_VERSION}
+
# Create a dedicated php-fpm config
#
-# usage: ynh_add_fpm_config [--phpversion=7.X]
-# | arg: -v, --phpversion - Version of php to use.
+# usage 1: ynh_add_fpm_config [--phpversion=7.X] [--use_template] [--package=packages] [--dedicated_service]
+# | arg: -v, --phpversion= - Version of php to use.
+# | arg: -t, --use_template - Use this helper in template mode.
+# | arg: -p, --package= - Additionnal php packages to install
+# | arg: -d, --dedicated_service - Use a dedicated php-fpm service instead of the common one.
+#
+# -----------------------------------------------------------------------------
+#
+# usage 2: ynh_add_fpm_config [--phpversion=7.X] --usage=usage --footprint=footprint [--package=packages] [--dedicated_service]
+# | arg: -v, --phpversion= - Version of php to use.
+# | arg: -f, --footprint= - Memory footprint of the service (low/medium/high).
+# low - Less than 20Mb of ram by pool.
+# medium - Between 20Mb and 40Mb of ram by pool.
+# high - More than 40Mb of ram by pool.
+# Or specify exactly the footprint, the load of the service as Mb by pool instead of having a standard value.
+# To have this value, use the following command and stress the service.
+# watch -n0.5 ps -o user,cmd,%cpu,rss -u APP
+#
+# | arg: -u, --usage= - Expected usage of the service (low/medium/high).
+# low - Personal usage, behind the sso.
+# medium - Low usage, few people or/and publicly accessible.
+# high - High usage, frequently visited website.
+#
+# | arg: -p, --package= - Additionnal php packages to install for a specific version of php
+# | arg: -d, --dedicated_service - Use a dedicated php-fpm service instead of the common one.
+#
+#
+# The footprint of the service will be used to defined the maximum footprint we can allow, which is half the maximum RAM.
+# So it will be used to defined 'pm.max_children'
+# A lower value for the footprint will allow more children for 'pm.max_children'. And so for
+# 'pm.start_servers', 'pm.min_spare_servers' and 'pm.max_spare_servers' which are defined from the
+# value of 'pm.max_children'
+# NOTE: 'pm.max_children' can't exceed 4 times the number of processor's cores.
+#
+# The usage value will defined the way php will handle the children for the pool.
+# A value set as 'low' will set the process manager to 'ondemand'. Children will start only if the
+# service is used, otherwise no child will stay alive. This config gives the lower footprint when the
+# service is idle. But will use more proc since it has to start a child as soon it's used.
+# Set as 'medium', the process manager will be at dynamic. If the service is idle, a number of children
+# equal to pm.min_spare_servers will stay alive. So the service can be quick to answer to any request.
+# The number of children can grow if needed. The footprint can stay low if the service is idle, but
+# not null. The impact on the proc is a little bit less than 'ondemand' as there's always a few
+# children already available.
+# Set as 'high', the process manager will be set at 'static'. There will be always as many children as
+# 'pm.max_children', the footprint is important (but will be set as maximum a quarter of the maximum
+# RAM) but the impact on the proc is lower. The service will be quick to answer as there's always many
+# children ready to answer.
#
# Requires YunoHost version 2.7.2 or higher.
+# Requires YunoHost version 3.5.1 or higher for the argument --phpversion
+# Requires YunoHost version 3.8.1 or higher for the arguments --use_template, --usage, --footprint, --package and --dedicated_service
ynh_add_fpm_config () {
- # Declare an array to define the options of this helper.
- local legacy_args=v
- declare -Ar args_array=( [v]=phpversion= )
- local phpversion
- # Manage arguments with getopts
- ynh_handle_getopts_args "$@"
+ # Declare an array to define the options of this helper.
+ local legacy_args=vtufpd
+ local -A args_array=( [v]=phpversion= [t]=use_template [u]=usage= [f]=footprint= [p]=package= [d]=dedicated_service )
+ local phpversion
+ local use_template
+ local usage
+ local footprint
+ local package
+ local dedicated_service
+ # Manage arguments with getopts
+ ynh_handle_getopts_args "$@"
+ package=${package:-}
- # Configure PHP-FPM 7.0 by default
- phpversion="${phpversion:-7.0}"
+ # The default behaviour is to use the template.
+ use_template="${use_template:-1}"
+ usage="${usage:-}"
+ footprint="${footprint:-}"
+ if [ -n "$usage" ] || [ -n "$footprint" ]; then
+ use_template=0
+ fi
+ # Do not use a dedicated service by default
+ dedicated_service=${dedicated_service:-0}
- local fpm_config_dir="/etc/php/$phpversion/fpm"
- local fpm_service="php${phpversion}-fpm"
- # Configure PHP-FPM 5 on Debian Jessie
- if [ "$(ynh_get_debian_release)" == "jessie" ]; then
- fpm_config_dir="/etc/php5/fpm"
- fpm_service="php5-fpm"
- fi
- ynh_app_setting_set --app=$app --key=fpm_config_dir --value="$fpm_config_dir"
- ynh_app_setting_set --app=$app --key=fpm_service --value="$fpm_service"
- finalphpconf="$fpm_config_dir/pool.d/$app.conf"
- ynh_backup_if_checksum_is_different --file="$finalphpconf"
- sudo cp ../conf/php-fpm.conf "$finalphpconf"
- ynh_replace_string --match_string="__NAMETOCHANGE__" --replace_string="$app" --target_file="$finalphpconf"
- ynh_replace_string --match_string="__FINALPATH__" --replace_string="$final_path" --target_file="$finalphpconf"
- ynh_replace_string --match_string="__USER__" --replace_string="$app" --target_file="$finalphpconf"
- ynh_replace_string --match_string="__PHPVERSION__" --replace_string="$phpversion" --target_file="$finalphpconf"
- sudo chown root: "$finalphpconf"
- ynh_store_file_checksum --file="$finalphpconf"
+ # Set the default PHP-FPM version by default
+ phpversion="${phpversion:-$YNH_PHP_VERSION}"
- if [ -e "../conf/php-fpm.ini" ]
- then
- echo "Packagers ! Please do not use a separate php ini file, merge your directives in the pool file instead." >&2
- finalphpini="$fpm_config_dir/conf.d/20-$app.ini"
- ynh_backup_if_checksum_is_different "$finalphpini"
- sudo cp ../conf/php-fpm.ini "$finalphpini"
- sudo chown root: "$finalphpini"
- ynh_store_file_checksum "$finalphpini"
- fi
- ynh_systemd_action --service_name=$fpm_service --action=reload
+ # If the requested php version is not the default version for YunoHost
+ if [ "$phpversion" != "$YNH_DEFAULT_PHP_VERSION" ]
+ then
+ # If the argument --package is used, add the packages to ynh_install_php to install them from sury
+ if [ -n "$package" ]
+ then
+ local additionnal_packages="--package=$package"
+ else
+ local additionnal_packages=""
+ fi
+ # Install this specific version of php.
+ ynh_install_php --phpversion="$phpversion" "$additionnal_packages"
+ elif [ -n "$package" ]
+ then
+ # Install the additionnal packages from the default repository
+ ynh_add_app_dependencies --package="$package"
+ fi
+
+ if [ $dedicated_service -eq 1 ]
+ then
+ local fpm_service="${app}-phpfpm"
+ local fpm_config_dir="/etc/php/$phpversion/dedicated-fpm"
+ else
+ local fpm_service="php${phpversion}-fpm"
+ local fpm_config_dir="/etc/php/$phpversion/fpm"
+ fi
+ # Configure PHP-FPM 5 on Debian Jessie
+ if [ "$(ynh_get_debian_release)" == "jessie" ]
+ then
+ fpm_config_dir="/etc/php5/fpm"
+ fpm_service="php5-fpm"
+ fi
+
+ # Create the directory for fpm pools
+ mkdir --parents "$fpm_config_dir/pool.d"
+
+ ynh_app_setting_set --app=$app --key=fpm_config_dir --value="$fpm_config_dir"
+ ynh_app_setting_set --app=$app --key=fpm_service --value="$fpm_service"
+ ynh_app_setting_set --app=$app --key=fpm_dedicated_service --value="$dedicated_service"
+ ynh_app_setting_set --app=$app --key=phpversion --value=$phpversion
+ finalphpconf="$fpm_config_dir/pool.d/$app.conf"
+
+ # Migrate from mutual php service to dedicated one.
+ if [ $dedicated_service -eq 1 ]
+ then
+ local old_fpm_config_dir="/etc/php/$phpversion/fpm"
+ # If a config file exist in the common pool, move it.
+ if [ -e "$old_fpm_config_dir/pool.d/$app.conf" ]
+ then
+ ynh_print_info --message="Migrate to a dedicated php-fpm service for $app."
+ # Create a backup of the old file before migration
+ ynh_backup_if_checksum_is_different --file="$old_fpm_config_dir/pool.d/$app.conf"
+ # Remove the old php config file
+ ynh_secure_remove --file="$old_fpm_config_dir/pool.d/$app.conf"
+ # Reload php to release the socket and allow the dedicated service to use it
+ ynh_systemd_action --service_name=php${phpversion}-fpm --action=reload
+ fi
+ fi
+
+ ynh_backup_if_checksum_is_different --file="$finalphpconf"
+
+ if [ $use_template -eq 1 ]
+ then
+ # Usage 1, use the template in ../conf/php-fpm.conf
+ cp ../conf/php-fpm.conf "$finalphpconf"
+ ynh_replace_string --match_string="__NAMETOCHANGE__" --replace_string="$app" --target_file="$finalphpconf"
+ ynh_replace_string --match_string="__FINALPATH__" --replace_string="$final_path" --target_file="$finalphpconf"
+ ynh_replace_string --match_string="__USER__" --replace_string="$app" --target_file="$finalphpconf"
+ ynh_replace_string --match_string="__PHPVERSION__" --replace_string="$phpversion" --target_file="$finalphpconf"
+
+ else
+ # Usage 2, generate a php-fpm config file with ynh_get_scalable_phpfpm
+
+ # Store settings
+ ynh_app_setting_set --app=$app --key=fpm_footprint --value=$footprint
+ ynh_app_setting_set --app=$app --key=fpm_usage --value=$usage
+
+ # Define the values to use for the configuration of php.
+ ynh_get_scalable_phpfpm --usage=$usage --footprint=$footprint
+
+ # Copy the default file
+ cp "/etc/php/$phpversion/fpm/pool.d/www.conf" "$finalphpconf"
+
+ # Replace standard variables into the default file
+ ynh_replace_string --match_string="^\[www\]" --replace_string="[$app]" --target_file="$finalphpconf"
+ ynh_replace_string --match_string=".*listen = .*" --replace_string="listen = /var/run/php/php$phpversion-fpm-$app.sock" --target_file="$finalphpconf"
+ ynh_replace_string --match_string="^user = .*" --replace_string="user = $app" --target_file="$finalphpconf"
+ ynh_replace_string --match_string="^group = .*" --replace_string="group = $app" --target_file="$finalphpconf"
+ ynh_replace_string --match_string=".*chdir = .*" --replace_string="chdir = $final_path" --target_file="$finalphpconf"
+
+ # Configure fpm children
+ ynh_replace_string --match_string=".*pm = .*" --replace_string="pm = $php_pm" --target_file="$finalphpconf"
+ ynh_replace_string --match_string=".*pm.max_children = .*" --replace_string="pm.max_children = $php_max_children" --target_file="$finalphpconf"
+ ynh_replace_string --match_string=".*pm.max_requests = .*" --replace_string="pm.max_requests = 500" --target_file="$finalphpconf"
+ ynh_replace_string --match_string=".*request_terminate_timeout = .*" --replace_string="request_terminate_timeout = 1d" --target_file="$finalphpconf"
+ if [ "$php_pm" = "dynamic" ]
+ then
+ ynh_replace_string --match_string=".*pm.start_servers = .*" --replace_string="pm.start_servers = $php_start_servers" --target_file="$finalphpconf"
+ ynh_replace_string --match_string=".*pm.min_spare_servers = .*" --replace_string="pm.min_spare_servers = $php_min_spare_servers" --target_file="$finalphpconf"
+ ynh_replace_string --match_string=".*pm.max_spare_servers = .*" --replace_string="pm.max_spare_servers = $php_max_spare_servers" --target_file="$finalphpconf"
+ elif [ "$php_pm" = "ondemand" ]
+ then
+ ynh_replace_string --match_string=".*pm.process_idle_timeout = .*" --replace_string="pm.process_idle_timeout = 10s" --target_file="$finalphpconf"
+ fi
+
+ # Comment unused parameters
+ if [ "$php_pm" != "dynamic" ]
+ then
+ ynh_replace_string --match_string=".*\(pm.start_servers = .*\)" --replace_string=";\1" --target_file="$finalphpconf"
+ ynh_replace_string --match_string=".*\(pm.min_spare_servers = .*\)" --replace_string=";\1" --target_file="$finalphpconf"
+ ynh_replace_string --match_string=".*\(pm.max_spare_servers = .*\)" --replace_string=";\1" --target_file="$finalphpconf"
+ fi
+ if [ "$php_pm" != "ondemand" ]
+ then
+ ynh_replace_string --match_string=".*\(pm.process_idle_timeout = .*\)" --replace_string=";\1" --target_file="$finalphpconf"
+ fi
+
+ # Concatene the extra config.
+ if [ -e ../conf/extra_php-fpm.conf ]; then
+ cat ../conf/extra_php-fpm.conf >> "$finalphpconf"
+ fi
+ fi
+
+ chown root: "$finalphpconf"
+ ynh_store_file_checksum --file="$finalphpconf"
+
+ if [ -e "../conf/php-fpm.ini" ]
+ then
+ ynh_print_warn -message="Packagers ! Please do not use a separate php ini file, merge your directives in the pool file instead."
+ finalphpini="$fpm_config_dir/conf.d/20-$app.ini"
+ ynh_backup_if_checksum_is_different "$finalphpini"
+ cp ../conf/php-fpm.ini "$finalphpini"
+ chown root: "$finalphpini"
+ ynh_store_file_checksum "$finalphpini"
+ fi
+
+ if [ $dedicated_service -eq 1 ]
+ then
+ # Create a dedicated php-fpm.conf for the service
+ local globalphpconf=$fpm_config_dir/php-fpm-$app.conf
+ cp /etc/php/${phpversion}/fpm/php-fpm.conf $globalphpconf
+
+ ynh_replace_string --match_string="^[; ]*pid *=.*" --replace_string="pid = /run/php/php${phpversion}-fpm-$app.pid" --target_file="$globalphpconf"
+ ynh_replace_string --match_string="^[; ]*error_log *=.*" --replace_string="error_log = /var/log/php/fpm-php.$app.log" --target_file="$globalphpconf"
+ ynh_replace_string --match_string="^[; ]*syslog.ident *=.*" --replace_string="syslog.ident = php-fpm-$app" --target_file="$globalphpconf"
+ ynh_replace_string --match_string="^[; ]*include *=.*" --replace_string="include = $finalphpconf" --target_file="$globalphpconf"
+
+ # Create a config for a dedicated php-fpm service for the app
+ echo "[Unit]
+Description=PHP $phpversion FastCGI Process Manager for $app
+After=network.target
+
+[Service]
+Type=notify
+PIDFile=/run/php/php${phpversion}-fpm-$app.pid
+ExecStart=/usr/sbin/php-fpm$phpversion --nodaemonize --fpm-config $globalphpconf
+ExecReload=/bin/kill -USR2 \$MAINPID
+
+[Install]
+WantedBy=multi-user.target
+" > ../conf/$fpm_service
+
+ # Create this dedicated php-fpm service
+ ynh_add_systemd_config --service=$fpm_service --template=$fpm_service
+ # Integrate the service in YunoHost admin panel
+ yunohost service add $fpm_service --log /var/log/php/fpm-php.$app.log --log_type file --description "Php-fpm dedicated to $app"
+ # Configure log rotate
+ ynh_use_logrotate --logfile=/var/log/php
+ # Restart the service, as this service is either stopped or only for this app
+ ynh_systemd_action --service_name=$fpm_service --action=restart
+ else
+ # Reload php, to not impact other parts of the system using php
+ ynh_systemd_action --service_name=$fpm_service --action=reload
+ fi
}
# Remove the dedicated php-fpm config
@@ -54,14 +267,291 @@ ynh_add_fpm_config () {
#
# Requires YunoHost version 2.7.2 or higher.
ynh_remove_fpm_config () {
- local fpm_config_dir=$(ynh_app_setting_get --app=$app --key=fpm_config_dir)
- local fpm_service=$(ynh_app_setting_get --app=$app --key=fpm_service)
- # Assume php version 7 if not set
- if [ -z "$fpm_config_dir" ]; then
- fpm_config_dir="/etc/php/7.0/fpm"
- fpm_service="php7.0-fpm"
- fi
- ynh_secure_remove --file="$fpm_config_dir/pool.d/$app.conf"
- ynh_secure_remove --file="$fpm_config_dir/conf.d/20-$app.ini" 2>&1
- ynh_systemd_action --service_name=$fpm_service --action=reload
+ local fpm_config_dir=$(ynh_app_setting_get --app=$app --key=fpm_config_dir)
+ local fpm_service=$(ynh_app_setting_get --app=$app --key=fpm_service)
+ local dedicated_service=$(ynh_app_setting_get --app=$app --key=fpm_dedicated_service)
+ dedicated_service=${dedicated_service:-0}
+ # Get the version of php used by this app
+ local phpversion=$(ynh_app_setting_get $app phpversion)
+
+ # Assume default PHP-FPM version by default
+ phpversion="${phpversion:-$YNH_DEFAULT_PHP_VERSION}"
+
+ # Assume default php files if not set
+ if [ -z "$fpm_config_dir" ]
+ then
+ fpm_config_dir="/etc/php/$YNH_DEFAULT_PHP_VERSION/fpm"
+ fpm_service="php$YNH_DEFAULT_PHP_VERSION-fpm"
+ fi
+
+ if [ $dedicated_service -eq 1 ]
+ then
+ # Remove the dedicated service php-fpm service for the app
+ ynh_remove_systemd_config --service=$fpm_service
+ # Remove the global php-fpm conf
+ ynh_secure_remove --file="$fpm_config_dir/php-fpm-$app.conf"
+ # Remove the service from the list of services known by Yunohost
+ yunohost service remove $fpm_service
+ elif ynh_package_is_installed --package="php${phpversion}-fpm"; then
+ ynh_systemd_action --service_name=$fpm_service --action=reload
+ fi
+
+ ynh_secure_remove --file="$fpm_config_dir/pool.d/$app.conf"
+ ynh_exec_warn_less ynh_secure_remove --file="$fpm_config_dir/conf.d/20-$app.ini"
+
+ # If the php version used is not the default version for YunoHost
+ if [ "$phpversion" != "$YNH_DEFAULT_PHP_VERSION" ]
+ then
+ # Remove this specific version of php
+ ynh_remove_php
+ fi
+}
+
+# Install another version of php.
+#
+# [internal]
+#
+# usage: ynh_install_php --phpversion=phpversion [--package=packages]
+# | arg: -v, --phpversion= - Version of php to install.
+# | arg: -p, --package= - Additionnal php packages to install
+#
+# Requires YunoHost version 3.8.1 or higher.
+ynh_install_php () {
+ # Declare an array to define the options of this helper.
+ local legacy_args=vp
+ local -A args_array=( [v]=phpversion= [p]=package= )
+ local phpversion
+ local package
+ # Manage arguments with getopts
+ ynh_handle_getopts_args "$@"
+ package=${package:-}
+
+ # Store phpversion into the config of this app
+ ynh_app_setting_set $app phpversion $phpversion
+
+ if [ "$phpversion" == "$YNH_DEFAULT_PHP_VERSION" ]
+ then
+ ynh_die "Do not use ynh_install_php to install php$YNH_DEFAULT_PHP_VERSION"
+ fi
+
+ # Create the file if doesn't exist already
+ touch /etc/php/ynh_app_version
+
+ # Do not add twice the same line
+ if ! grep --quiet "$YNH_APP_INSTANCE_NAME:" "/etc/php/ynh_app_version"
+ then
+ # Store the ID of this app and the version of php requested for it
+ echo "$YNH_APP_INSTANCE_NAME:$phpversion" | tee --append "/etc/php/ynh_app_version"
+ fi
+
+ # Add an extra repository for those packages
+ ynh_install_extra_repo --repo="https://packages.sury.org/php/ $(ynh_get_debian_release) main" --key="https://packages.sury.org/php/apt.gpg" --priority=995 --name=extra_php_version
+
+ # Install requested dependencies from this extra repository.
+ # Install php-fpm first, otherwise php will install apache as a dependency.
+ ynh_add_app_dependencies --package="php${phpversion}-fpm"
+ ynh_add_app_dependencies --package="php$phpversion php${phpversion}-common $package"
+
+ # Set the default php version back as the default version for php-cli.
+ update-alternatives --set php /usr/bin/php$YNH_DEFAULT_PHP_VERSION
+
+ # Pin this extra repository after packages are installed to prevent sury of doing shit
+ ynh_pin_repo --package="*" --pin="origin \"packages.sury.org\"" --priority=200 --name=extra_php_version
+ ynh_pin_repo --package="php${YNH_DEFAULT_PHP_VERSION}*" --pin="origin \"packages.sury.org\"" --priority=600 --name=extra_php_version --append
+
+ # Advertise service in admin panel
+ yunohost service add php${phpversion}-fpm --log "/var/log/php${phpversion}-fpm.log"
+}
+
+# Remove the specific version of php used by the app.
+#
+# [internal]
+#
+# usage: ynh_install_php
+#
+# Requires YunoHost version 3.8.1 or higher.
+ynh_remove_php () {
+ # Get the version of php used by this app
+ local phpversion=$(ynh_app_setting_get $app phpversion)
+
+ if [ "$phpversion" == "$YNH_DEFAULT_PHP_VERSION" ] || [ -z "$phpversion" ]
+ then
+ if [ "$phpversion" == "$YNH_DEFAULT_PHP_VERSION" ]
+ then
+ ynh_print_err "Do not use ynh_remove_php to remove php$YNH_DEFAULT_PHP_VERSION !"
+ fi
+ return 0
+ fi
+
+ # Create the file if doesn't exist already
+ touch /etc/php/ynh_app_version
+
+ # Remove the line for this app
+ sed --in-place "/$YNH_APP_INSTANCE_NAME:$phpversion/d" "/etc/php/ynh_app_version"
+
+ # If no other app uses this version of php, remove it.
+ if ! grep --quiet "$phpversion" "/etc/php/ynh_app_version"
+ then
+ # Remove the service from the admin panel
+ if ynh_package_is_installed --package="php${phpversion}-fpm"; then
+ yunohost service remove php${phpversion}-fpm
+ fi
+
+ # Purge php dependencies for this version.
+ ynh_package_autopurge "php$phpversion php${phpversion}-fpm php${phpversion}-common"
+ fi
+}
+
+# Define the values to configure php-fpm
+#
+# [internal]
+#
+# usage: ynh_get_scalable_phpfpm --usage=usage --footprint=footprint [--print]
+# | arg: -f, --footprint= - Memory footprint of the service (low/medium/high).
+# low - Less than 20Mb of ram by pool.
+# medium - Between 20Mb and 40Mb of ram by pool.
+# high - More than 40Mb of ram by pool.
+# Or specify exactly the footprint, the load of the service as Mb by pool instead of having a standard value.
+# To have this value, use the following command and stress the service.
+# watch -n0.5 ps -o user,cmd,%cpu,rss -u APP
+#
+# | arg: -u, --usage= - Expected usage of the service (low/medium/high).
+# low - Personal usage, behind the sso.
+# medium - Low usage, few people or/and publicly accessible.
+# high - High usage, frequently visited website.
+#
+# | arg: -p, --print - Print the result (intended for debug purpose only when packaging the app)
+ynh_get_scalable_phpfpm () {
+ local legacy_args=ufp
+ # Declare an array to define the options of this helper.
+ local -A args_array=( [u]=usage= [f]=footprint= [p]=print )
+ local usage
+ local footprint
+ local print
+ # Manage arguments with getopts
+ ynh_handle_getopts_args "$@"
+ # Set all characters as lowercase
+ footprint=${footprint,,}
+ usage=${usage,,}
+ print=${print:-0}
+
+ if [ "$footprint" = "low" ]
+ then
+ footprint=20
+ elif [ "$footprint" = "medium" ]
+ then
+ footprint=35
+ elif [ "$footprint" = "high" ]
+ then
+ footprint=50
+ fi
+
+ # Define the factor to determine min_spare_servers
+ # to avoid having too few children ready to start for heavy apps
+ if [ $footprint -le 20 ]
+ then
+ min_spare_servers_factor=8
+ elif [ $footprint -le 35 ]
+ then
+ min_spare_servers_factor=5
+ else
+ min_spare_servers_factor=3
+ fi
+
+ # Define the way the process manager handle child processes.
+ if [ "$usage" = "low" ]
+ then
+ php_pm=ondemand
+ elif [ "$usage" = "medium" ]
+ then
+ php_pm=dynamic
+ elif [ "$usage" = "high" ]
+ then
+ php_pm=static
+ else
+ ynh_die --message="Does not recognize '$usage' as an usage value."
+ fi
+
+ # Get the total of RAM available, except swap.
+ local max_ram=$(ynh_get_ram --total --ignore_swap)
+
+ at_least_one() {
+ # Do not allow value below 1
+ if [ $1 -le 0 ]
+ then
+ echo 1
+ else
+ echo $1
+ fi
+ }
+
+ # Define pm.max_children
+ # The value of pm.max_children is the total amount of ram divide by 2 and divide again by the footprint of a pool for this app.
+ # So if php-fpm start the maximum of children, it won't exceed half of the ram.
+ php_max_children=$(( $max_ram / 2 / $footprint ))
+ # If process manager is set as static, use half less children.
+ # Used as static, there's always as many children as the value of pm.max_children
+ if [ "$php_pm" = "static" ]
+ then
+ php_max_children=$(( $php_max_children / 2 ))
+ fi
+ php_max_children=$(at_least_one $php_max_children)
+
+ # To not overload the proc, limit the number of children to 4 times the number of cores.
+ local core_number=$(nproc)
+ local max_proc=$(( $core_number * 4 ))
+ if [ $php_max_children -gt $max_proc ]
+ then
+ php_max_children=$max_proc
+ fi
+
+ # Get a potential forced value for php_max_children
+ local php_forced_max_children=$(ynh_app_setting_get --app=$app --key=php_forced_max_children)
+ if [ -n "$php_forced_max_children" ]; then
+ php_max_children=$php_forced_max_children
+ fi
+
+ if [ "$php_pm" = "dynamic" ]
+ then
+ # Define pm.start_servers, pm.min_spare_servers and pm.max_spare_servers for a dynamic process manager
+ php_min_spare_servers=$(( $php_max_children / $min_spare_servers_factor ))
+ php_min_spare_servers=$(at_least_one $php_min_spare_servers)
+
+ php_max_spare_servers=$(( $php_max_children / 2 ))
+ php_max_spare_servers=$(at_least_one $php_max_spare_servers)
+
+ php_start_servers=$(( $php_min_spare_servers + ( $php_max_spare_servers - $php_min_spare_servers ) /2 ))
+ php_start_servers=$(at_least_one $php_start_servers)
+ else
+ php_min_spare_servers=0
+ php_max_spare_servers=0
+ php_start_servers=0
+ fi
+
+ if [ $print -eq 1 ]
+ then
+ ynh_debug --message="Footprint=${footprint}Mb by pool."
+ ynh_debug --message="Process manager=$php_pm"
+ ynh_debug --message="Max RAM=${max_ram}Mb"
+ if [ "$php_pm" != "static" ]
+ then
+ ynh_debug --message="\nMax estimated footprint=$(( $php_max_children * $footprint ))"
+ ynh_debug --message="Min estimated footprint=$(( $php_min_spare_servers * $footprint ))"
+ fi
+ if [ "$php_pm" = "dynamic" ]
+ then
+ ynh_debug --message="Estimated average footprint=$(( $php_max_spare_servers * $footprint ))"
+ elif [ "$php_pm" = "static" ]
+ then
+ ynh_debug --message="Estimated footprint=$(( $php_max_children * $footprint ))"
+ fi
+ ynh_debug --message="\nRaw php-fpm values:"
+ ynh_debug --message="pm.max_children = $php_max_children"
+ if [ "$php_pm" = "dynamic" ]
+ then
+ ynh_debug --message="pm.start_servers = $php_start_servers"
+ ynh_debug --message="pm.min_spare_servers = $php_min_spare_servers"
+ ynh_debug --message="pm.max_spare_servers = $php_max_spare_servers"
+ fi
+ fi
}
diff --git a/data/helpers.d/postgresql b/data/helpers.d/postgresql
index 954f44d0b..e2bef8746 100644
--- a/data/helpers.d/postgresql
+++ b/data/helpers.d/postgresql
@@ -9,65 +9,65 @@ PSQL_ROOT_PWD_FILE=/etc/yunohost/psql
# ynh_psql_connect_as 'user' 'pass' < /path/to/file.sql
#
# usage: ynh_psql_connect_as --user=user --password=password [--database=database]
-# | arg: -u, --user - the user name to connect as
-# | arg: -p, --password - the user password
-# | arg: -d, --database - the database to connect to
+# | arg: -u, --user= - the user name to connect as
+# | arg: -p, --password= - the user password
+# | arg: -d, --database= - the database to connect to
#
# Requires YunoHost version 3.5.0 or higher.
ynh_psql_connect_as() {
- # Declare an array to define the options of this helper.
- local legacy_args=upd
- declare -Ar args_array=([u]=user= [p]=password= [d]=database=)
- local user
- local password
- local database
- # Manage arguments with getopts
- ynh_handle_getopts_args "$@"
- database="${database:-}"
+ # Declare an array to define the options of this helper.
+ local legacy_args=upd
+ local -A args_array=([u]=user= [p]=password= [d]=database=)
+ local user
+ local password
+ local database
+ # Manage arguments with getopts
+ ynh_handle_getopts_args "$@"
+ database="${database:-}"
- sudo --login --user=postgres PGUSER="$user" PGPASSWORD="$password" psql "$database"
+ sudo --login --user=postgres PGUSER="$user" PGPASSWORD="$password" psql "$database"
}
# Execute a command as root user
#
# usage: ynh_psql_execute_as_root --sql=sql [--database=database]
-# | arg: -s, --sql - the SQL command to execute
-# | arg: -d, --database - the database to connect to
+# | arg: -s, --sql= - the SQL command to execute
+# | arg: -d, --database= - the database to connect to
#
# Requires YunoHost version 3.5.0 or higher.
ynh_psql_execute_as_root() {
- # Declare an array to define the options of this helper.
- local legacy_args=sd
- declare -Ar args_array=([s]=sql= [d]=database=)
- local sql
- local database
- # Manage arguments with getopts
- ynh_handle_getopts_args "$@"
- database="${database:-}"
+ # Declare an array to define the options of this helper.
+ local legacy_args=sd
+ local -A args_array=([s]=sql= [d]=database=)
+ local sql
+ local database
+ # Manage arguments with getopts
+ ynh_handle_getopts_args "$@"
+ database="${database:-}"
- ynh_psql_connect_as --user="postgres" --password="$(sudo cat $PSQL_ROOT_PWD_FILE)" \
- --database="$database" <<<"$sql"
+ ynh_psql_connect_as --user="postgres" --password="$(cat $PSQL_ROOT_PWD_FILE)" \
+ --database="$database" <<<"$sql"
}
# Execute a command from a file as root user
#
# usage: ynh_psql_execute_file_as_root --file=file [--database=database]
-# | arg: -f, --file - the file containing SQL commands
-# | arg: -d, --database - the database to connect to
+# | arg: -f, --file= - the file containing SQL commands
+# | arg: -d, --database= - the database to connect to
#
# Requires YunoHost version 3.5.0 or higher.
ynh_psql_execute_file_as_root() {
- # Declare an array to define the options of this helper.
- local legacy_args=fd
- declare -Ar args_array=([f]=file= [d]=database=)
- local file
- local database
- # Manage arguments with getopts
- ynh_handle_getopts_args "$@"
- database="${database:-}"
+ # Declare an array to define the options of this helper.
+ local legacy_args=fd
+ local -A args_array=([f]=file= [d]=database=)
+ local file
+ local database
+ # Manage arguments with getopts
+ ynh_handle_getopts_args "$@"
+ database="${database:-}"
- ynh_psql_connect_as --user="postgres" --password="$(sudo cat $PSQL_ROOT_PWD_FILE)" \
- --database="$database" <"$file"
+ ynh_psql_connect_as --user="postgres" --password="$(cat $PSQL_ROOT_PWD_FILE)" \
+ --database="$database" <"$file"
}
# Create a database and grant optionnaly privilegies to a user
@@ -80,17 +80,18 @@ ynh_psql_execute_file_as_root() {
#
# Requires YunoHost version 3.5.0 or higher.
ynh_psql_create_db() {
- local db=$1
- local user=${2:-}
+ local db=$1
+ local user=${2:-}
- local sql="CREATE DATABASE ${db};"
+ local sql="CREATE DATABASE ${db};"
- # grant all privilegies to user
- if [ -n "$user" ]; then
- sql+="GRANT ALL PRIVILEGES ON DATABASE ${db} TO ${user} WITH GRANT OPTION;"
- fi
+ # grant all privilegies to user
+ if [ -n "$user" ]; then
+ sql+="ALTER DATABASE ${db} OWNER TO ${user};"
+ sql+="GRANT ALL PRIVILEGES ON DATABASE ${db} TO ${user} WITH GRANT OPTION;"
+ fi
- ynh_psql_execute_as_root --sql="$sql"
+ ynh_psql_execute_as_root --sql="$sql"
}
# Drop a database
@@ -105,12 +106,12 @@ ynh_psql_create_db() {
#
# Requires YunoHost version 3.5.0 or higher.
ynh_psql_drop_db() {
- local db=$1
- # First, force disconnection of all clients connected to the database
- # https://stackoverflow.com/questions/5408156/how-to-drop-a-postgresql-database-if-there-are-active-connections-to-it
- # https://dba.stackexchange.com/questions/16426/how-to-drop-all-connections-to-a-specific-database-without-stopping-the-server
- ynh_psql_execute_as_root --sql="SELECT pg_terminate_backend (pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = '$db';" --database="$db"
- sudo --login --user=postgres dropdb $db
+ local db=$1
+ # First, force disconnection of all clients connected to the database
+ # https://stackoverflow.com/questions/17449420/postgresql-unable-to-drop-database-because-of-some-auto-connections-to-db
+ ynh_psql_execute_as_root --sql="REVOKE CONNECT ON DATABASE $db FROM public;" --database="$db"
+ ynh_psql_execute_as_root --sql="SELECT pg_terminate_backend (pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = '$db' AND pid <> pg_backend_pid();" --database="$db"
+ sudo --login --user=postgres dropdb $db
}
# Dump a database
@@ -118,19 +119,19 @@ ynh_psql_drop_db() {
# example: ynh_psql_dump_db 'roundcube' > ./dump.sql
#
# usage: ynh_psql_dump_db --database=database
-# | arg: -d, --database - the database name to dump
+# | arg: -d, --database= - the database name to dump
# | ret: the psqldump output
#
# Requires YunoHost version 3.5.0 or higher.
ynh_psql_dump_db() {
- # Declare an array to define the options of this helper.
- local legacy_args=d
- declare -Ar args_array=([d]=database=)
- local database
- # Manage arguments with getopts
- ynh_handle_getopts_args "$@"
+ # Declare an array to define the options of this helper.
+ local legacy_args=d
+ local -A args_array=([d]=database=)
+ local database
+ # Manage arguments with getopts
+ ynh_handle_getopts_args "$@"
- sudo --login --user=postgres pg_dump "$database"
+ sudo --login --user=postgres pg_dump "$database"
}
# Create a user
@@ -143,47 +144,55 @@ ynh_psql_dump_db() {
#
# Requires YunoHost version 3.5.0 or higher.
ynh_psql_create_user() {
- local user=$1
- local pwd=$2
- ynh_psql_execute_as_root --sql="CREATE USER $user WITH ENCRYPTED PASSWORD '$pwd'"
+ local user=$1
+ local pwd=$2
+ ynh_psql_execute_as_root --sql="CREATE USER $user WITH ENCRYPTED PASSWORD '$pwd'"
}
# Check if a psql user exists
#
# usage: ynh_psql_user_exists --user=user
-# | arg: -u, --user - the user for which to check existence
+# | arg: -u, --user= - the user for which to check existence
+# | exit: Return 1 if the user doesn't exist, 0 otherwise
+#
+# Requires YunoHost version 3.5.0 or higher.
ynh_psql_user_exists() {
- # Declare an array to define the options of this helper.
- local legacy_args=u
- declare -Ar args_array=([u]=user=)
- local user
- # Manage arguments with getopts
- ynh_handle_getopts_args "$@"
+ # Declare an array to define the options of this helper.
+ local legacy_args=u
+ local -A args_array=([u]=user=)
+ local user
+ # Manage arguments with getopts
+ ynh_handle_getopts_args "$@"
- if ! sudo --login --user=postgres PGUSER="postgres" PGPASSWORD="$(sudo cat $PSQL_ROOT_PWD_FILE)" psql -tAc "SELECT rolname FROM pg_roles WHERE rolname='$user';" | grep --quiet "$user" ; then
- return 1
- else
- return 0
- fi
+ if ! sudo --login --user=postgres PGUSER="postgres" PGPASSWORD="$(cat $PSQL_ROOT_PWD_FILE)" psql -tAc "SELECT rolname FROM pg_roles WHERE rolname='$user';" | grep --quiet "$user"
+ then
+ return 1
+ else
+ return 0
+ fi
}
# Check if a psql database exists
#
# usage: ynh_psql_database_exists --database=database
-# | arg: -d, --database - the database for which to check existence
+# | arg: -d, --database= - the database for which to check existence
+# | exit: Return 1 if the database doesn't exist, 0 otherwise
+#
+# Requires YunoHost version 3.5.0 or higher.
ynh_psql_database_exists() {
- # Declare an array to define the options of this helper.
- local legacy_args=d
- declare -Ar args_array=([d]=database=)
- local database
- # Manage arguments with getopts
- ynh_handle_getopts_args "$@"
+ # Declare an array to define the options of this helper.
+ local legacy_args=d
+ local -A args_array=([d]=database=)
+ local database
+ # Manage arguments with getopts
+ ynh_handle_getopts_args "$@"
- if ! sudo --login --user=postgres PGUSER="postgres" PGPASSWORD="$(sudo cat $PSQL_ROOT_PWD_FILE)" psql -tAc "SELECT datname FROM pg_database WHERE datname='$database';" | grep --quiet "$database"; then
- return 1
- else
- return 0
- fi
+ if ! sudo --login --user=postgres PGUSER="postgres" PGPASSWORD="$(cat $PSQL_ROOT_PWD_FILE)" psql -tAc "SELECT datname FROM pg_database WHERE datname='$database';" | grep --quiet "$database"
+ then
+ return 1
+ else
+ return 0
+ fi
}
# Drop a user
@@ -195,110 +204,121 @@ ynh_psql_database_exists() {
#
# Requires YunoHost version 3.5.0 or higher.
ynh_psql_drop_user() {
- ynh_psql_execute_as_root --sql="DROP USER ${1};"
+ ynh_psql_execute_as_root --sql="DROP USER ${1};"
}
# Create a database, an user and its password. Then store the password in the app's config
#
+# usage: ynh_psql_setup_db --db_user=user --db_name=name [--db_pwd=pwd]
+# | arg: -u, --db_user= - Owner of the database
+# | arg: -n, --db_name= - Name of the database
+# | arg: -p, --db_pwd= - Password of the database. If not given, a password will be generated
+#
# After executing this helper, the password of the created database will be available in $db_pwd
# It will also be stored as "psqlpwd" into the app settings.
#
-# usage: ynh_psql_setup_db --db_user=user --db_name=name [--db_pwd=pwd]
-# | arg: -u, --db_user - Owner of the database
-# | arg: -n, --db_name - Name of the database
-# | arg: -p, --db_pwd - Password of the database. If not given, a password will be generated
+# Requires YunoHost version 2.7.13 or higher.
ynh_psql_setup_db() {
- # Declare an array to define the options of this helper.
- local legacy_args=unp
- declare -Ar args_array=([u]=db_user= [n]=db_name= [p]=db_pwd=)
- local db_user
- local db_name
- db_pwd=""
- # Manage arguments with getopts
- ynh_handle_getopts_args "$@"
+ # Declare an array to define the options of this helper.
+ local legacy_args=unp
+ local -A args_array=([u]=db_user= [n]=db_name= [p]=db_pwd=)
+ local db_user
+ local db_name
+ db_pwd=""
+ # Manage arguments with getopts
+ ynh_handle_getopts_args "$@"
- local new_db_pwd=$(ynh_string_random) # Generate a random password
- # If $db_pwd is not given, use new_db_pwd instead for db_pwd
- db_pwd="${db_pwd:-$new_db_pwd}"
+ local new_db_pwd=$(ynh_string_random) # Generate a random password
+ # If $db_pwd is not given, use new_db_pwd instead for db_pwd
+ db_pwd="${db_pwd:-$new_db_pwd}"
- if ! ynh_psql_user_exists --user=$db_user; then
- ynh_psql_create_user "$db_user" "$db_pwd"
- fi
+ if ! ynh_psql_user_exists --user=$db_user; then
+ ynh_psql_create_user "$db_user" "$db_pwd"
+ fi
- ynh_psql_create_db "$db_name" "$db_user" # Create the database
- ynh_app_setting_set --app=$app --key=psqlpwd --value=$db_pwd # Store the password in the app's config
+ ynh_psql_create_db "$db_name" "$db_user" # Create the database
+ ynh_app_setting_set --app=$app --key=psqlpwd --value=$db_pwd # Store the password in the app's config
}
# Remove a database if it exists, and the associated user
#
# usage: ynh_psql_remove_db --db_user=user --db_name=name
-# | arg: -u, --db_user - Owner of the database
-# | arg: -n, --db_name - Name of the database
+# | arg: -u, --db_user= - Owner of the database
+# | arg: -n, --db_name= - Name of the database
+#
+# Requires YunoHost version 2.7.13 or higher.
ynh_psql_remove_db() {
- # Declare an array to define the options of this helper.
- local legacy_args=un
- declare -Ar args_array=([u]=db_user= [n]=db_name=)
- local db_user
- local db_name
- # Manage arguments with getopts
- ynh_handle_getopts_args "$@"
+ # Declare an array to define the options of this helper.
+ local legacy_args=un
+ local -A args_array=([u]=db_user= [n]=db_name=)
+ local db_user
+ local db_name
+ # Manage arguments with getopts
+ ynh_handle_getopts_args "$@"
- local psql_root_password=$(sudo cat $PSQL_ROOT_PWD_FILE)
- if ynh_psql_database_exists --database=$db_name; then # Check if the database exists
- ynh_psql_drop_db $db_name # Remove the database
- else
- ynh_print_warn --message="Database $db_name not found"
- fi
+ if ynh_psql_database_exists --database=$db_name
+ then # Check if the database exists
+ ynh_psql_drop_db $db_name # Remove the database
+ else
+ ynh_print_warn --message="Database $db_name not found"
+ fi
- # Remove psql user if it exists
- if ynh_psql_user_exists --user=$db_user; then
- ynh_psql_drop_user $db_user
- else
- ynh_print_warn --message="User $db_user not found"
- fi
+ # Remove psql user if it exists
+ if ynh_psql_user_exists --user=$db_user
+ then
+ ynh_psql_drop_user $db_user
+ else
+ ynh_print_warn --message="User $db_user not found"
+ fi
}
# Create a master password and set up global settings
# Please always call this script in install and restore scripts
#
# usage: ynh_psql_test_if_first_run
+#
+# Requires YunoHost version 2.7.13 or higher.
ynh_psql_test_if_first_run() {
- if [ -f "$PSQL_ROOT_PWD_FILE" ]; then
- echo "PostgreSQL is already installed, no need to create master password"
- else
- local psql_root_password="$(ynh_string_random)"
- echo "$psql_root_password" >$PSQL_ROOT_PWD_FILE
- if [ -e /etc/postgresql/9.4/ ]; then
- local pg_hba=/etc/postgresql/9.4/main/pg_hba.conf
- local logfile=/var/log/postgresql/postgresql-9.4-main.log
- elif [ -e /etc/postgresql/9.6/ ]; then
- local pg_hba=/etc/postgresql/9.6/main/pg_hba.conf
- local logfile=/var/log/postgresql/postgresql-9.6-main.log
- else
- if dpkg --list | grep -q "ii postgresql-9."
- then
- ynh_die "It looks like postgresql was not properly configured ? /etc/postgresql/9.* is missing ... Could be due to a locale issue, c.f.https://serverfault.com/questions/426989/postgresql-etc-postgresql-doesnt-exist"
- else
- ynh_die "postgresql shoud be 9.4 or 9.6 or "
- fi
-
- fi
+ if [ -f "$PSQL_ROOT_PWD_FILE" ]
+ then
+ ynh_print_info --message="PostgreSQL is already installed, no need to create master password"
+ return
+ fi
- ynh_systemd_action --service_name=postgresql --action=start
+ local psql_root_password="$(ynh_string_random)"
+ echo "$psql_root_password" >$PSQL_ROOT_PWD_FILE
- sudo --login --user=postgres psql -c"ALTER user postgres WITH PASSWORD '$psql_root_password'" postgres
+ if [ -e /etc/postgresql/9.4/ ]
+ then
+ local pg_hba=/etc/postgresql/9.4/main/pg_hba.conf
+ local logfile=/var/log/postgresql/postgresql-9.4-main.log
+ elif [ -e /etc/postgresql/9.6/ ]
+ then
+ local pg_hba=/etc/postgresql/9.6/main/pg_hba.conf
+ local logfile=/var/log/postgresql/postgresql-9.6-main.log
+ else
+ if dpkg --list | grep -q "ii postgresql-9."
+ then
+ ynh_die "It looks like postgresql was not properly configured ? /etc/postgresql/9.* is missing ... Could be due to a locale issue, c.f.https://serverfault.com/questions/426989/postgresql-etc-postgresql-doesnt-exist"
+ else
+ ynh_die "postgresql shoud be 9.4 or 9.6"
+ fi
+ fi
- # force all user to connect to local databases using hashed passwords
- # https://www.postgresql.org/docs/current/static/auth-pg-hba-conf.html#EXAMPLE-PG-HBA.CONF
- # Note: we can't use peer since YunoHost create users with nologin
- # See: https://github.com/YunoHost/yunohost/blob/unstable/data/helpers.d/user
- ynh_replace_string --match_string="local\(\s*\)all\(\s*\)all\(\s*\)peer" --replace_string="local\1all\2all\3md5" --target_file="$pg_hba"
+ ynh_systemd_action --service_name=postgresql --action=start
- # Advertise service in admin panel
- yunohost service add postgresql --log "$logfile"
+ sudo --login --user=postgres psql -c"ALTER user postgres WITH PASSWORD '$psql_root_password'" postgres
- systemctl enable postgresql
- ynh_systemd_action --service_name=postgresql --action=reload
- fi
+ # force all user to connect to local databases using hashed passwords
+ # https://www.postgresql.org/docs/current/static/auth-pg-hba-conf.html#EXAMPLE-PG-HBA.CONF
+ # Note: we can't use peer since YunoHost create users with nologin
+ # See: https://github.com/YunoHost/yunohost/blob/unstable/data/helpers.d/user
+ ynh_replace_string --match_string="local\(\s*\)all\(\s*\)all\(\s*\)peer" --replace_string="local\1all\2all\3md5" --target_file="$pg_hba"
+
+ # Advertise service in admin panel
+ yunohost service add postgresql --log "$logfile"
+
+ systemctl enable postgresql
+ ynh_systemd_action --service_name=postgresql --action=reload
}
diff --git a/data/helpers.d/setting b/data/helpers.d/setting
index da711b4bd..61397151b 100644
--- a/data/helpers.d/setting
+++ b/data/helpers.d/setting
@@ -3,14 +3,14 @@
# Get an application setting
#
# usage: ynh_app_setting_get --app=app --key=key
-# | arg: -a, --app - the application id
-# | arg: -k, --key - the setting to get
+# | arg: -a, --app= - the application id
+# | arg: -k, --key= - the setting to get
#
# Requires YunoHost version 2.2.4 or higher.
ynh_app_setting_get() {
# Declare an array to define the options of this helper.
local legacy_args=ak
- declare -Ar args_array=( [a]=app= [k]=key= )
+ local -A args_array=( [a]=app= [k]=key= )
local app
local key
# Manage arguments with getopts
@@ -22,15 +22,15 @@ ynh_app_setting_get() {
# Set an application setting
#
# usage: ynh_app_setting_set --app=app --key=key --value=value
-# | arg: -a, --app - the application id
-# | arg: -k, --key - the setting name to set
-# | arg: -v, --value - the setting value to set
+# | arg: -a, --app= - the application id
+# | arg: -k, --key= - the setting name to set
+# | arg: -v, --value= - the setting value to set
#
# Requires YunoHost version 2.2.4 or higher.
ynh_app_setting_set() {
# Declare an array to define the options of this helper.
local legacy_args=akv
- declare -Ar args_array=( [a]=app= [k]=key= [v]=value= )
+ local -A args_array=( [a]=app= [k]=key= [v]=value= )
local app
local key
local value
@@ -43,14 +43,14 @@ ynh_app_setting_set() {
# Delete an application setting
#
# usage: ynh_app_setting_delete --app=app --key=key
-# | arg: -a, --app - the application id
-# | arg: -k, --key - the setting to delete
+# | arg: -a, --app= - the application id
+# | arg: -k, --key= - the setting to delete
#
# Requires YunoHost version 2.2.4 or higher.
ynh_app_setting_delete() {
# Declare an array to define the options of this helper.
local legacy_args=ak
- declare -Ar args_array=( [a]=app= [k]=key= )
+ local -A args_array=( [a]=app= [k]=key= )
local app
local key
# Manage arguments with getopts
@@ -59,98 +59,6 @@ ynh_app_setting_delete() {
ynh_app_setting "delete" "$app" "$key"
}
-# Add skipped_uris urls into the config
-#
-# usage: ynh_add_skipped_uris [--appid=app] --url=url1,url2 [--regex]
-# | arg: -a, --appid - the application id
-# | arg: -u, --url - the urls to add to the sso for this app
-# | arg: -r, --regex - Use the key 'skipped_regex' instead of 'skipped_uris'
-#
-# An URL set with 'skipped_uris' key will be totally ignored by the SSO,
-# which means that the access will be public and the logged-in user information will not be passed to the app.
-#
-# Requires YunoHost version 3.6.0 or higher.
-ynh_add_skipped_uris() {
- # Declare an array to define the options of this helper.
- local legacy_args=aur
- declare -Ar args_array=( [a]=appid= [u]=url= [r]=regex )
- local appid
- local url
- local regex
- # Manage arguments with getopts
- ynh_handle_getopts_args "$@"
- appid={appid:-$app}
- regex={regex:-0}
-
- local key=skipped_uris
- if [ $regex -eq 1 ]; then
- key=skipped_regex
- fi
-
- ynh_app_setting_set --app=$appid --key=$key --value="$url"
-}
-
-# Add unprotected_uris urls into the config
-#
-# usage: ynh_add_unprotected_uris [--appid=app] --url=url1,url2 [--regex]
-# | arg: -a, --appid - the application id
-# | arg: -u, --url - the urls to add to the sso for this app
-# | arg: -r, --regex - Use the key 'unprotected_regex' instead of 'unprotected_uris'
-#
-# An URL set with unprotected_uris key will be accessible publicly, but if an user is logged in,
-# his information will be accessible (through HTTP headers) to the app.
-#
-# Requires YunoHost version 3.6.0 or higher.
-ynh_add_unprotected_uris() {
- # Declare an array to define the options of this helper.
- local legacy_args=aur
- declare -Ar args_array=( [a]=appid= [u]=url= [r]=regex )
- local appid
- local url
- local regex
- # Manage arguments with getopts
- ynh_handle_getopts_args "$@"
- appid={appid:-$app}
- regex={regex:-0}
-
- local key=unprotected_uris
- if [ $regex -eq 1 ]; then
- key=unprotected_regex
- fi
-
- ynh_app_setting_set --app=$appid --key=$key --value="$url"
-}
-
-# Add protected_uris urls into the config
-#
-# usage: ynh_add_protected_uris [--appid=app] --url=url1,url2 [--regex]
-# | arg: -a, --appid - the application id
-# | arg: -u, --url - the urls to add to the sso for this app
-# | arg: -r, --regex - Use the key 'protected_regex' instead of 'protected_uris'
-#
-# An URL set with protected_uris will be blocked by the SSO and accessible only to authenticated and authorized users.
-#
-# Requires YunoHost version 3.6.0 or higher.
-ynh_add_protected_uris() {
- # Declare an array to define the options of this helper.
- local legacy_args=aur
- declare -Ar args_array=( [a]=appid= [u]=url= [r]=regex )
- local appid
- local url
- local regex
- # Manage arguments with getopts
- ynh_handle_getopts_args "$@"
- appid={appid:-$app}
- regex={regex:-0}
-
- local key=protected_uris
- if [ $regex -eq 1 ]; then
- key=protected_regex
- fi
-
- ynh_app_setting_set --app=$appid --key=$key --value="$url"
-}
-
# Small "hard-coded" interface to avoid calling "yunohost app" directly each
# time dealing with a setting is needed (which may be so slow on ARM boards)
#
@@ -158,8 +66,13 @@ ynh_add_protected_uris() {
#
ynh_app_setting()
{
- ACTION="$1" APP="$2" KEY="$3" VALUE="${4:-}" python - < domain.tld/app
+# /admin -> domain.tld/app/admin
+# domain.tld/app/api -> domain.tld/app/api
+#
+# 'url' can be later treated as a regex if it starts with "re:".
+# For example:
+# re:/api/[A-Z]*$ -> domain.tld/app/api/[A-Z]*$
+# re:domain.tld/app/api/[A-Z]*$ -> domain.tld/app/api/[A-Z]*$
+#
+# Requires YunoHost version 3.7.0 or higher.
ynh_permission_create() {
- declare -Ar args_array=( [a]=app= [p]=permission= [d]=defaultdisallow [u]=urls= )
- local app
+ # Declare an array to define the options of this helper.
+ local legacy_args=pua
+ local -A args_array=( [p]=permission= [u]=url= [a]=allowed= )
local permission
- local defaultdisallow
- local urls
+ local url
+ local allowed
ynh_handle_getopts_args "$@"
- if [[ -n ${defaultdisallow:-} ]]; then
- defaultdisallow=",default_allow=False"
+ url=${url:-}
+ allowed=${allowed:-}
+
+ if [[ -n $url ]]
+ then
+ url="'$url'"
+ else
+ url="None"
fi
- if [[ -n ${urls:-} ]]; then
- urls=",urls=['${urls//';'/"','"}']"
+ if [[ -n $allowed ]]; then
+ allowed=",allowed=['${allowed//';'/"','"}']"
fi
- yunohost tools shell -c "from yunohost.permission import permission_add; permission_add('$app', '$permission' ${defaultdisallow:-} ${urls:-}, sync_perm=False)"
+
+ yunohost tools shell -c "from yunohost.permission import permission_create; permission_create('$app.$permission', url=$url $allowed , sync_perm=False)"
}
# Remove a permission for the app (note that when the app is removed all permission is automatically removed)
#
-# usage: ynh_permission_remove --app "app" --permission "permission"
-# | arg: app - the application id
-# | arg: permission - the name for the permission (by default a permission named "main" is removed automatically when the app is removed)
-ynh_permission_remove() {
- declare -Ar args_array=( [a]=app= [p]=permission= )
- local app
+# example: ynh_permission_delete --permission=editors
+#
+# usage: ynh_permission_delete --permission="permission"
+# | arg: -p, --permission= - the name for the permission (by default a permission named "main" is removed automatically when the app is removed)
+#
+# Requires YunoHost version 3.7.0 or higher.
+ynh_permission_delete() {
+ # Declare an array to define the options of this helper.
+ local legacy_args=p
+ local -A args_array=( [p]=permission= )
local permission
ynh_handle_getopts_args "$@"
- yunohost tools shell -c "from yunohost.permission import permission_remove; permission_remove('$app', '$permission', sync_perm=False)"
+ yunohost tools shell -c "from yunohost.permission import permission_delete; permission_delete('$app.$permission', sync_perm=False)"
}
-# Add a path managed by the SSO
+# Check if a permission exists
#
-# usage: ynh_permission_add_path --app "app" --permission "permission" --url "url" ["url" ...]
-# | arg: app - the application id
-# | arg: permission - the name for the permission
-# | arg: url - the FULL url for the the permission (ex domain.tld/apps/admin)
-ynh_permission_add_path() {
- declare -Ar args_array=( [a]=app= [p]=permission= [u]=url= )
- local app
+# usage: ynh_permission_exists --permission=permission
+# | arg: -p, --permission= - the permission to check
+# | exit: Return 1 if the permission doesn't exist, 0 otherwise
+#
+# Requires YunoHost version 3.7.0 or higher.
+ynh_permission_exists() {
+ # Declare an array to define the options of this helper.
+ local legacy_args=p
+ local -A args_array=( [p]=permission= )
+ local permission
+ ynh_handle_getopts_args "$@"
+
+ yunohost user permission list --short | grep --word-regexp --quiet "$app.$permission"
+}
+
+# Redefine the url associated to a permission
+#
+# usage: ynh_permission_url --permission="permission" [--url="url"]
+# | arg: -p, --permission= - the name for the permission (by default a permission named "main" is removed automatically when the app is removed)
+# | arg: -u, --url= - (optional) URL for which access will be allowed/forbidden
+#
+# Requires YunoHost version 3.7.0 or higher.
+ynh_permission_url() {
+ # Declare an array to define the options of this helper.
+ local legacy_args=pu
+ local -A args_array=([p]=permission= [u]=url=)
local permission
local url
ynh_handle_getopts_args "$@"
+ url=${url:-}
- yunohost tools shell -c "from yunohost.permission import permission_update; permission_update('$app', '$permission', add_url=['${url//';'/"','"}'], sync_perm=False)"
+ if [[ -n $url ]]
+ then
+ url="'$url'"
+ else
+ url="None"
+ fi
+
+ yunohost tools shell -c "from yunohost.permission import permission_url; permission_url('$app.$permission', url=$url)"
}
-# Remove a path managed by the SSO
+
+# Update a permission for the app
#
-# usage: ynh_permission_del_path --app "app" --permission "permission" --url "url" ["url" ...]
-# | arg: app - the application id
-# | arg: permission - the name for the permission
-# | arg: url - the FULL url for the the permission (ex domain.tld/apps/admin)
-ynh_permission_del_path() {
- declare -Ar args_array=( [a]=app= [p]=permission= [u]=url= )
- local app
+# example: ynh_permission_update --permission admin --add=samdoe --remove=all_users
+#
+# usage: ynh_permission_update --permission="permission" [--add="group1 group2"] [--remove="group1 group2"]
+# | arg: -p, --permission= - the name for the permission (by default a permission named "main" already exist)
+# | arg: -a, --add= - the list of group or users to enable add to the permission
+# | arg: -r, --remove= - the list of group or users to remove from the permission
+#
+# Requires YunoHost version 3.7.0 or higher.
+ynh_permission_update() {
+ # Declare an array to define the options of this helper.
+ local legacy_args=par
+ local -A args_array=( [p]=permission= [a]=add= [r]=remove= )
local permission
- local url
+ local add
+ local remove
+ ynh_handle_getopts_args "$@"
+ add=${add:-}
+ remove=${remove:-}
+
+ if [[ -n $add ]]; then
+ add="--add ${add//';'/" "}"
+ fi
+ if [[ -n $remove ]]; then
+ remove="--remove ${remove//';'/" "} "
+ fi
+
+ yunohost user permission update "$app.$permission" $add $remove
+}
+
+# Check if a permission has an user
+#
+# example: ynh_permission_has_user --permission=main --user=visitors
+#
+# usage: ynh_permission_has_user --permission=permission --user=user
+# | arg: -p, --permission= - the permission to check
+# | arg: -u, --user= - the user seek in the permission
+# | exit: Return 1 if the permission doesn't have that user or doesn't exist, 0 otherwise
+#
+# Requires YunoHost version 3.7.1 or higher.
+ynh_permission_has_user() {
+ local legacy_args=pu
+ # Declare an array to define the options of this helper.
+ local -A args_array=( [p]=permission= [u]=user= )
+ local permission
+ local user
+ # Manage arguments with getopts
ynh_handle_getopts_args "$@"
- yunohost tools shell -c "from yunohost.permission import permission_update; permission_update('$app', '$permission', remove_url=['${url//';'/"','"}'], sync_perm=False)"
+ if ! ynh_permission_exists --permission=$permission
+ then
+ return 1
+ fi
+
+ yunohost user permission info "$app.$permission" | grep --word-regexp --quiet "$user"
}
diff --git a/data/helpers.d/string b/data/helpers.d/string
index fcbc5190d..a0bcdbfaf 100644
--- a/data/helpers.d/string
+++ b/data/helpers.d/string
@@ -5,29 +5,30 @@
# example: pwd=$(ynh_string_random --length=8)
#
# usage: ynh_string_random [--length=string_length]
-# | arg: -l, --length - the string length to generate (default: 24)
+# | arg: -l, --length= - the string length to generate (default: 24)
+# | ret: the generated string
#
# Requires YunoHost version 2.2.4 or higher.
ynh_string_random() {
# Declare an array to define the options of this helper.
local legacy_args=l
- declare -Ar args_array=( [l]=length= )
+ local -A args_array=( [l]=length= )
local length
# Manage arguments with getopts
ynh_handle_getopts_args "$@"
length=${length:-24}
dd if=/dev/urandom bs=1 count=1000 2> /dev/null \
- | tr -c -d 'A-Za-z0-9' \
- | sed -n 's/\(.\{'"$length"'\}\).*/\1/p'
+ | tr --complement --delete 'A-Za-z0-9' \
+ | sed --quiet 's/\(.\{'"$length"'\}\).*/\1/p'
}
# Substitute/replace a string (or expression) by another in a file
#
# usage: ynh_replace_string --match_string=match_string --replace_string=replace_string --target_file=target_file
-# | arg: -m, --match_string - String to be searched and replaced in the file
-# | arg: -r, --replace_string - String that will replace matches
-# | arg: -f, --target_file - File in which the string will be replaced.
+# | arg: -m, --match_string= - String to be searched and replaced in the file
+# | arg: -r, --replace_string= - String that will replace matches
+# | arg: -f, --target_file= - File in which the string will be replaced.
#
# As this helper is based on sed command, regular expressions and
# references to sub-expressions can be used
@@ -35,53 +36,53 @@ ynh_string_random() {
#
# Requires YunoHost version 2.6.4 or higher.
ynh_replace_string () {
- # Declare an array to define the options of this helper.
- local legacy_args=mrf
- declare -Ar args_array=( [m]=match_string= [r]=replace_string= [f]=target_file= )
- local match_string
- local replace_string
- local target_file
- # Manage arguments with getopts
- ynh_handle_getopts_args "$@"
+ # Declare an array to define the options of this helper.
+ local legacy_args=mrf
+ local -A args_array=( [m]=match_string= [r]=replace_string= [f]=target_file= )
+ local match_string
+ local replace_string
+ local target_file
+ # Manage arguments with getopts
+ ynh_handle_getopts_args "$@"
- local delimit=@
- # Escape the delimiter if it's in the string.
- match_string=${match_string//${delimit}/"\\${delimit}"}
- replace_string=${replace_string//${delimit}/"\\${delimit}"}
+ local delimit=@
+ # Escape the delimiter if it's in the string.
+ match_string=${match_string//${delimit}/"\\${delimit}"}
+ replace_string=${replace_string//${delimit}/"\\${delimit}"}
- sudo sed --in-place "s${delimit}${match_string}${delimit}${replace_string}${delimit}g" "$target_file"
+ sed --in-place "s${delimit}${match_string}${delimit}${replace_string}${delimit}g" "$target_file"
}
# Substitute/replace a special string by another in a file
#
# usage: ynh_replace_special_string --match_string=match_string --replace_string=replace_string --target_file=target_file
-# | arg: -m, --match_string - String to be searched and replaced in the file
-# | arg: -r, --replace_string - String that will replace matches
-# | arg: -t, --target_file - File in which the string will be replaced.
+# | arg: -m, --match_string= - String to be searched and replaced in the file
+# | arg: -r, --replace_string= - String that will replace matches
+# | arg: -t, --target_file= - File in which the string will be replaced.
#
# This helper will use ynh_replace_string, but as you can use special
# characters, you can't use some regular expressions and sub-expressions.
#
# Requires YunoHost version 2.7.7 or higher.
ynh_replace_special_string () {
- # Declare an array to define the options of this helper.
- local legacy_args=mrf
- declare -Ar args_array=( [m]=match_string= [r]=replace_string= [f]=target_file= )
- local match_string
- local replace_string
- local target_file
- # Manage arguments with getopts
- ynh_handle_getopts_args "$@"
+ # Declare an array to define the options of this helper.
+ local legacy_args=mrf
+ local -A args_array=( [m]=match_string= [r]=replace_string= [f]=target_file= )
+ local match_string
+ local replace_string
+ local target_file
+ # Manage arguments with getopts
+ ynh_handle_getopts_args "$@"
- # Escape any backslash to preserve them as simple backslash.
- match_string=${match_string//\\/"\\\\"}
- replace_string=${replace_string//\\/"\\\\"}
+ # Escape any backslash to preserve them as simple backslash.
+ match_string=${match_string//\\/"\\\\"}
+ replace_string=${replace_string//\\/"\\\\"}
- # Escape the & character, who has a special function in sed.
- match_string=${match_string//&/"\&"}
- replace_string=${replace_string//&/"\&"}
+ # Escape the & character, who has a special function in sed.
+ match_string=${match_string//&/"\&"}
+ replace_string=${replace_string//&/"\&"}
- ynh_replace_string --match_string="$match_string" --replace_string="$replace_string" --target_file="$target_file"
+ ynh_replace_string --match_string="$match_string" --replace_string="$replace_string" --target_file="$target_file"
}
# Sanitize a string intended to be the name of a database
@@ -90,24 +91,26 @@ ynh_replace_special_string () {
# example: dbname=$(ynh_sanitize_dbid $app)
#
# usage: ynh_sanitize_dbid --db_name=name
-# | arg: -n, --db_name - name to correct/sanitize
+# | arg: -n, --db_name= - name to correct/sanitize
# | ret: the corrected name
#
# Requires YunoHost version 2.2.4 or higher.
ynh_sanitize_dbid () {
- # Declare an array to define the options of this helper.
- local legacy_args=n
- declare -Ar args_array=( [n]=db_name= )
- local db_name
- # Manage arguments with getopts
- ynh_handle_getopts_args "$@"
+ # Declare an array to define the options of this helper.
+ local legacy_args=n
+ local -A args_array=( [n]=db_name= )
+ local db_name
+ # Manage arguments with getopts
+ ynh_handle_getopts_args "$@"
- # We should avoid having - and . in the name of databases. They are replaced by _
- echo ${db_name//[-.]/_}
+ # We should avoid having - and . in the name of databases. They are replaced by _
+ echo ${db_name//[-.]/_}
}
# Normalize the url path syntax
#
+# [internal]
+#
# Handle the slash at the beginning of path and its absence at ending
# Return a normalized url path
#
@@ -119,23 +122,23 @@ ynh_sanitize_dbid () {
# ynh_normalize_url_path / # -> /
#
# usage: ynh_normalize_url_path --path_url=path_to_normalize
-# | arg: -p, --path_url - URL path to normalize before using it
+# | arg: -p, --path_url= - URL path to normalize before using it
#
# Requires YunoHost version 2.6.4 or higher.
ynh_normalize_url_path () {
- # Declare an array to define the options of this helper.
- local legacy_args=p
- declare -Ar args_array=( [p]=path_url= )
- local path_url
- # Manage arguments with getopts
- ynh_handle_getopts_args "$@"
+ # Declare an array to define the options of this helper.
+ local legacy_args=p
+ local -A args_array=( [p]=path_url= )
+ local path_url
+ # Manage arguments with getopts
+ ynh_handle_getopts_args "$@"
- test -n "$path_url" || ynh_die --message="ynh_normalize_url_path expect a URL path as first argument and received nothing."
- if [ "${path_url:0:1}" != "/" ]; then # If the first character is not a /
- path_url="/$path_url" # Add / at begin of path variable
- fi
- if [ "${path_url:${#path_url}-1}" == "/" ] && [ ${#path_url} -gt 1 ]; then # If the last character is a / and that not the only character.
- path_url="${path_url:0:${#path_url}-1}" # Delete the last character
- fi
- echo $path_url
+ test -n "$path_url" || ynh_die --message="ynh_normalize_url_path expect a URL path as first argument and received nothing."
+ if [ "${path_url:0:1}" != "/" ]; then # If the first character is not a /
+ path_url="/$path_url" # Add / at begin of path variable
+ fi
+ if [ "${path_url:${#path_url}-1}" == "/" ] && [ ${#path_url} -gt 1 ]; then # If the last character is a / and that not the only character.
+ path_url="${path_url:0:${#path_url}-1}" # Delete the last character
+ fi
+ echo $path_url
}
diff --git a/data/helpers.d/systemd b/data/helpers.d/systemd
index 4b3b5a289..39db5db53 100644
--- a/data/helpers.d/systemd
+++ b/data/helpers.d/systemd
@@ -3,8 +3,8 @@
# Create a dedicated systemd config
#
# usage: ynh_add_systemd_config [--service=service] [--template=template]
-# | arg: -s, --service - Service name (optionnal, $app by default)
-# | arg: -t, --template - Name of template file (optionnal, this is 'systemd' by default, meaning ./conf/systemd.service will be used as template)
+# | arg: -s, --service= - Service name (optionnal, $app by default)
+# | arg: -t, --template= - Name of template file (optionnal, this is 'systemd' by default, meaning ./conf/systemd.service will be used as template)
#
# This will use the template ../conf/.service
# to generate a systemd config, by replacing the following keywords
@@ -14,103 +14,106 @@
# __APP__ by $app
# __FINALPATH__ by $final_path
#
-# Requires YunoHost version 2.7.2 or higher.
+# Requires YunoHost version 2.7.11 or higher.
ynh_add_systemd_config () {
- # Declare an array to define the options of this helper.
- local legacy_args=st
- declare -Ar args_array=( [s]=service= [t]=template= )
- local service
- local template
- # Manage arguments with getopts
- ynh_handle_getopts_args "$@"
- local service="${service:-$app}"
- local template="${template:-systemd.service}"
+ # Declare an array to define the options of this helper.
+ local legacy_args=st
+ local -A args_array=( [s]=service= [t]=template= )
+ local service
+ local template
+ # Manage arguments with getopts
+ ynh_handle_getopts_args "$@"
+ local service="${service:-$app}"
+ local template="${template:-systemd.service}"
- finalsystemdconf="/etc/systemd/system/$service.service"
- ynh_backup_if_checksum_is_different --file="$finalsystemdconf"
- sudo cp ../conf/$template "$finalsystemdconf"
+ finalsystemdconf="/etc/systemd/system/$service.service"
+ ynh_backup_if_checksum_is_different --file="$finalsystemdconf"
+ cp ../conf/$template "$finalsystemdconf"
- # To avoid a break by set -u, use a void substitution ${var:-}. If the variable is not set, it's simply set with an empty variable.
- # Substitute in a nginx config file only if the variable is not empty
- if test -n "${final_path:-}"; then
- ynh_replace_string --match_string="__FINALPATH__" --replace_string="$final_path" --target_file="$finalsystemdconf"
- fi
- if test -n "${app:-}"; then
- ynh_replace_string --match_string="__APP__" --replace_string="$app" --target_file="$finalsystemdconf"
- fi
- ynh_store_file_checksum --file="$finalsystemdconf"
+ # To avoid a break by set -u, use a void substitution ${var:-}. If the variable is not set, it's simply set with an empty variable.
+ # Substitute in a nginx config file only if the variable is not empty
+ if [ -n "${final_path:-}" ]; then
+ ynh_replace_string --match_string="__FINALPATH__" --replace_string="$final_path" --target_file="$finalsystemdconf"
+ fi
+ if [ -n "${app:-}" ]; then
+ ynh_replace_string --match_string="__APP__" --replace_string="$app" --target_file="$finalsystemdconf"
+ fi
+ ynh_store_file_checksum --file="$finalsystemdconf"
- sudo chown root: "$finalsystemdconf"
- sudo systemctl enable $service
- sudo systemctl daemon-reload
+ chown root: "$finalsystemdconf"
+ systemctl enable $service
+ systemctl daemon-reload
}
# Remove the dedicated systemd config
#
# usage: ynh_remove_systemd_config [--service=service]
-# | arg: -s, --service - Service name (optionnal, $app by default)
+# | arg: -s, --service= - Service name (optionnal, $app by default)
#
# Requires YunoHost version 2.7.2 or higher.
ynh_remove_systemd_config () {
- # Declare an array to define the options of this helper.
- local legacy_args=s
- declare -Ar args_array=( [s]=service= )
- local service
- # Manage arguments with getopts
- ynh_handle_getopts_args "$@"
- local service="${service:-$app}"
+ # Declare an array to define the options of this helper.
+ local legacy_args=s
+ local -A args_array=( [s]=service= )
+ local service
+ # Manage arguments with getopts
+ ynh_handle_getopts_args "$@"
+ local service="${service:-$app}"
- local finalsystemdconf="/etc/systemd/system/$service.service"
- if [ -e "$finalsystemdconf" ]; then
- ynh_systemd_action --service_name=$service --action=stop
- systemctl disable $service
- ynh_secure_remove --file="$finalsystemdconf"
- systemctl daemon-reload
- fi
+ local finalsystemdconf="/etc/systemd/system/$service.service"
+ if [ -e "$finalsystemdconf" ]
+ then
+ ynh_systemd_action --service_name=$service --action=stop
+ systemctl disable $service
+ ynh_secure_remove --file="$finalsystemdconf"
+ systemctl daemon-reload
+ fi
}
# Start (or other actions) a service, print a log in case of failure and optionnaly wait until the service is completely started
#
-# usage: ynh_systemd_action [-n service_name] [-a action] [ [-l "line to match"] [-p log_path] [-t timeout] [-e length] ]
+# usage: ynh_systemd_action [--service_name=service_name] [--action=action] [ [--line_match="line to match"] [--log_path=log_path] [--timeout=300] [--length=20] ]
# | arg: -n, --service_name= - Name of the service to start. Default : $app
# | arg: -a, --action= - Action to perform with systemctl. Default: start
# | arg: -l, --line_match= - Line to match - The line to find in the log to attest the service have finished to boot. If not defined it don't wait until the service is completely started. WARNING: When using --line_match, you should always add `ynh_clean_check_starting` into your `ynh_clean_setup` at the beginning of the script. Otherwise, tail will not stop in case of failure of the script. The script will then hang forever.
# | arg: -p, --log_path= - Log file - Path to the log file. Default : /var/log/$app/$app.log
# | arg: -t, --timeout= - Timeout - The maximum time to wait before ending the watching. Default : 300 seconds.
# | arg: -e, --length= - Length of the error log : Default : 20
+#
+# Requires YunoHost version 3.5.0 or higher.
ynh_systemd_action() {
# Declare an array to define the options of this helper.
local legacy_args=nalpte
- declare -Ar args_array=( [n]=service_name= [a]=action= [l]=line_match= [p]=log_path= [t]=timeout= [e]=length= )
+ local -A args_array=( [n]=service_name= [a]=action= [l]=line_match= [p]=log_path= [t]=timeout= [e]=length= )
local service_name
local action
local line_match
local length
local log_path
local timeout
-
# Manage arguments with getopts
ynh_handle_getopts_args "$@"
-
- local service_name="${service_name:-$app}"
- local action=${action:-start}
- local log_path="${log_path:-/var/log/$service_name/$service_name.log}"
- local length=${length:-20}
- local timeout=${timeout:-300}
+ service_name="${service_name:-$app}"
+ action=${action:-start}
+ line_match=${line_match:-}
+ length=${length:-20}
+ log_path="${log_path:-/var/log/$service_name/$service_name.log}"
+ timeout=${timeout:-300}
# Start to read the log
- if [[ -n "${line_match:-}" ]]
+ if [[ -n "$line_match" ]]
then
local templog="$(mktemp)"
# Following the starting of the app in its log
- if [ "$log_path" == "systemd" ] ; then
+ if [ "$log_path" == "systemd" ]
+ then
# Read the systemd journal
journalctl --unit=$service_name --follow --since=-0 --quiet > "$templog" &
# Get the PID of the journalctl command
local pid_tail=$!
else
# Read the specified log file
- tail -F -n0 "$log_path" > "$templog" 2>&1 &
+ tail --follow=name --retry --lines=0 "$log_path" > "$templog" 2>&1 &
# Get the PID of the tail command
local pid_tail=$!
fi
@@ -121,10 +124,20 @@ ynh_systemd_action() {
action="reload-or-restart"
fi
- systemctl $action $service_name \
- || ( journalctl --no-pager --lines=$length -u $service_name >&2 \
- ; test -e "$log_path" && echo "--" >&2 && tail --lines=$length "$log_path" >&2 \
- ; false )
+ # If the service fails to perform the action
+ if ! systemctl $action $service_name
+ then
+ # Show syslog for this service
+ ynh_exec_err journalctl --no-pager --lines=$length --unit=$service_name
+ # If a log is specified for this service, show also the content of this log
+ if [ -e "$log_path" ]
+ then
+ ynh_print_err --message="--"
+ ynh_exec_err tail --lines=$length "$log_path"
+ fi
+ # Fail the app script, since the service failed.
+ ynh_die
+ fi
# Start the timeout and try to find line_match
if [[ -n "${line_match:-}" ]]
@@ -133,9 +146,9 @@ ynh_systemd_action() {
for i in $(seq 1 $timeout)
do
# Read the log until the sentence is found, that means the app finished to start. Or run until the timeout
- if grep --quiet "$line_match" "$templog"
+ if grep --extended-regexp --quiet "$line_match" "$templog"
then
- ynh_print_info --message="The service $service_name has correctly started."
+ ynh_print_info --message="The service $service_name has correctly executed the action ${action}."
break
fi
if [ $i -eq 3 ]; then
@@ -151,10 +164,14 @@ ynh_systemd_action() {
fi
if [ $i -eq $timeout ]
then
- ynh_print_warn --message="The service $service_name didn't fully started before the timeout."
+ ynh_print_warn --message="The service $service_name didn't fully executed the action ${action} before the timeout."
ynh_print_warn --message="Please find here an extract of the end of the log of the service $service_name:"
- journalctl --no-pager --lines=$length -u $service_name >&2
- test -e "$log_path" && echo "--" >&2 && tail --lines=$length "$log_path" >&2
+ ynh_exec_warn journalctl --no-pager --lines=$length --unit=$service_name
+ if [ -e "$log_path" ]
+ then
+ ynh_print_warn --message="\-\-\-"
+ ynh_exec_warn tail --lines=$length "$log_path"
+ fi
fi
ynh_clean_check_starting
fi
@@ -164,10 +181,17 @@ ynh_systemd_action() {
# (usually used in ynh_clean_setup scripts)
#
# usage: ynh_clean_check_starting
+#
+# Requires YunoHost version 3.5.0 or higher.
ynh_clean_check_starting () {
- # Stop the execution of tail.
- kill -s 15 $pid_tail 2>&1
- ynh_secure_remove "$templog" 2>&1
+ if [ -n "$pid_tail" ]
+ then
+ # Stop the execution of tail.
+ kill -SIGTERM $pid_tail 2>&1
+ fi
+ if [ -n "$templog" ]
+ then
+ ynh_secure_remove "$templog" 2>&1
+ fi
}
-
diff --git a/data/helpers.d/user b/data/helpers.d/user
index e7890ccb2..aeac3a9c5 100644
--- a/data/helpers.d/user
+++ b/data/helpers.d/user
@@ -5,18 +5,19 @@
# example: ynh_user_exists 'toto' || exit 1
#
# usage: ynh_user_exists --username=username
-# | arg: -u, --username - the username to check
+# | arg: -u, --username= - the username to check
+# | exit: Return 1 if the user doesn't exist, 0 otherwise
#
# Requires YunoHost version 2.2.4 or higher.
ynh_user_exists() {
# Declare an array to define the options of this helper.
local legacy_args=u
- declare -Ar args_array=( [u]=username= )
+ local -A args_array=( [u]=username= )
local username
# Manage arguments with getopts
ynh_handle_getopts_args "$@"
- sudo yunohost user list --output-as json | grep -q "\"username\": \"${username}\""
+ yunohost user list --output-as json | grep --quiet "\"username\": \"${username}\""
}
# Retrieve a YunoHost user information
@@ -24,21 +25,21 @@ ynh_user_exists() {
# example: mail=$(ynh_user_get_info 'toto' 'mail')
#
# usage: ynh_user_get_info --username=username --key=key
-# | arg: -u, --username - the username to retrieve info from
-# | arg: -k, --key - the key to retrieve
+# | arg: -u, --username= - the username to retrieve info from
+# | arg: -k, --key= - the key to retrieve
# | ret: string - the key's value
#
# Requires YunoHost version 2.2.4 or higher.
ynh_user_get_info() {
# Declare an array to define the options of this helper.
local legacy_args=uk
- declare -Ar args_array=( [u]=username= [k]=key= )
+ local -A args_array=( [u]=username= [k]=key= )
local username
local key
# Manage arguments with getopts
ynh_handle_getopts_args "$@"
- sudo yunohost user info "$username" --output-as plain | ynh_get_plain_key "$key"
+ yunohost user info "$username" --output-as plain | ynh_get_plain_key "$key"
}
# Get the list of YunoHost users
@@ -50,20 +51,21 @@ ynh_user_get_info() {
#
# Requires YunoHost version 2.4.0 or higher.
ynh_user_list() {
- sudo yunohost user list --output-as plain --quiet \
- | awk '/^##username$/{getline; print}'
+ yunohost user list --output-as plain --quiet \
+ | awk '/^##username$/{getline; print}'
}
# Check if a user exists on the system
#
# usage: ynh_system_user_exists --username=username
-# | arg: -u, --username - the username to check
+# | arg: -u, --username= - the username to check
+# | exit: Return 1 if the user doesn't exist, 0 otherwise
#
# Requires YunoHost version 2.2.4 or higher.
ynh_system_user_exists() {
# Declare an array to define the options of this helper.
local legacy_args=u
- declare -Ar args_array=( [u]=username= )
+ local -A args_array=( [u]=username= )
local username
# Manage arguments with getopts
ynh_handle_getopts_args "$@"
@@ -74,11 +76,14 @@ ynh_system_user_exists() {
# Check if a group exists on the system
#
# usage: ynh_system_group_exists --group=group
-# | arg: -g, --group - the group to check
+# | arg: -g, --group= - the group to check
+# | exit: Return 1 if the group doesn't exist, 0 otherwise
+#
+# Requires YunoHost version 3.5.0.2 or higher.
ynh_system_group_exists() {
# Declare an array to define the options of this helper.
local legacy_args=g
- declare -Ar args_array=( [g]=group= )
+ local -A args_array=( [g]=group= )
local group
# Manage arguments with getopts
ynh_handle_getopts_args "$@"
@@ -95,49 +100,51 @@ ynh_system_group_exists() {
# ynh_system_user_create --username=discourse --home_dir=/var/www/discourse --use_shell
#
# usage: ynh_system_user_create --username=user_name [--home_dir=home_dir] [--use_shell]
-# | arg: -u, --username - Name of the system user that will be create
-# | arg: -h, --home_dir - Path of the home dir for the user. Usually the final path of the app. If this argument is omitted, the user will be created without home
-# | arg: -s, --use_shell - Create a user using the default login shell if present. If this argument is omitted, the user will be created with /usr/sbin/nologin shell
+# | arg: -u, --username= - Name of the system user that will be create
+# | arg: -h, --home_dir= - Path of the home dir for the user. Usually the final path of the app. If this argument is omitted, the user will be created without home
+# | arg: -s, --use_shell - Create a user using the default login shell if present. If this argument is omitted, the user will be created with /usr/sbin/nologin shell
#
# Requires YunoHost version 2.6.4 or higher.
ynh_system_user_create () {
- # Declare an array to define the options of this helper.
- local legacy_args=uhs
- declare -Ar args_array=( [u]=username= [h]=home_dir= [s]=use_shell )
- local username
- local home_dir
- local use_shell
- # Manage arguments with getopts
- ynh_handle_getopts_args "$@"
- use_shell="${use_shell:-0}"
- home_dir="${home_dir:-}"
+ # Declare an array to define the options of this helper.
+ local legacy_args=uhs
+ local -A args_array=( [u]=username= [h]=home_dir= [s]=use_shell )
+ local username
+ local home_dir
+ local use_shell
+ # Manage arguments with getopts
+ ynh_handle_getopts_args "$@"
+ use_shell="${use_shell:-0}"
+ home_dir="${home_dir:-}"
- if ! ynh_system_user_exists "$username" # Check if the user exists on the system
- then # If the user doesn't exist
- if [ -n "$home_dir" ]; then # If a home dir is mentioned
- local user_home_dir="-d $home_dir"
- else
- local user_home_dir="--no-create-home"
- fi
- if [ $use_shell -eq 1 ]; then # If we want a shell for the user
- local shell="" # Use default shell
- else
- local shell="--shell /usr/sbin/nologin"
- fi
- useradd $user_home_dir --system --user-group $username $shell || ynh_die "Unable to create $username system account"
- fi
+ if ! ynh_system_user_exists "$username" # Check if the user exists on the system
+ then # If the user doesn't exist
+ if [ -n "$home_dir" ]
+ then # If a home dir is mentioned
+ local user_home_dir="--home-dir $home_dir"
+ else
+ local user_home_dir="--no-create-home"
+ fi
+ if [ $use_shell -eq 1 ]
+ then # If we want a shell for the user
+ local shell="" # Use default shell
+ else
+ local shell="--shell /usr/sbin/nologin"
+ fi
+ useradd $user_home_dir --system --user-group $username $shell || ynh_die --message="Unable to create $username system account"
+ fi
}
# Delete a system user
#
# usage: ynh_system_user_delete --username=user_name
-# | arg: -u, --username - Name of the system user that will be create
+# | arg: -u, --username= - Name of the system user that will be create
#
# Requires YunoHost version 2.6.4 or higher.
ynh_system_user_delete () {
# Declare an array to define the options of this helper.
local legacy_args=u
- declare -Ar args_array=( [u]=username= )
+ local -A args_array=( [u]=username= )
local username
# Manage arguments with getopts
ynh_handle_getopts_args "$@"
@@ -145,14 +152,14 @@ ynh_system_user_delete () {
# Check if the user exists on the system
if ynh_system_user_exists "$username"
then
- deluser $username
- else
- ynh_print_warn --message="The user $username was not found"
+ deluser $username
+ else
+ ynh_print_warn --message="The user $username was not found"
fi
# Check if the group exists on the system
if ynh_system_group_exists "$username"
then
- delgroup $username
+ delgroup $username
fi
}
diff --git a/data/helpers.d/utils b/data/helpers.d/utils
index e1feed6b1..fb50305ce 100644
--- a/data/helpers.d/utils
+++ b/data/helpers.d/utils
@@ -16,60 +16,26 @@
#
# It prints a warning to inform that the script was failed, and execute the ynh_clean_setup function if used in the app script
#
+# Requires YunoHost version 2.6.4 or higher.
ynh_exit_properly () {
- local exit_code=$?
- if [ "$exit_code" -eq 0 ]; then
- exit 0 # Exit without error if the script ended correctly
- fi
+ local exit_code=$?
+ if [ "$exit_code" -eq 0 ]; then
+ exit 0 # Exit without error if the script ended correctly
+ fi
- trap '' EXIT # Ignore new exit signals
- set +eu # Do not exit anymore if a command fail or if a variable is empty
+ trap '' EXIT # Ignore new exit signals
+ # Do not exit anymore if a command fail or if a variable is empty
+ set +o errexit # set +e
+ set +o nounset # set +u
- # Small tempo to avoid the next message being mixed up with other DEBUG messages
- sleep 0.5
+ # Small tempo to avoid the next message being mixed up with other DEBUG messages
+ sleep 0.5
- ynh_print_err --message="!!\n $app's script has encountered an error. Its execution was cancelled.\n!!"
+ if type -t ynh_clean_setup > /dev/null; then # Check if the function exist in the app script.
+ ynh_clean_setup # Call the function to do specific cleaning for the app.
+ fi
- # If the script is executed from the CLI, dump the end of the log that precedes the crash.
- if [ "$YNH_INTERFACE" == "cli" ]
- then
- # Unset xtrace to not spoil the log
- set +x
-
- local ynh_log="/var/log/yunohost/yunohost-cli.log"
-
- # Wait for the log to be fill with the data until the crash.
- local timeout=0
- while ! tail --lines=20 "$ynh_log" | grep --quiet "+ ynh_exit_properly"
- do
- ((timeout++))
- if [ $timeout -eq 500 ]; then
- break
- fi
- done
-
- echo -e "\e[34m\e[1mPlease find here an extract of the log before the crash:\e[0m" >&2
- # Tail the last 30 lines of log of YunoHost
- # But remove all lines after "ynh_exit_properly"
- # Remove the timestamp at the beginning of the line
- # Remove "yunohost.hook..."
- # Add DEBUG and color it at the beginning of each log line.
- echo -e "$(tail --lines=30 "$ynh_log" \
- | sed '1,/+ ynh_exit_properly/!d' \
- | sed 's/^[[:digit:]: ,-]*//g' \
- | sed 's/ *yunohost.hook.*\]/ -/g' \
- | sed 's/^WARNING /&/g' \
- | sed 's/^DEBUG /& /g' \
- | sed 's/^INFO /& /g' \
- | sed 's/^/\\e[34m\\e[1m[DEBUG]\\e[0m: /g')" >&2
- set -x
- fi
-
- if type -t ynh_clean_setup > /dev/null; then # Check if the function exist in the app script.
- ynh_clean_setup # Call the function to do specific cleaning for the app.
- fi
-
- ynh_die # Exit with error status
+ ynh_die # Exit with error status
}
# Exits if an error occurs during the execution of the script.
@@ -83,12 +49,17 @@ ynh_exit_properly () {
#
# Requires YunoHost version 2.6.4 or higher.
ynh_abort_if_errors () {
- set -eu # Exit if a command fail, and if a variable is used unset.
- trap ynh_exit_properly EXIT # Capturing exit signals on shell script
+ set -o errexit # set -e; Exit if a command fail
+ set -o nounset # set -u; And if a variable is used unset
+ trap ynh_exit_properly EXIT # Capturing exit signals on shell script
}
# Download, check integrity, uncompress and patch the source from app.src
#
+# usage: ynh_setup_source --dest_dir=dest_dir [--source_id=source_id]
+# | arg: -d, --dest_dir= - Directory where to setup sources
+# | arg: -s, --source_id= - Name of the app, if the package contains more than one app
+#
# The file conf/app.src need to contains:
#
# SOURCE_URL=Address to download the app archive
@@ -127,16 +98,11 @@ ynh_abort_if_errors () {
# Finally, patches named sources/patches/${src_id}-*.patch and extra files in
# sources/extra_files/$src_id will be applied to dest_dir
#
-#
-# usage: ynh_setup_source --dest_dir=dest_dir [--source_id=source_id]
-# | arg: -d, --dest_dir - Directory where to setup sources
-# | arg: -s, --source_id - Name of the app, if the package contains more than one app
-#
# Requires YunoHost version 2.6.4 or higher.
ynh_setup_source () {
# Declare an array to define the options of this helper.
local legacy_args=ds
- declare -Ar args_array=( [d]=dest_dir= [s]=source_id= )
+ local -A args_array=( [d]=dest_dir= [s]=source_id= )
local dest_dir
local source_id
# Manage arguments with getopts
@@ -152,13 +118,13 @@ ynh_setup_source () {
# Load value from configuration file (see above for a small doc about this file
# format)
- local src_url=$(grep 'SOURCE_URL=' "$src_file_path" | cut -d= -f2-)
- local src_sum=$(grep 'SOURCE_SUM=' "$src_file_path" | cut -d= -f2-)
- local src_sumprg=$(grep 'SOURCE_SUM_PRG=' "$src_file_path" | cut -d= -f2-)
- local src_format=$(grep 'SOURCE_FORMAT=' "$src_file_path" | cut -d= -f2-)
- local src_extract=$(grep 'SOURCE_EXTRACT=' "$src_file_path" | cut -d= -f2-)
- local src_in_subdir=$(grep 'SOURCE_IN_SUBDIR=' "$src_file_path" | cut -d= -f2-)
- local src_filename=$(grep 'SOURCE_FILENAME=' "$src_file_path" | cut -d= -f2-)
+ local src_url=$(grep 'SOURCE_URL=' "$src_file_path" | cut --delimiter='=' --fields=2-)
+ local src_sum=$(grep 'SOURCE_SUM=' "$src_file_path" | cut --delimiter='=' --fields=2-)
+ local src_sumprg=$(grep 'SOURCE_SUM_PRG=' "$src_file_path" | cut --delimiter='=' --fields=2-)
+ local src_format=$(grep 'SOURCE_FORMAT=' "$src_file_path" | cut --delimiter='=' --fields=2-)
+ local src_extract=$(grep 'SOURCE_EXTRACT=' "$src_file_path" | cut --delimiter='=' --fields=2-)
+ local src_in_subdir=$(grep 'SOURCE_IN_SUBDIR=' "$src_file_path" | cut --delimiter='=' --fields=2-)
+ local src_filename=$(grep 'SOURCE_FILENAME=' "$src_file_path" | cut --delimiter='=' --fields=2-)
# Default value
src_sumprg=${src_sumprg:-sha256sum}
@@ -166,7 +132,7 @@ ynh_setup_source () {
src_format=${src_format:-tar.gz}
src_format=$(echo "$src_format" | tr '[:upper:]' '[:lower:]')
src_extract=${src_extract:-true}
- if [ "$src_filename" = "" ] ; then
+ if [ "$src_filename" = "" ]; then
src_filename="${source_id}.${src_format}"
fi
local local_src="/opt/yunohost-apps-src/${YNH_APP_ID}/${src_filename}"
@@ -175,15 +141,15 @@ ynh_setup_source () {
then # Use the local source file if it is present
cp $local_src $src_filename
else # If not, download the source
- local out=`wget -nv -O $src_filename $src_url 2>&1` || ynh_print_err --message="$out"
+ local out=`wget --no-verbose --output-document=$src_filename $src_url 2>&1` || ynh_print_err --message="$out"
fi
# Check the control sum
- echo "${src_sum} ${src_filename}" | ${src_sumprg} -c --status \
+ echo "${src_sum} ${src_filename}" | ${src_sumprg} --check --status \
|| ynh_die --message="Corrupt source"
# Extract source into the app dir
- mkdir -p "$dest_dir"
+ mkdir --parents "$dest_dir"
if ! "$src_extract"
then
@@ -192,10 +158,11 @@ ynh_setup_source () {
then
# Zip format
# Using of a temp directory, because unzip doesn't manage --strip-components
- if $src_in_subdir ; then
- local tmp_dir=$(mktemp -d)
+ if $src_in_subdir
+ then
+ local tmp_dir=$(mktemp --directory)
unzip -quo $src_filename -d "$tmp_dir"
- cp -a $tmp_dir/*/. "$dest_dir"
+ cp --archive $tmp_dir/*/. "$dest_dir"
ynh_secure_remove --file="$tmp_dir"
else
unzip -quo $src_filename -d "$dest_dir"
@@ -204,40 +171,40 @@ ynh_setup_source () {
local strip=""
if [ "$src_in_subdir" != "false" ]
then
- if [ "$src_in_subdir" == "true" ]; then
+ if [ "$src_in_subdir" == "true" ]
+ then
local sub_dirs=1
else
local sub_dirs="$src_in_subdir"
fi
strip="--strip-components $sub_dirs"
fi
- if [[ "$src_format" =~ ^tar.gz|tar.bz2|tar.xz$ ]] ; then
- tar -xf $src_filename -C "$dest_dir" $strip
+ if [[ "$src_format" =~ ^tar.gz|tar.bz2|tar.xz$ ]]
+ then
+ tar --extract --file=$src_filename --directory="$dest_dir" $strip
else
ynh_die --message="Archive format unrecognized."
fi
fi
# Apply patches
- if (( $(find $YNH_CWD/../sources/patches/ -type f -name "${source_id}-*.patch" 2> /dev/null | wc -l) > "0" )); then
- local old_dir=$(pwd)
- (cd "$dest_dir" \
- && for p in $YNH_CWD/../sources/patches/${source_id}-*.patch; do \
- patch -p1 < $p; done) \
- || ynh_die --message="Unable to apply patches"
- cd $old_dir
+ if (( $(find $YNH_CWD/../sources/patches/ -type f -name "${source_id}-*.patch" 2> /dev/null | wc --lines) > "0" ))
+ then
+ (cd "$dest_dir"
+ for p in $YNH_CWD/../sources/patches/${source_id}-*.patch
+ do
+ patch --strip=1 < $p
+ done) || ynh_die --message="Unable to apply patches"
fi
# Add supplementary files
if test -e "$YNH_CWD/../sources/extra_files/${source_id}"; then
- cp -a $YNH_CWD/../sources/extra_files/$source_id/. "$dest_dir"
+ cp --archive $YNH_CWD/../sources/extra_files/$source_id/. "$dest_dir"
fi
}
# Curl abstraction to help with POST requests to local pages (such as installation forms)
#
-# $domain and $path_url should be defined externally (and correspond to the domain.tld and the /path (of the app?))
-#
# example: ynh_local_curl "/install.php?installButton" "foo=$var1" "bar=$var2"
#
# usage: ynh_local_curl "page_uri" "key1=value1" "key2=value2" ...
@@ -246,6 +213,10 @@ ynh_setup_source () {
# | arg: key2=value2 - (Optionnal) Another POST key and corresponding value
# | arg: ... - (Optionnal) More POST keys and values
#
+# For multiple calls, cookies are persisted between each call for the same app
+#
+# $domain and $path_url should be defined externally (and correspond to the domain.tld and the /path (of the app?))
+#
# Requires YunoHost version 2.6.4 or higher.
ynh_local_curl () {
# Define url of page to curl
@@ -273,13 +244,20 @@ ynh_local_curl () {
# Wait untils nginx has fully reloaded (avoid curl fail with http2)
sleep 2
+
+ local cookiefile=/tmp/ynh-$app-cookie.txt
+ touch $cookiefile
+ chown root $cookiefile
+ chmod 700 $cookiefile
# Curl the URL
- curl --silent --show-error -kL -H "Host: $domain" --resolve $domain:443:127.0.0.1 $POST_data "$full_page_url"
+ curl --silent --show-error --insecure --location --header "Host: $domain" --resolve $domain:443:127.0.0.1 $POST_data "$full_page_url" --cookie-jar $cookiefile --cookie $cookiefile
}
# Render templates with Jinja2
#
+# [internal]
+#
# Attention : Variables should be exported before calling this helper to be
# accessible inside templates.
#
@@ -287,13 +265,13 @@ ynh_local_curl () {
# | arg: some_template - Template file to be rendered
# | arg: output_path - The path where the output will be redirected to
ynh_render_template() {
- local template_path=$1
- local output_path=$2
- mkdir -p "$(dirname $output_path)"
- # Taken from https://stackoverflow.com/a/35009576
- python2.7 -c 'import os, sys, jinja2; sys.stdout.write(
+ local template_path=$1
+ local output_path=$2
+ mkdir -p "$(dirname $output_path)"
+ # Taken from https://stackoverflow.com/a/35009576
+ python2.7 -c 'import os, sys, jinja2; sys.stdout.write(
jinja2.Template(sys.stdin.read()
- ).render(os.environ));' < $template_path > $output_path
+ ).render(os.environ));' < $template_path > $output_path
}
# Fetch the Debian release codename
@@ -303,7 +281,7 @@ ynh_render_template() {
#
# Requires YunoHost version 2.7.12 or higher.
ynh_get_debian_release () {
- echo $(lsb_release --codename --short)
+ echo $(lsb_release --codename --short)
}
# Create a directory under /tmp
@@ -318,7 +296,7 @@ ynh_mkdir_tmp() {
ynh_print_warn --message="The helper ynh_mkdir_tmp is deprecated."
ynh_print_warn --message="You should use 'mktemp -d' instead and manage permissions \
properly with chmod/chown."
- local TMP_DIR=$(mktemp -d)
+ local TMP_DIR=$(mktemp --directory)
# Give rights to other users could be a security risk.
# But for retrocompatibility we need it. (This helpers is deprecated)
@@ -329,13 +307,13 @@ properly with chmod/chown."
# Remove a file or a directory securely
#
# usage: ynh_secure_remove --file=path_to_remove
-# | arg: -f, --file - File or directory to remove
+# | arg: -f, --file= - File or directory to remove
#
# Requires YunoHost version 2.6.4 or higher.
ynh_secure_remove () {
# Declare an array to define the options of this helper.
local legacy_args=f
- declare -Ar args_array=( [f]=file= )
+ local -A args_array=( [f]=file= )
local file
# Manage arguments with getopts
ynh_handle_getopts_args "$@"
@@ -349,26 +327,29 @@ ynh_secure_remove () {
ynh_print_warn --message="/!\ Packager ! You provided more than one argument to ynh_secure_remove but it will be ignored... Use this helper with one argument at time."
fi
- if [[ "$forbidden_path" =~ "$file" \
+ if [[ -z "$file" ]]
+ then
+ ynh_print_warn --message="ynh_secure_remove called with empty argument, ignoring."
+ elif [[ "$forbidden_path" =~ "$file" \
# Match all paths or subpaths in $forbidden_path
|| "$file" =~ ^/[[:alnum:]]+$ \
# Match all first level paths from / (Like /var, /root, etc...)
|| "${file:${#file}-1}" = "/" ]]
# Match if the path finishes by /. Because it seems there is an empty variable
then
- ynh_print_warn --message="Avoid deleting $file."
+ ynh_print_warn --message="Not deleting '$file' because it is not an acceptable path to delete."
+ elif [ -e "$file" ]
+ then
+ rm --recursive "$file"
else
- if [ -e "$file" ]
- then
- sudo rm -R "$file"
- else
- ynh_print_info --message="$file wasn't deleted because it doesn't exist."
- fi
+ ynh_print_info --message="'$file' wasn't deleted because it doesn't exist."
fi
}
# Extract a key from a plain command output
#
+# [internal]
+#
# example: yunohost user info tata --output-as plain | ynh_get_plain_key mail
#
# usage: ynh_get_plain_key key [subkey [subsubkey ...]]
@@ -382,12 +363,16 @@ ynh_get_plain_key() {
# an info to be redacted by the core
local key_=$1
shift
- while read line; do
- if [[ "$founded" == "1" ]] ; then
+ while read line
+ do
+ if [[ "$founded" == "1" ]]
+ then
[[ "$line" =~ ^${prefix}[^#] ]] && return
echo $line
- elif [[ "$line" =~ ^${prefix}${key_}$ ]]; then
- if [[ -n "${1:-}" ]]; then
+ elif [[ "$line" =~ ^${prefix}${key_}$ ]]
+ then
+ if [[ -n "${1:-}" ]]
+ then
prefix+="#"
key_=$1
shift
@@ -400,72 +385,75 @@ ynh_get_plain_key() {
# Read the value of a key in a ynh manifest file
#
-# usage: ynh_read_manifest manifest key
-# | arg: -m, --manifest= - Path of the manifest to read
-# | arg: -k, --key= - Name of the key to find
+# usage: ynh_read_manifest --manifest="manifest.json" --key="key"
+# | arg: -m, --manifest= - Path of the manifest to read
+# | arg: -k, --key= - Name of the key to find
+# | ret: the value associate to that key
#
# Requires YunoHost version 3.5.0 or higher.
ynh_read_manifest () {
- # Declare an array to define the options of this helper.
- local legacy_args=mk
- declare -Ar args_array=( [m]=manifest= [k]=manifest_key= )
- local manifest
- local manifest_key
- # Manage arguments with getopts
- ynh_handle_getopts_args "$@"
+ # Declare an array to define the options of this helper.
+ local legacy_args=mk
+ local -A args_array=( [m]=manifest= [k]=manifest_key= )
+ local manifest
+ local manifest_key
+ # Manage arguments with getopts
+ ynh_handle_getopts_args "$@"
- if [ ! -e "$manifest" ]; then
- # If the manifest isn't found, try the common place for backup and restore script.
- manifest="../settings/manifest.json"
- fi
+ if [ ! -e "$manifest" ]; then
+ # If the manifest isn't found, try the common place for backup and restore script.
+ manifest="../settings/manifest.json"
+ fi
- jq ".$manifest_key" "$manifest" --raw-output
+ jq ".$manifest_key" "$manifest" --raw-output
}
# Read the upstream version from the manifest
#
+# usage: ynh_app_upstream_version [--manifest="manifest.json"]
+# | arg: -m, --manifest= - Path of the manifest to read
+# | ret: the version number of the upstream app
+#
# The version number in the manifest is defined by ~ynh
# For example : 4.3-2~ynh3
# This include the number before ~ynh
# In the last example it return 4.3-2
#
-# usage: ynh_app_upstream_version [-m manifest]
-# | arg: -m, --manifest= - Path of the manifest to read
-#
# Requires YunoHost version 3.5.0 or higher.
ynh_app_upstream_version () {
# Declare an array to define the options of this helper.
local legacy_args=m
- declare -Ar args_array=( [m]=manifest= )
+ local -A args_array=( [m]=manifest= )
local manifest
# Manage arguments with getopts
ynh_handle_getopts_args "$@"
-
manifest="${manifest:-../manifest.json}"
+
version_key=$(ynh_read_manifest --manifest="$manifest" --manifest_key="version")
echo "${version_key/~ynh*/}"
}
# Read package version from the manifest
#
+# usage: ynh_app_package_version [--manifest="manifest.json"]
+# | arg: -m, --manifest= - Path of the manifest to read
+# | ret: the version number of the package
+#
# The version number in the manifest is defined by ~ynh
# For example : 4.3-2~ynh3
# This include the number after ~ynh
# In the last example it return 3
#
-# usage: ynh_app_package_version [-m manifest]
-# | arg: -m, --manifest= - Path of the manifest to read
-#
# Requires YunoHost version 3.5.0 or higher.
ynh_app_package_version () {
# Declare an array to define the options of this helper.
local legacy_args=m
- declare -Ar args_array=( [m]=manifest= )
+ local -A args_array=( [m]=manifest= )
local manifest
# Manage arguments with getopts
ynh_handle_getopts_args "$@"
-
manifest="${manifest:-../manifest.json}"
+
version_key=$(ynh_read_manifest --manifest="$manifest" --manifest_key="version")
echo "${version_key/*~ynh/}"
}
@@ -488,32 +476,34 @@ ynh_app_package_version () {
#
# Requires YunoHost version 3.5.0 or higher.
ynh_check_app_version_changed () {
- local force_upgrade=${YNH_FORCE_UPGRADE:-0}
- local package_check=${PACKAGE_CHECK_EXEC:-0}
+ local force_upgrade=${YNH_FORCE_UPGRADE:-0}
+ local package_check=${PACKAGE_CHECK_EXEC:-0}
- # By default, upstream app version has changed
- local return_value="UPGRADE_APP"
+ # By default, upstream app version has changed
+ local return_value="UPGRADE_APP"
- local current_version=$(ynh_read_manifest --manifest="/etc/yunohost/apps/$YNH_APP_INSTANCE_NAME/manifest.json" --manifest_key="version" || echo 1.0)
- local current_upstream_version="$(ynh_app_upstream_version --manifest="/etc/yunohost/apps/$YNH_APP_INSTANCE_NAME/manifest.json")"
- local update_version=$(ynh_read_manifest --manifest="../manifest.json" --manifest_key="version" || echo 1.0)
- local update_upstream_version="$(ynh_app_upstream_version)"
+ local current_version=$(ynh_read_manifest --manifest="/etc/yunohost/apps/$YNH_APP_INSTANCE_NAME/manifest.json" --manifest_key="version" || echo 1.0)
+ local current_upstream_version="$(ynh_app_upstream_version --manifest="/etc/yunohost/apps/$YNH_APP_INSTANCE_NAME/manifest.json")"
+ local update_version=$(ynh_read_manifest --manifest="../manifest.json" --manifest_key="version" || echo 1.0)
+ local update_upstream_version="$(ynh_app_upstream_version)"
- if [ "$current_version" == "$update_version" ] ; then
- # Complete versions are the same
- if [ "$force_upgrade" != "0" ]
- then
- ynh_print_info --message="Upgrade forced by YNH_FORCE_UPGRADE."
- unset YNH_FORCE_UPGRADE
- elif [ "$package_check" != "0" ]
- then
- ynh_print_info --message="Upgrade forced for package check."
- else
- ynh_die "Up-to-date, nothing to do" 0
- fi
- elif [ "$current_upstream_version" == "$update_upstream_version" ] ; then
- # Upstream versions are the same, only YunoHost package versions differ
- return_value="UPGRADE_PACKAGE"
- fi
- echo $return_value
+ if [ "$current_version" == "$update_version" ]
+ then
+ # Complete versions are the same
+ if [ "$force_upgrade" != "0" ]
+ then
+ ynh_print_info --message="Upgrade forced by YNH_FORCE_UPGRADE."
+ unset YNH_FORCE_UPGRADE
+ elif [ "$package_check" != "0" ]
+ then
+ ynh_print_info --message="Upgrade forced for package check."
+ else
+ ynh_die "Up-to-date, nothing to do" 0
+ fi
+ elif [ "$current_upstream_version" == "$update_upstream_version" ]
+ then
+ # Upstream versions are the same, only YunoHost package versions differ
+ return_value="UPGRADE_PACKAGE"
+ fi
+ echo $return_value
}
diff --git a/data/hooks/backup/05-conf_ldap b/data/hooks/backup/05-conf_ldap
index 9ae22095e..75b4c2075 100755
--- a/data/hooks/backup/05-conf_ldap
+++ b/data/hooks/backup/05-conf_ldap
@@ -11,7 +11,7 @@ backup_dir="${1}/conf/ldap"
# Backup the configuration
ynh_backup "/etc/ldap/slapd.conf" "${backup_dir}/slapd.conf"
-sudo slapcat -b cn=config -l "${backup_dir}/cn=config.master.ldif"
+slapcat -b cn=config -l "${backup_dir}/cn=config.master.ldif"
# Backup the database
-sudo slapcat -b dc=yunohost,dc=org -l "${backup_dir}/dc=yunohost-dc=org.ldif"
+slapcat -b dc=yunohost,dc=org -l "${backup_dir}/dc=yunohost-dc=org.ldif"
diff --git a/data/hooks/conf_regen/01-yunohost b/data/hooks/conf_regen/01-yunohost
index faf041110..236619079 100755
--- a/data/hooks/conf_regen/01-yunohost
+++ b/data/hooks/conf_regen/01-yunohost
@@ -38,28 +38,35 @@ do_pre_regen() {
if [[ -f $services_path ]]; then
tmp_services_path="${services_path}-tmp"
new_services_path="${services_path}-new"
- sudo cp "$services_path" "$tmp_services_path"
+ cp "$services_path" "$tmp_services_path"
_update_services "$new_services_path" || {
- sudo mv "$tmp_services_path" "$services_path"
+ mv "$tmp_services_path" "$services_path"
exit 1
}
if [[ -f $new_services_path ]]; then
# replace services.yml with new one
- sudo mv "$new_services_path" "$services_path"
- sudo mv "$tmp_services_path" "${services_path}-old"
+ mv "$new_services_path" "$services_path"
+ mv "$tmp_services_path" "${services_path}-old"
else
- sudo rm -f "$tmp_services_path"
+ rm -f "$tmp_services_path"
fi
else
- sudo cp services.yml /etc/yunohost/services.yml
+ cp services.yml /etc/yunohost/services.yml
fi
- mkdir -p "$pending_dir"/etc/etckeeper/
- cp etckeeper.conf "$pending_dir"/etc/etckeeper/
+ # add cron job for diagnosis to be ran at 7h and 19h + a random delay between
+ # 0 and 10min, meant to avoid every instances running their diagnosis at
+ # exactly the same time, which may overload the diagnosis server.
+ mkdir -p $pending_dir/etc/cron.d/
+ cat > $pending_dir/etc/cron.d/yunohost-diagnosis << EOF
+SHELL=/bin/bash
+0 7,19 * * * root : YunoHost Diagnosis; sleep \$((RANDOM\\%600)); yunohost diagnosis run > /dev/null
+EOF
+
}
_update_services() {
- sudo python2 - << EOF
+ python2 - << EOF
import yaml
@@ -104,6 +111,12 @@ for service, conf in new_services.items():
if conffiles:
services[service]['conffiles'] = conffiles
+ # Remove legacy /var/log/daemon.log and /var/log/syslog from log entries
+ # because they are too general. Instead, now the journalctl log is
+ # returned by default which is more relevant.
+ if "log" in services[service]:
+ if services[service]["log"] in ["/var/log/syslog", "/var/log/daemon.log"]:
+ del services[service]["log"]
if updated:
with open('/etc/yunohost/services.yml-new', 'w') as f:
diff --git a/data/hooks/conf_regen/02-ssl b/data/hooks/conf_regen/02-ssl
index 1df3a3260..a893b21e1 100755
--- a/data/hooks/conf_regen/02-ssl
+++ b/data/hooks/conf_regen/02-ssl
@@ -99,13 +99,13 @@ do_post_regen() {
[[ -f "${index_txt}" ]] || {
if [[ -f "${index_txt}.saved" ]]; then
# use saved database from 2.2
- sudo cp "${index_txt}.saved" "${index_txt}"
+ cp "${index_txt}.saved" "${index_txt}"
elif [[ -f "${index_txt}.old" ]]; then
# ... or use the state-1 database
- sudo cp "${index_txt}.old" "${index_txt}"
+ cp "${index_txt}.old" "${index_txt}"
else
# ... or create an empty one
- sudo touch "${index_txt}"
+ touch "${index_txt}"
fi
}
diff --git a/data/hooks/conf_regen/06-slapd b/data/hooks/conf_regen/06-slapd
index 4f7adda78..9b2c20138 100755
--- a/data/hooks/conf_regen/06-slapd
+++ b/data/hooks/conf_regen/06-slapd
@@ -63,6 +63,9 @@ do_pre_regen() {
cp -a ldap.conf slapd.conf "$ldap_dir"
cp -a sudo.schema mailserver.schema yunohost.schema "$schema_dir"
+ mkdir -p ${pending_dir}/etc/systemd/system/slapd.service.d/
+ cp systemd-override.conf ${pending_dir}/etc/systemd/system/slapd.service.d/ynh-override.conf
+
install -D -m 644 slapd.default "${pending_dir}/etc/default/slapd"
}
@@ -81,6 +84,14 @@ do_post_regen() {
chown -R openldap:openldap /etc/ldap/slapd.d/
chown -R root:ssl-cert /etc/yunohost/certs/yunohost.org/
chmod o-rwx /etc/yunohost/certs/yunohost.org/
+ chmod -R g+rx /etc/yunohost/certs/yunohost.org/
+
+ # If we changed the systemd ynh-override conf
+ if echo "$regen_conf_files" | sed 's/,/\n/g' | grep -q "^/etc/systemd/system/slapd.service.d/ynh-override.conf$"
+ then
+ systemctl daemon-reload
+ systemctl restart slapd
+ fi
[ -z "$regen_conf_files" ] && exit 0
@@ -126,7 +137,7 @@ do_post_regen() {
# 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))
+ while ! su admin -c '' && ((number_of_wait < 60))
do
sleep 5
((number_of_wait += 1))
diff --git a/data/hooks/conf_regen/09-nslcd b/data/hooks/conf_regen/09-nslcd
index 5071ac1fd..7090fc758 100755
--- a/data/hooks/conf_regen/09-nslcd
+++ b/data/hooks/conf_regen/09-nslcd
@@ -14,7 +14,7 @@ do_post_regen() {
regen_conf_files=$1
[[ -z "$regen_conf_files" ]] \
- || sudo service nslcd restart
+ || service nslcd restart
}
FORCE=${2:-0}
diff --git a/data/hooks/conf_regen/12-metronome b/data/hooks/conf_regen/12-metronome
index 4214722fc..55433e13c 100755
--- a/data/hooks/conf_regen/12-metronome
+++ b/data/hooks/conf_regen/12-metronome
@@ -14,7 +14,7 @@ do_pre_regen() {
# retrieve variables
main_domain=$(cat /etc/yunohost/current_host)
- domain_list=$(sudo yunohost domain list --output-as plain --quiet)
+ domain_list=$(yunohost domain list --output-as plain --quiet)
# install main conf file
cat metronome.cfg.lua \
@@ -41,20 +41,25 @@ do_pre_regen() {
do_post_regen() {
regen_conf_files=$1
- # fix some permissions
- sudo chown -R metronome: /var/lib/metronome/
- sudo chown -R metronome: /etc/metronome/conf.d/
-
# retrieve variables
- domain_list=$(sudo yunohost domain list --output-as plain --quiet)
+ main_domain=$(cat /etc/yunohost/current_host)
+ domain_list=$(yunohost domain list --exclude-subdomains --output-as plain --quiet)
# create metronome directories for domains
for domain in $domain_list; do
- sudo mkdir -p "/var/lib/metronome/${domain//./%2e}/pep"
+ mkdir -p "/var/lib/metronome/${domain//./%2e}/pep"
+ # http_upload directory must be writable by metronome and readable by nginx
+ mkdir -p "/var/xmpp-upload/${domain}/upload"
+ chmod g+s "/var/xmpp-upload/${domain}/upload"
+ chown -R metronome:www-data "/var/xmpp-upload/${domain}"
done
+ # fix some permissions
+ chown -R metronome: /var/lib/metronome/
+ chown -R metronome: /etc/metronome/conf.d/
+
[[ -z "$regen_conf_files" ]] \
- || sudo service metronome restart
+ || service metronome restart
}
FORCE=${2:-0}
diff --git a/data/hooks/conf_regen/15-nginx b/data/hooks/conf_regen/15-nginx
index 59654a771..86c7c2438 100755
--- a/data/hooks/conf_regen/15-nginx
+++ b/data/hooks/conf_regen/15-nginx
@@ -23,10 +23,12 @@ do_init_regen() {
rm -f "${nginx_dir}/sites-enabled/default"
export compatibility="intermediate"
+ ynh_render_template "security.conf.inc" "${nginx_conf_dir}/security.conf.inc"
ynh_render_template "yunohost_admin.conf" "${nginx_conf_dir}/yunohost_admin.conf"
# Restart nginx if conf looks good, otherwise display error and exit unhappy
- nginx -t 2>/dev/null && service nginx restart || (nginx -t && exit 1)
+ nginx -t 2>/dev/null || { nginx -t; exit 1; }
+ systemctl restart nginx || { journalctl --no-pager --lines=10 -u nginx >&2; exit 1; }
exit 0
}
@@ -45,10 +47,11 @@ do_pre_regen() {
# retrieve variables
main_domain=$(cat /etc/yunohost/current_host)
- domain_list=$(sudo yunohost domain list --output-as plain --quiet)
+ domain_list=$(yunohost domain list --output-as plain --quiet)
# Support different strategy for security configurations
export compatibility="$(yunohost settings get 'security.nginx.compatibility')"
+ ynh_render_template "security.conf.inc" "${nginx_conf_dir}/security.conf.inc"
# add domain conf files
for domain in $domain_list; do
@@ -102,15 +105,30 @@ do_post_regen() {
[ -z "$regen_conf_files" ] && exit 0
# retrieve variables
- domain_list=$(sudo yunohost domain list --output-as plain --quiet)
+ domain_list=$(yunohost domain list --output-as plain --quiet)
# create NGINX conf directories for domains
for domain in $domain_list; do
- sudo mkdir -p "/etc/nginx/conf.d/${domain}.d"
+ mkdir -p "/etc/nginx/conf.d/${domain}.d"
done
- # Reload nginx configuration
- pgrep nginx && sudo service nginx reload
+ # Get rid of legacy lets encrypt snippets
+ for domain in $domain_list; do
+ # If the legacy letsencrypt / acme-challenge domain-specific snippet is still there
+ if [ -e /etc/nginx/conf.d/${domain}.d/000-acmechallenge.conf ]
+ then
+ # And if we're effectively including the new domain-independant snippet now
+ if grep -q "include /etc/nginx/conf.d/acme-challenge.conf.inc;" /etc/nginx/conf.d/${domain}.conf
+ then
+ # Delete the old domain-specific snippet
+ rm /etc/nginx/conf.d/${domain}.d/000-acmechallenge.conf
+ fi
+ fi
+ done
+
+ # Reload nginx if conf looks good, otherwise display error and exit unhappy
+ nginx -t 2>/dev/null || { nginx -t; exit 1; }
+ pgrep nginx && systemctl reload nginx || { journalctl --no-pager --lines=10 -u nginx >&2; exit 1; }
}
FORCE=${2:-0}
diff --git a/data/hooks/conf_regen/19-postfix b/data/hooks/conf_regen/19-postfix
index b37425984..10076b680 100755
--- a/data/hooks/conf_regen/19-postfix
+++ b/data/hooks/conf_regen/19-postfix
@@ -20,7 +20,7 @@ do_pre_regen() {
# prepare main.cf conf file
main_domain=$(cat /etc/yunohost/current_host)
- domain_list=$(sudo yunohost domain list --output-as plain --quiet | tr '\n' ' ')
+ domain_list=$(yunohost domain list --output-as plain --quiet | tr '\n' ' ')
# Support different strategy for security configurations
export compatibility="$(yunohost settings get 'security.postfix.compatibility')"
@@ -35,7 +35,8 @@ do_pre_regen() {
> "${default_dir}/postsrsd"
# adapt it for IPv4-only hosts
- if [ ! -f /proc/net/if_inet6 ]; then
+ ipv6="$(yunohost settings get 'smtp.allow_ipv6')"
+ if [ "$ipv6" == "False" ] || [ ! -f /proc/net/if_inet6 ]; then
sed -i \
's/ \[::ffff:127.0.0.0\]\/104 \[::1\]\/128//g' \
"${postfix_dir}/main.cf"
@@ -49,7 +50,7 @@ do_post_regen() {
regen_conf_files=$1
[[ -z "$regen_conf_files" ]] \
- || { sudo service postfix restart && sudo service postsrsd restart; }
+ || { service postfix restart && service postsrsd restart; }
}
diff --git a/data/hooks/conf_regen/25-dovecot b/data/hooks/conf_regen/25-dovecot
index 4c5ae24c1..46c9bdf3e 100755
--- a/data/hooks/conf_regen/25-dovecot
+++ b/data/hooks/conf_regen/25-dovecot
@@ -2,6 +2,8 @@
set -e
+. /usr/share/yunohost/helpers
+
do_pre_regen() {
pending_dir=$1
@@ -14,11 +16,10 @@ do_pre_regen() {
cp dovecot-ldap.conf "${dovecot_dir}/dovecot-ldap.conf"
cp dovecot.sieve "${dovecot_dir}/global_script/dovecot.sieve"
- # prepare dovecot.conf conf file
- main_domain=$(cat /etc/yunohost/current_host)
- cat dovecot.conf \
- | sed "s/{{ main_domain }}/${main_domain}/g" \
- > "${dovecot_dir}/dovecot.conf"
+ export pop3_enabled="$(yunohost settings get 'pop3.enabled')"
+ export main_domain=$(cat /etc/yunohost/current_host)
+
+ ynh_render_template "dovecot.conf" "${dovecot_dir}/dovecot.conf"
# adapt it for IPv4-only hosts
if [ ! -f /proc/net/if_inet6 ]; then
@@ -35,28 +36,28 @@ do_pre_regen() {
do_post_regen() {
regen_conf_files=$1
- sudo mkdir -p "/etc/dovecot/yunohost.d/pre-ext.d"
- sudo mkdir -p "/etc/dovecot/yunohost.d/post-ext.d"
+ mkdir -p "/etc/dovecot/yunohost.d/pre-ext.d"
+ mkdir -p "/etc/dovecot/yunohost.d/post-ext.d"
# create vmail user
id vmail > /dev/null 2>&1 \
- || sudo adduser --system --ingroup mail --uid 500 vmail
+ || adduser --system --ingroup mail --uid 500 vmail
# fix permissions
- sudo chown -R vmail:mail /etc/dovecot/global_script
- sudo chmod 770 /etc/dovecot/global_script
- sudo chown root:mail /var/mail
- sudo chmod 1775 /var/mail
+ chown -R vmail:mail /etc/dovecot/global_script
+ chmod 770 /etc/dovecot/global_script
+ chown root:mail /var/mail
+ chmod 1775 /var/mail
[ -z "$regen_conf_files" ] && exit 0
# compile sieve script
[[ "$regen_conf_files" =~ dovecot\.sieve ]] && {
- sudo sievec /etc/dovecot/global_script/dovecot.sieve
- sudo chown -R vmail:mail /etc/dovecot/global_script
+ sievec /etc/dovecot/global_script/dovecot.sieve
+ chown -R vmail:mail /etc/dovecot/global_script
}
- sudo service dovecot restart
+ service dovecot restart
}
FORCE=${2:-0}
diff --git a/data/hooks/conf_regen/31-rspamd b/data/hooks/conf_regen/31-rspamd
index d263d9cc9..26fea4336 100755
--- a/data/hooks/conf_regen/31-rspamd
+++ b/data/hooks/conf_regen/31-rspamd
@@ -22,11 +22,11 @@ do_post_regen() {
##
# create DKIM directory with proper permission
- sudo mkdir -p /etc/dkim
- sudo chown _rspamd /etc/dkim
+ mkdir -p /etc/dkim
+ chown _rspamd /etc/dkim
# retrieve domain list
- domain_list=$(sudo yunohost domain list --output-as plain --quiet)
+ domain_list=$(yunohost domain list --output-as plain --quiet)
# create DKIM key for domains
for domain in $domain_list; do
@@ -34,30 +34,30 @@ do_post_regen() {
[ ! -f "$domain_key" ] && {
# We use a 1024 bit size because nsupdate doesn't seem to be able to
# handle 2048...
- sudo opendkim-genkey --domain="$domain" \
+ opendkim-genkey --domain="$domain" \
--selector=mail --directory=/etc/dkim -b 1024
- sudo mv /etc/dkim/mail.private "$domain_key"
- sudo mv /etc/dkim/mail.txt "/etc/dkim/${domain}.mail.txt"
+ mv /etc/dkim/mail.private "$domain_key"
+ mv /etc/dkim/mail.txt "/etc/dkim/${domain}.mail.txt"
}
done
# fix DKIM keys permissions
- sudo chown _rspamd /etc/dkim/*.mail.key
- sudo chmod 400 /etc/dkim/*.mail.key
+ chown _rspamd /etc/dkim/*.mail.key
+ chmod 400 /etc/dkim/*.mail.key
regen_conf_files=$1
[ -z "$regen_conf_files" ] && exit 0
# compile sieve script
[[ "$regen_conf_files" =~ rspamd\.sieve ]] && {
- sudo sievec /etc/dovecot/global_script/rspamd.sieve
- sudo chown -R vmail:mail /etc/dovecot/global_script
- sudo systemctl restart dovecot
+ sievec /etc/dovecot/global_script/rspamd.sieve
+ chown -R vmail:mail /etc/dovecot/global_script
+ systemctl restart dovecot
}
# Restart rspamd due to the upgrade
# https://rspamd.com/announce/2016/08/01/rspamd-1.3.1.html
- sudo systemctl -q restart rspamd.service
+ systemctl -q restart rspamd.service
}
FORCE=${2:-0}
diff --git a/data/hooks/conf_regen/34-mysql b/data/hooks/conf_regen/34-mysql
index 8f7b5455e..43f9fdde1 100755
--- a/data/hooks/conf_regen/34-mysql
+++ b/data/hooks/conf_regen/34-mysql
@@ -18,12 +18,12 @@ do_post_regen() {
if [ ! -f /etc/yunohost/mysql ]; then
# ensure that mysql is running
- sudo systemctl -q is-active mysql.service \
- || sudo service mysql start
+ systemctl -q is-active mysql.service \
+ || service mysql start
# generate and set new root password
mysql_password=$(ynh_string_random 10)
- sudo mysqladmin -s -u root -pyunohost password "$mysql_password" || {
+ mysqladmin -s -u root -pyunohost password "$mysql_password" || {
if [ $FORCE -eq 1 ]; then
echo "It seems that you have already configured MySQL." \
"YunoHost needs to have a root access to MySQL to runs its" \
@@ -31,13 +31,13 @@ do_post_regen() {
"You can find this new password in /etc/yunohost/mysql." >&2
# set new password with debconf
- sudo debconf-set-selections << EOF
+ debconf-set-selections << EOF
$MYSQL_PKG mysql-server/root_password password $mysql_password
$MYSQL_PKG mysql-server/root_password_again password $mysql_password
EOF
# reconfigure Debian package
- sudo dpkg-reconfigure -freadline -u "$MYSQL_PKG" 2>&1
+ dpkg-reconfigure -freadline -u "$MYSQL_PKG" 2>&1
else
echo "It seems that you have already configured MySQL." \
"YunoHost needs to have a root access to MySQL to runs its" \
@@ -49,12 +49,12 @@ EOF
}
# store new root password
- echo "$mysql_password" | sudo tee /etc/yunohost/mysql
- sudo chmod 400 /etc/yunohost/mysql
+ echo "$mysql_password" | tee /etc/yunohost/mysql
+ chmod 400 /etc/yunohost/mysql
fi
[[ -z "$regen_conf_files" ]] \
- || sudo service mysql restart
+ || service mysql restart
}
FORCE=${2:-0}
diff --git a/data/hooks/conf_regen/37-avahi-daemon b/data/hooks/conf_regen/37-avahi-daemon
index 655a2e054..239c3ad0c 100755
--- a/data/hooks/conf_regen/37-avahi-daemon
+++ b/data/hooks/conf_regen/37-avahi-daemon
@@ -15,7 +15,7 @@ do_post_regen() {
regen_conf_files=$1
[[ -z "$regen_conf_files" ]] \
- || sudo service avahi-daemon restart
+ || service avahi-daemon restart
}
FORCE=${2:-0}
diff --git a/data/hooks/conf_regen/40-glances b/data/hooks/conf_regen/40-glances
index a19d35d56..70b8f4b5a 100755
--- a/data/hooks/conf_regen/40-glances
+++ b/data/hooks/conf_regen/40-glances
@@ -14,7 +14,7 @@ do_post_regen() {
regen_conf_files=$1
[[ -z "$regen_conf_files" ]] \
- || sudo service glances restart
+ || service glances restart
}
FORCE=${2:-0}
diff --git a/data/hooks/conf_regen/43-dnsmasq b/data/hooks/conf_regen/43-dnsmasq
index ed795c058..59a1f8a06 100755
--- a/data/hooks/conf_regen/43-dnsmasq
+++ b/data/hooks/conf_regen/43-dnsmasq
@@ -26,7 +26,7 @@ do_pre_regen() {
ynh_validate_ip4 "$ipv4" || ipv4='127.0.0.1'
ipv6=$(curl -s -6 https://ip6.yunohost.org 2>/dev/null || true)
ynh_validate_ip6 "$ipv6" || ipv6=''
- domain_list=$(sudo yunohost domain list --output-as plain --quiet)
+ domain_list=$(yunohost domain list --output-as plain --quiet)
# add domain conf files
for domain in $domain_list; do
@@ -50,8 +50,23 @@ do_pre_regen() {
do_post_regen() {
regen_conf_files=$1
+ # Fuck it, those domain/search entries from dhclient are usually annoying
+ # lying shit from the ISP trying to MiTM
+ if grep -q -E "^ *(domain|search)" /run/resolvconf/resolv.conf
+ then
+ if grep -q -E "^ *(domain|search)" /run/resolvconf/interface/*.dhclient 2>/dev/null
+ then
+ sed -E "s/^(domain|search)/#\1/g" -i /run/resolvconf/interface/*.dhclient
+ fi
+
+ grep -q '^supersede domain-name "";' /etc/dhcp/dhclient.conf 2>/dev/null || echo 'supersede domain-name "";' >> /etc/dhcp/dhclient.conf
+ grep -q '^supersede domain-search "";' /etc/dhcp/dhclient.conf 2>/dev/null || echo 'supersede domain-search "";' >> /etc/dhcp/dhclient.conf
+ grep -q '^supersede name "";' /etc/dhcp/dhclient.conf 2>/dev/null || echo 'supersede name "";' >> /etc/dhcp/dhclient.conf
+ systemctl restart resolvconf
+ fi
+
[[ -z "$regen_conf_files" ]] \
- || sudo service dnsmasq restart
+ || service dnsmasq restart
}
FORCE=${2:-0}
diff --git a/data/hooks/conf_regen/46-nsswitch b/data/hooks/conf_regen/46-nsswitch
index 06a596e44..fa9b07511 100755
--- a/data/hooks/conf_regen/46-nsswitch
+++ b/data/hooks/conf_regen/46-nsswitch
@@ -14,7 +14,7 @@ do_post_regen() {
regen_conf_files=$1
[[ -z "$regen_conf_files" ]] \
- || sudo service unscd restart
+ || service unscd restart
}
FORCE=${2:-0}
diff --git a/data/hooks/conf_regen/52-fail2ban b/data/hooks/conf_regen/52-fail2ban
index 950f27b5b..3cb499db7 100755
--- a/data/hooks/conf_regen/52-fail2ban
+++ b/data/hooks/conf_regen/52-fail2ban
@@ -20,7 +20,7 @@ do_post_regen() {
regen_conf_files=$1
[[ -z "$regen_conf_files" ]] \
- || sudo service fail2ban restart
+ || service fail2ban restart
}
FORCE=${2:-0}
diff --git a/data/hooks/diagnosis/00-basesystem.py b/data/hooks/diagnosis/00-basesystem.py
new file mode 100644
index 000000000..51926924a
--- /dev/null
+++ b/data/hooks/diagnosis/00-basesystem.py
@@ -0,0 +1,78 @@
+#!/usr/bin/env python
+
+import os
+
+from moulinette.utils.process import check_output
+from moulinette.utils.filesystem import read_file
+from yunohost.diagnosis import Diagnoser
+from yunohost.utils.packages import ynh_packages_version
+
+
+class BaseSystemDiagnoser(Diagnoser):
+
+ id_ = os.path.splitext(os.path.basename(__file__))[0].split("-")[1]
+ cache_duration = 600
+ dependencies = []
+
+ def run(self):
+
+ # Detect virt technology (if not bare metal) and arch
+ # Gotta have this "|| true" because it systemd-detect-virt return 'none'
+ # with an error code on bare metal ~.~
+ virt = check_output("systemd-detect-virt || true", shell=True).strip()
+ if virt.lower() == "none":
+ virt = "bare-metal"
+
+ # Detect arch
+ arch = check_output("dpkg --print-architecture").strip()
+ hardware = dict(meta={"test": "hardware"},
+ status="INFO",
+ data={"virt": virt, "arch": arch},
+ summary="diagnosis_basesystem_hardware")
+
+ # Also possibly the board name
+ if os.path.exists("/proc/device-tree/model"):
+ model = read_file('/proc/device-tree/model').strip()
+ hardware["data"]["model"] = model
+ hardware["details"] = ["diagnosis_basesystem_hardware_board"]
+
+ yield hardware
+
+ # Kernel version
+ kernel_version = read_file('/proc/sys/kernel/osrelease').strip()
+ yield dict(meta={"test": "kernel"},
+ data={"kernel_version": kernel_version},
+ status="INFO",
+ summary="diagnosis_basesystem_kernel")
+
+ # Debian release
+ debian_version = read_file("/etc/debian_version").strip()
+ yield dict(meta={"test": "host"},
+ data={"debian_version": debian_version},
+ status="INFO",
+ summary="diagnosis_basesystem_host")
+
+ # Yunohost packages versions
+ # We check if versions are consistent (e.g. all 3.6 and not 3 packages with 3.6 and the other with 3.5)
+ # This is a classical issue for upgrades that failed in the middle
+ # (or people upgrading half of the package because they did 'apt upgrade' instead of 'dist-upgrade')
+ # Here, ynh_core_version is for example "3.5.4.12", so [:3] is "3.5" and we check it's the same for all packages
+ ynh_packages = ynh_packages_version()
+ ynh_core_version = ynh_packages["yunohost"]["version"]
+ consistent_versions = all(infos["version"][:3] == ynh_core_version[:3] for infos in ynh_packages.values())
+ ynh_version_details = [("diagnosis_basesystem_ynh_single_version",
+ {"package":package,
+ "version": infos["version"],
+ "repo": infos["repo"]}
+ )
+ for package, infos in ynh_packages.items()]
+
+ yield dict(meta={"test": "ynh_versions"},
+ data={"main_version": ynh_core_version, "repo": ynh_packages["yunohost"]["repo"]},
+ status="INFO" if consistent_versions else "ERROR",
+ summary="diagnosis_basesystem_ynh_main_version" if consistent_versions else "diagnosis_basesystem_ynh_inconsistent_versions",
+ details=ynh_version_details)
+
+
+def main(args, env, loggers):
+ return BaseSystemDiagnoser(args, env, loggers).diagnose()
diff --git a/data/hooks/diagnosis/10-ip.py b/data/hooks/diagnosis/10-ip.py
new file mode 100644
index 000000000..c0d35278c
--- /dev/null
+++ b/data/hooks/diagnosis/10-ip.py
@@ -0,0 +1,159 @@
+#!/usr/bin/env python
+
+import os
+import random
+
+from moulinette.utils.network import download_text
+from moulinette.utils.process import check_output
+from moulinette.utils.filesystem import read_file
+
+from yunohost.diagnosis import Diagnoser
+from yunohost.utils.network import get_network_interfaces
+
+class IPDiagnoser(Diagnoser):
+
+ id_ = os.path.splitext(os.path.basename(__file__))[0].split("-")[1]
+ cache_duration = 600
+ dependencies = []
+
+ def run(self):
+
+ # ############################################################ #
+ # PING : Check that we can ping outside at least in ipv4 or v6 #
+ # ############################################################ #
+
+ can_ping_ipv4 = self.can_ping_outside(4)
+ can_ping_ipv6 = self.can_ping_outside(6)
+
+ if not can_ping_ipv4 and not can_ping_ipv6:
+ yield dict(meta={"test": "ping"},
+ status="ERROR",
+ summary="diagnosis_ip_not_connected_at_all")
+ # Not much else we can do if there's no internet at all
+ return
+
+ # ###################################################### #
+ # DNS RESOLUTION : Check that we can resolve domain name #
+ # (later needed to talk to ip. and ip6.yunohost.org) #
+ # ###################################################### #
+
+ can_resolve_dns = self.can_resolve_dns()
+
+ # In every case, we can check that resolvconf seems to be okay
+ # (symlink managed by resolvconf service + pointing to dnsmasq)
+ good_resolvconf = self.good_resolvconf()
+
+ # If we can't resolve domain names at all, that's a pretty big issue ...
+ # If it turns out that at the same time, resolvconf is bad, that's probably
+ # the cause of this, so we use a different message in that case
+ if not can_resolve_dns:
+ yield dict(meta={"test": "dnsresolv"},
+ status="ERROR",
+ summary="diagnosis_ip_broken_dnsresolution" if good_resolvconf else "diagnosis_ip_broken_resolvconf")
+ return
+ # Otherwise, if the resolv conf is bad but we were able to resolve domain name,
+ # still warn that we're using a weird resolv conf ...
+ elif not good_resolvconf:
+ yield dict(meta={"test": "dnsresolv"},
+ status="WARNING",
+ summary="diagnosis_ip_weird_resolvconf",
+ details=["diagnosis_ip_weird_resolvconf_details"])
+ else:
+ yield dict(meta={"test": "dnsresolv"},
+ status="SUCCESS",
+ summary="diagnosis_ip_dnsresolution_working")
+
+ # ##################################################### #
+ # IP DIAGNOSIS : Check that we're actually able to talk #
+ # to a web server to fetch current IPv4 and v6 #
+ # ##################################################### #
+
+ ipv4 = self.get_public_ip(4) if can_ping_ipv4 else None
+ ipv6 = self.get_public_ip(6) if can_ping_ipv6 else None
+
+ network_interfaces = get_network_interfaces()
+ def get_local_ip(version):
+ local_ip = {iface:addr[version].split("/")[0]
+ for iface, addr in network_interfaces.items() if version in addr}
+ if not local_ip:
+ return None
+ elif len(local_ip):
+ return next(iter(local_ip.values()))
+ else:
+ return local_ip
+
+ yield dict(meta={"test": "ipv4"},
+ data={"global": ipv4, "local": get_local_ip("ipv4")},
+ status="SUCCESS" if ipv4 else "ERROR",
+ summary="diagnosis_ip_connected_ipv4" if ipv4 else "diagnosis_ip_no_ipv4",
+ details=["diagnosis_ip_global", "diagnosis_ip_local"] if ipv4 else None)
+
+ yield dict(meta={"test": "ipv6"},
+ data={"global": ipv6, "local": get_local_ip("ipv6")},
+ status="SUCCESS" if ipv6 else "WARNING",
+ summary="diagnosis_ip_connected_ipv6" if ipv6 else "diagnosis_ip_no_ipv6",
+ details=["diagnosis_ip_global", "diagnosis_ip_local"] if ipv6 else None)
+
+ # TODO / FIXME : add some attempt to detect ISP (using whois ?) ?
+
+ def can_ping_outside(self, protocol=4):
+
+ assert protocol in [4, 6], "Invalid protocol version, it should be either 4 or 6 and was '%s'" % repr(protocol)
+
+ # We can know that ipv6 is not available directly if this file does not exists
+ if protocol == 6 and not os.path.exists("/proc/net/if_inet6"):
+ return False
+
+ # If we are indeed connected in ipv4 or ipv6, we should find a default route
+ routes = check_output("ip -%s route" % protocol).split("\n")
+ if not any(r.startswith("default") for r in routes):
+ return False
+
+ # We use the resolver file as a list of well-known, trustable (ie not google ;)) IPs that we can ping
+ resolver_file = "/usr/share/yunohost/templates/dnsmasq/plain/resolv.dnsmasq.conf"
+ resolvers = [r.split(" ")[1] for r in read_file(resolver_file).split("\n") if r.startswith("nameserver")]
+
+ if protocol == 4:
+ resolvers = [r for r in resolvers if ":" not in r]
+ if protocol == 6:
+ resolvers = [r for r in resolvers if ":" in r]
+
+ assert resolvers != [], "Uhoh, need at least one IPv%s DNS resolver in %s ..." % (protocol, resolver_file)
+
+ # So let's try to ping the first 4~5 resolvers (shuffled)
+ # If we succesfully ping any of them, we conclude that we are indeed connected
+ def ping(protocol, target):
+ return os.system("ping%s -c1 -W 3 %s >/dev/null 2>/dev/null" % ("" if protocol == 4 else "6", target)) == 0
+
+ random.shuffle(resolvers)
+ return any(ping(protocol, resolver) for resolver in resolvers[:5])
+
+ def can_resolve_dns(self):
+ return os.system("dig +short ip.yunohost.org >/dev/null 2>/dev/null") == 0
+
+ def good_resolvconf(self):
+ content = read_file("/etc/resolv.conf").strip().split("\n")
+ # Ignore comments and empty lines
+ content = [l.strip() for l in content if l.strip() and not l.strip().startswith("#") and not l.strip().startswith("search")]
+ # We should only find a "nameserver 127.0.0.1"
+ return len(content) == 1 and content[0].split() == ["nameserver", "127.0.0.1"]
+
+ def get_public_ip(self, protocol=4):
+
+ # FIXME - TODO : here we assume that DNS resolution for ip.yunohost.org is working
+ # but if we want to be able to diagnose DNS resolution issues independently from
+ # internet connectivity, we gotta rely on fixed IPs first....
+
+ assert protocol in [4, 6], "Invalid protocol version, it should be either 4 or 6 and was '%s'" % repr(protocol)
+
+ url = 'https://ip%s.yunohost.org' % ('6' if protocol == 6 else '')
+
+ try:
+ return download_text(url, timeout=30).strip()
+ except Exception as e:
+ self.logger_debug("Could not get public IPv%s : %s" % (str(protocol), str(e)))
+ return None
+
+
+def main(args, env, loggers):
+ return IPDiagnoser(args, env, loggers).diagnose()
diff --git a/data/hooks/diagnosis/12-dnsrecords.py b/data/hooks/diagnosis/12-dnsrecords.py
new file mode 100644
index 000000000..53afb2c2d
--- /dev/null
+++ b/data/hooks/diagnosis/12-dnsrecords.py
@@ -0,0 +1,141 @@
+#!/usr/bin/env python
+
+import os
+
+from moulinette.utils.filesystem import read_file
+
+from yunohost.utils.network import dig
+from yunohost.diagnosis import Diagnoser
+from yunohost.domain import domain_list, _build_dns_conf, _get_maindomain
+
+
+class DNSRecordsDiagnoser(Diagnoser):
+
+ id_ = os.path.splitext(os.path.basename(__file__))[0].split("-")[1]
+ cache_duration = 600
+ dependencies = ["ip"]
+
+ def run(self):
+
+ resolvers = read_file("/etc/resolv.dnsmasq.conf").split("\n")
+ ipv4_resolvers = [r.split(" ")[1] for r in resolvers if r.startswith("nameserver") and ":" not in r]
+ # FIXME some day ... handle ipv4-only and ipv6-only servers. For now we assume we have at least ipv4
+ assert ipv4_resolvers != [], "Uhoh, need at least one IPv4 DNS resolver ..."
+
+ self.resolver = ipv4_resolvers[0]
+ main_domain = _get_maindomain()
+
+ all_domains = domain_list()["domains"]
+ for domain in all_domains:
+ self.logger_debug("Diagnosing DNS conf for %s" % domain)
+ is_subdomain = domain.split(".",1)[1] in all_domains
+ for report in self.check_domain(domain, domain == main_domain, is_subdomain=is_subdomain):
+ yield report
+
+ # FIXME : somewhere, should implement a check for reverse DNS ...
+
+ # FIXME / TODO : somewhere, could also implement a check for domain expiring soon
+
+ def check_domain(self, domain, is_main_domain, is_subdomain):
+
+ expected_configuration = _build_dns_conf(domain, include_empty_AAAA_if_no_ipv6=True)
+
+ categories = ["basic", "mail", "xmpp", "extra"]
+ # For subdomains, we only diagnosis A and AAAA records
+ if is_subdomain:
+ categories = ["basic"]
+
+ for category in categories:
+
+ records = expected_configuration[category]
+ discrepancies = []
+ results = {}
+
+ for r in records:
+ id_ = r["type"] + ":" + r["name"]
+ r["current"] = self.get_current_record(domain, r["name"], r["type"])
+ if r["value"] == "@":
+ r["value"] = domain + "."
+
+ if self.current_record_match_expected(r):
+ results[id_] = "OK"
+ else:
+ if r["current"] is None:
+ results[id_] = "MISSING"
+ discrepancies.append(("diagnosis_dns_missing_record", r))
+ else:
+ results[id_] = "WRONG"
+ discrepancies.append(("diagnosis_dns_discrepancy", r))
+
+
+ def its_important():
+ # Every mail DNS records are important for main domain
+ # For other domain, we only report it as a warning for now...
+ if is_main_domain and category == "mail":
+ return True
+ elif category == "basic":
+ # A bad or missing A record is critical ...
+ # And so is a wrong AAAA record
+ # (However, a missing AAAA record is acceptable)
+ if results["A:@"] != "OK" or results["AAAA:@"] == "WRONG":
+ return True
+
+ return False
+
+ if discrepancies:
+ status = "ERROR" if its_important() else "WARNING"
+ summary = "diagnosis_dns_bad_conf"
+ else:
+ status = "SUCCESS"
+ summary = "diagnosis_dns_good_conf"
+
+ output = dict(meta={"domain": domain, "category": category},
+ data=results,
+ status=status,
+ summary=summary)
+
+ if discrepancies:
+ output["details"] = ["diagnosis_dns_point_to_doc"] + discrepancies
+
+ yield output
+
+ def get_current_record(self, domain, name, type_):
+
+ query = "%s.%s" % (name, domain) if name != "@" else domain
+ success, answers = dig(query, type_, resolvers="force_external")
+
+ if success != "ok":
+ return None
+ else:
+ return answers[0] if len(answers) == 1 else answers
+
+ def current_record_match_expected(self, r):
+ if r["value"] is not None and r["current"] is None:
+ return False
+ if r["value"] is None and r["current"] is not None:
+ return False
+ elif isinstance(r["current"], list):
+ return False
+
+ if r["type"] == "TXT":
+ # Split expected/current
+ # from "v=DKIM1; k=rsa; p=hugekey;"
+ # to a set like {'v=DKIM1', 'k=rsa', 'p=...'}
+ expected = set(r["value"].strip(';" ').replace(";", " ").split())
+ current = set(r["current"].strip(';" ').replace(";", " ").split())
+
+ # For SPF, ignore parts starting by ip4: or ip6:
+ if r["name"] == "@":
+ current = {part for part in current if not part.startswith("ip4:") and not part.startswith("ip6:")}
+ return expected == current
+ elif r["type"] == "MX":
+ # For MX, we want to ignore the priority
+ expected = r["value"].split()[-1]
+ current = r["current"].split()[-1]
+ return expected == current
+ else:
+ return r["current"] == r["value"]
+
+
+def main(args, env, loggers):
+ return DNSRecordsDiagnoser(args, env, loggers).diagnose()
diff --git a/data/hooks/diagnosis/14-ports.py b/data/hooks/diagnosis/14-ports.py
new file mode 100644
index 000000000..bd68c60d6
--- /dev/null
+++ b/data/hooks/diagnosis/14-ports.py
@@ -0,0 +1,116 @@
+#!/usr/bin/env python
+
+import os
+
+from yunohost.diagnosis import Diagnoser
+from yunohost.utils.error import YunohostError
+from yunohost.service import _get_services
+
+class PortsDiagnoser(Diagnoser):
+
+ id_ = os.path.splitext(os.path.basename(__file__))[0].split("-")[1]
+ cache_duration = 600
+ dependencies = ["ip"]
+
+ def run(self):
+
+ # TODO: report a warning if port 53 or 5353 is exposed to the outside world...
+
+ # This dict is something like :
+ # { 80: "nginx",
+ # 25: "postfix",
+ # 443: "nginx"
+ # ... }
+ ports = {}
+ services = _get_services()
+ for service, infos in services.items():
+ for port in infos.get("needs_exposed_ports", []):
+ ports[port] = service
+
+ ipversions = []
+ ipv4 = Diagnoser.get_cached_report("ip", item={"test": "ipv4"}) or {}
+ if ipv4.get("status") == "SUCCESS":
+ ipversions.append(4)
+
+ # To be discussed: we could also make this check dependent on the
+ # existence of an AAAA record...
+ ipv6 = Diagnoser.get_cached_report("ip", item={"test": "ipv6"}) or {}
+ if ipv6.get("status") == "SUCCESS":
+ ipversions.append(6)
+
+ # Fetch test result for each relevant IP version
+ results = {}
+ for ipversion in ipversions:
+ try:
+ r = Diagnoser.remote_diagnosis('check-ports',
+ data={'ports': ports.keys()},
+ ipversion=ipversion)
+ results[ipversion] = r["ports"]
+ except Exception as e:
+ yield dict(meta={"reason": "remote_diagnosis_failed", "ipversion": ipversion},
+ data={"error": str(e)},
+ status="WARNING",
+ summary="diagnosis_ports_could_not_diagnose",
+ details=["diagnosis_ports_could_not_diagnose_details"])
+ continue
+
+ ipversions = results.keys()
+ if not ipversions:
+ return
+
+ for port, service in sorted(ports.items()):
+ port = str(port)
+ category = services[service].get("category", "[?]")
+
+ # If both IPv4 and IPv6 (if applicable) are good
+ if all(results[ipversion].get(port) is True for ipversion in ipversions):
+ yield dict(meta={"port": port},
+ data={"service": service, "category": category},
+ status="SUCCESS",
+ summary="diagnosis_ports_ok",
+ details=["diagnosis_ports_needed_by"])
+ # If both IPv4 and IPv6 (if applicable) are failed
+ elif all(results[ipversion].get(port) is not True for ipversion in ipversions):
+ yield dict(meta={"port": port},
+ data={"service": service, "category": category},
+ status="ERROR",
+ summary="diagnosis_ports_unreachable",
+ details=["diagnosis_ports_needed_by", "diagnosis_ports_forwarding_tip"])
+ # If only IPv4 is failed or only IPv6 is failed (if applicable)
+ else:
+ passed, failed = (4, 6) if results[4].get(port) is True else (6, 4)
+
+ # Failing in ipv4 is critical.
+ # If we failed in IPv6 but there's in fact no AAAA record
+ # It's an acceptable situation and we shall not report an
+ # error
+ # If any AAAA record is set, IPv6 is important...
+ def ipv6_is_important():
+ dnsrecords = Diagnoser.get_cached_report("dnsrecords") or {}
+ return any(record["data"]["AAAA:@"] in ["OK", "WRONG"] for record in dnsrecords.get("items", []))
+
+ if failed == 4 or ipv6_is_important():
+ yield dict(meta={"port": port},
+ data={"service": service, "category": category, "passed": passed, "failed": failed},
+ status="ERROR",
+ summary="diagnosis_ports_partially_unreachable",
+ details=["diagnosis_ports_needed_by", "diagnosis_ports_forwarding_tip"])
+ # So otherwise we report a success
+ # And in addition we report an info about the failure in IPv6
+ # *with a different meta* (important to avoid conflicts when
+ # fetching the other info...)
+ else:
+ yield dict(meta={"port": port},
+ data={"service": service, "category": category},
+ status="SUCCESS",
+ summary="diagnosis_ports_ok",
+ details=["diagnosis_ports_needed_by"])
+ yield dict(meta={"test": "ipv6", "port": port},
+ data={"service": service, "category": category, "passed": passed, "failed": failed},
+ status="INFO",
+ summary="diagnosis_ports_partially_unreachable",
+ details=["diagnosis_ports_needed_by", "diagnosis_ports_forwarding_tip"])
+
+
+def main(args, env, loggers):
+ return PortsDiagnoser(args, env, loggers).diagnose()
diff --git a/data/hooks/diagnosis/21-web.py b/data/hooks/diagnosis/21-web.py
new file mode 100644
index 000000000..c1f6d912a
--- /dev/null
+++ b/data/hooks/diagnosis/21-web.py
@@ -0,0 +1,166 @@
+#!/usr/bin/env python
+
+import os
+import random
+import requests
+
+from moulinette.utils.filesystem import read_file
+
+from yunohost.diagnosis import Diagnoser
+from yunohost.domain import domain_list
+from yunohost.utils.error import YunohostError
+
+DIAGNOSIS_SERVER = "diagnosis.yunohost.org"
+
+
+class WebDiagnoser(Diagnoser):
+
+ id_ = os.path.splitext(os.path.basename(__file__))[0].split("-")[1]
+ cache_duration = 600
+ dependencies = ["ip"]
+
+ def run(self):
+
+ all_domains = domain_list()["domains"]
+ domains_to_check = []
+ for domain in all_domains:
+
+ # If the diagnosis location ain't defined, can't do diagnosis,
+ # probably because nginx conf manually modified...
+ nginx_conf = "/etc/nginx/conf.d/%s.conf" % domain
+ if ".well-known/ynh-diagnosis/" not in read_file(nginx_conf):
+ yield dict(meta={"domain": domain},
+ status="WARNING",
+ summary="diagnosis_http_nginx_conf_not_up_to_date",
+ details=["diagnosis_http_nginx_conf_not_up_to_date_details"])
+ else:
+ domains_to_check.append(domain)
+
+ self.nonce = ''.join(random.choice("0123456789abcedf") for i in range(16))
+ os.system("rm -rf /tmp/.well-known/ynh-diagnosis/")
+ os.system("mkdir -p /tmp/.well-known/ynh-diagnosis/")
+ os.system("touch /tmp/.well-known/ynh-diagnosis/%s" % self.nonce)
+
+ if not domains_to_check:
+ return
+
+ # To perform hairpinning test, we gotta make sure that port forwarding
+ # is working and therefore we'll do it only if at least one ipv4 domain
+ # works.
+ self.do_hairpinning_test = False
+
+ ipversions = []
+ ipv4 = Diagnoser.get_cached_report("ip", item={"test": "ipv4"}) or {}
+ if ipv4.get("status") == "SUCCESS":
+ ipversions.append(4)
+
+ # To be discussed: we could also make this check dependent on the
+ # existence of an AAAA record...
+ ipv6 = Diagnoser.get_cached_report("ip", item={"test": "ipv6"}) or {}
+ if ipv6.get("status") == "SUCCESS":
+ ipversions.append(6)
+
+ for item in self.test_http(domains_to_check, ipversions):
+ yield item
+
+ # If at least one domain is correctly exposed to the outside,
+ # attempt to diagnose hairpinning situations. On network with
+ # hairpinning issues, the server may be correctly exposed on the
+ # outside, but from the outside, it will be as if the port forwarding
+ # was not configured... Hence, calling for example
+ # "curl --head the.global.ip" will simply timeout...
+ if self.do_hairpinning_test:
+ global_ipv4 = ipv4.get("data", {}).get("global", None)
+ if global_ipv4:
+ try:
+ requests.head("http://" + global_ipv4, timeout=5)
+ except requests.exceptions.Timeout:
+ yield dict(meta={"test": "hairpinning"},
+ status="WARNING",
+ summary="diagnosis_http_hairpinning_issue",
+ details=["diagnosis_http_hairpinning_issue_details"])
+ except:
+ # Well I dunno what to do if that's another exception
+ # type... That'll most probably *not* be an hairpinning
+ # issue but something else super weird ...
+ pass
+
+ def test_http(self, domains, ipversions):
+
+ results = {}
+ for ipversion in ipversions:
+ try:
+ r = Diagnoser.remote_diagnosis('check-http',
+ data={'domains': domains,
+ "nonce": self.nonce},
+ ipversion=ipversion)
+ results[ipversion] = r["http"]
+ except Exception as e:
+ yield dict(meta={"reason": "remote_diagnosis_failed", "ipversion": ipversion},
+ data={"error": str(e)},
+ status="WARNING",
+ summary="diagnosis_http_could_not_diagnose",
+ details=["diagnosis_http_could_not_diagnose_details"])
+ continue
+
+ ipversions = results.keys()
+ if not ipversions:
+ return
+
+ for domain in domains:
+
+ # If both IPv4 and IPv6 (if applicable) are good
+ if all(results[ipversion][domain]["status"] == "ok" for ipversion in ipversions):
+ if 4 in ipversions:
+ self.do_hairpinning_test = True
+ yield dict(meta={"domain": domain},
+ status="SUCCESS",
+ summary="diagnosis_http_ok")
+ # If both IPv4 and IPv6 (if applicable) are failed
+ elif all(results[ipversion][domain]["status"] != "ok" for ipversion in ipversions):
+ detail = results[4 if 4 in ipversions else 6][domain]["status"]
+ yield dict(meta={"domain": domain},
+ status="ERROR",
+ summary="diagnosis_http_unreachable",
+ details=[detail.replace("error_http_check", "diagnosis_http")])
+ # If only IPv4 is failed or only IPv6 is failed (if applicable)
+ else:
+ passed, failed = (4, 6) if results[4][domain]["status"] == "ok" else (6, 4)
+ detail = results[failed][domain]["status"]
+
+ # Failing in ipv4 is critical.
+ # If we failed in IPv6 but there's in fact no AAAA record
+ # It's an acceptable situation and we shall not report an
+ # error
+ def ipv6_is_important_for_this_domain():
+ dnsrecords = Diagnoser.get_cached_report("dnsrecords", item={"domain": domain, "category": "basic"}) or {}
+ AAAA_status = dnsrecords.get("data", {}).get("AAAA:@")
+
+ return AAAA_status in ["OK", "WRONG"]
+
+ if failed == 4 or ipv6_is_important_for_this_domain():
+ yield dict(meta={"domain": domain},
+ data={"passed": passed, "failed": failed},
+ status="ERROR",
+ summary="diagnosis_http_partially_unreachable",
+ details=[detail.replace("error_http_check", "diagnosis_http")])
+ # So otherwise we report a success (note that this info is
+ # later used to know that ACME challenge is doable)
+ #
+ # And in addition we report an info about the failure in IPv6
+ # *with a different meta* (important to avoid conflicts when
+ # fetching the other info...)
+ else:
+ self.do_hairpinning_test = True
+ yield dict(meta={"domain": domain},
+ status="SUCCESS",
+ summary="diagnosis_http_ok")
+ yield dict(meta={"test": "ipv6", "domain": domain},
+ data={"passed": passed, "failed": failed},
+ status="INFO",
+ summary="diagnosis_http_partially_unreachable",
+ details=[detail.replace("error_http_check", "diagnosis_http")])
+
+
+def main(args, env, loggers):
+ return WebDiagnoser(args, env, loggers).diagnose()
diff --git a/data/hooks/diagnosis/24-mail.py b/data/hooks/diagnosis/24-mail.py
new file mode 100644
index 000000000..a60b4f0d4
--- /dev/null
+++ b/data/hooks/diagnosis/24-mail.py
@@ -0,0 +1,238 @@
+#!/usr/bin/env python
+
+import os
+import dns.resolver
+import socket
+import re
+
+from subprocess import CalledProcessError
+
+from moulinette.utils.process import check_output
+from moulinette.utils.filesystem import read_yaml
+
+from yunohost.diagnosis import Diagnoser
+from yunohost.domain import _get_maindomain, domain_list
+from yunohost.settings import settings_get
+from yunohost.utils.network import dig
+
+DEFAULT_DNS_BLACKLIST = "/usr/share/yunohost/other/dnsbl_list.yml"
+
+
+class MailDiagnoser(Diagnoser):
+
+ id_ = os.path.splitext(os.path.basename(__file__))[0].split("-")[1]
+ cache_duration = 600
+ dependencies = ["ip"]
+
+ def run(self):
+
+ self.ehlo_domain = _get_maindomain()
+ self.mail_domains = domain_list()["domains"]
+ self.ipversions, self.ips = self.get_ips_checked()
+
+ # TODO Is a A/AAAA and MX Record ?
+ # TODO Are outgoing public IPs authorized to send mail by SPF ?
+ # TODO Validate DKIM and dmarc ?
+ # TODO check that the recent mail logs are not filled with thousand of email sending (unusual number of mail sent)
+ # TODO check for unusual failed sending attempt being refused in the logs ?
+ checks = ["check_outgoing_port_25", "check_ehlo", "check_fcrdns",
+ "check_blacklist", "check_queue"]
+ for check in checks:
+ self.logger_debug("Running " + check)
+ reports = list(getattr(self, check)())
+ for report in reports:
+ yield report
+ if not reports:
+ name = check[6:]
+ yield dict(meta={"test": "mail_" + name},
+ status="SUCCESS",
+ summary="diagnosis_mail_" + name + "_ok")
+
+
+ def check_outgoing_port_25(self):
+ """
+ Check outgoing port 25 is open and not blocked by router
+ This check is ran on IPs we could used to send mail.
+ """
+
+ for ipversion in self.ipversions:
+ cmd = '/bin/nc -{ipversion} -z -w2 yunohost.org 25'.format(ipversion=ipversion)
+ if os.system(cmd) != 0:
+ yield dict(meta={"test": "outgoing_port_25", "ipversion": ipversion},
+ data={},
+ status="ERROR",
+ summary="diagnosis_mail_outgoing_port_25_blocked",
+ details=["diagnosis_mail_outgoing_port_25_blocked_details",
+ "diagnosis_mail_outgoing_port_25_blocked_relay_vpn"])
+
+
+ def check_ehlo(self):
+ """
+ Check the server is reachable from outside and it's the good one
+ This check is ran on IPs we could used to send mail.
+ """
+
+ for ipversion in self.ipversions:
+ try:
+ r = Diagnoser.remote_diagnosis('check-smtp',
+ data={},
+ ipversion=ipversion)
+ except Exception as e:
+ yield dict(meta={"test": "mail_ehlo", "reason": "remote_server_failed",
+ "ipversion": ipversion},
+ data={"error": str(e)},
+ status="WARNING",
+ summary="diagnosis_mail_ehlo_could_not_diagnose",
+ details=["diagnosis_mail_ehlo_could_not_diagnose_details"])
+ continue
+
+ if r["status"] != "ok":
+ summary = r["status"].replace("error_smtp_", "diagnosis_mail_ehlo_")
+ yield dict(meta={"test": "mail_ehlo", "ipversion": ipversion},
+ data={},
+ status="ERROR",
+ summary=summary,
+ details=[summary + "_details"])
+ elif r["helo"] != self.ehlo_domain:
+ yield dict(meta={"test": "mail_ehlo", "ipversion": ipversion},
+ data={"wrong_ehlo": r["helo"], "right_ehlo": self.ehlo_domain},
+ status="ERROR",
+ summary="diagnosis_mail_ehlo_wrong",
+ details=["diagnosis_mail_ehlo_wrong_details"])
+
+
+ def check_fcrdns(self):
+ """
+ Check the reverse DNS is well defined by doing a Forward-confirmed
+ reverse DNS check
+ This check is ran on IPs we could used to send mail.
+ """
+
+ for ip in self.ips:
+ if ":" in ip:
+ ipversion = 6
+ details = ["diagnosis_mail_fcrdns_nok_details",
+ "diagnosis_mail_fcrdns_nok_alternatives_6"]
+ else:
+ ipversion = 4
+ details = ["diagnosis_mail_fcrdns_nok_details",
+ "diagnosis_mail_fcrdns_nok_alternatives_4"]
+
+ try:
+ rdns_domain, _, _ = socket.gethostbyaddr(ip)
+ except socket.herror:
+ yield dict(meta={"test": "mail_fcrdns", "ipversion": ipversion},
+ data={"ip": ip, "ehlo_domain": self.ehlo_domain},
+ status="ERROR",
+ summary="diagnosis_mail_fcrdns_dns_missing",
+ details=details)
+ continue
+ if rdns_domain != self.ehlo_domain:
+ details = ["diagnosis_mail_fcrdns_different_from_ehlo_domain_details"] + details
+ yield dict(meta={"test": "mail_fcrdns", "ipversion": ipversion},
+ data={"ip": ip,
+ "ehlo_domain": self.ehlo_domain,
+ "rdns_domain": rdns_domain},
+ status="ERROR",
+ summary="diagnosis_mail_fcrdns_different_from_ehlo_domain",
+ details=details)
+
+
+ def check_blacklist(self):
+ """
+ Check with dig onto blacklist DNS server
+ This check is ran on IPs and domains we could used to send mail.
+ """
+
+ dns_blacklists = read_yaml(DEFAULT_DNS_BLACKLIST)
+ for item in self.ips + self.mail_domains:
+ for blacklist in dns_blacklists:
+ item_type = "domain"
+ if ":" in item:
+ item_type = 'ipv6'
+ elif re.match(r'^\d+\.\d+\.\d+\.\d+$', item):
+ item_type = 'ipv4'
+
+ if not blacklist[item_type]:
+ continue
+
+ # Build the query for DNSBL
+ subdomain = item
+ if item_type != "domain":
+ rev = dns.reversename.from_address(item)
+ subdomain = str(rev.split(3)[0])
+ query = subdomain + '.' + blacklist['dns_server']
+
+ # Do the DNS Query
+ status, _ = dig(query, 'A')
+ if status != 'ok':
+ continue
+
+ # Try to get the reason
+ details = []
+ status, answers = dig(query, 'TXT')
+ reason = "-"
+ if status == 'ok':
+ reason = ', '.join(answers)
+ details.append("diagnosis_mail_blacklist_reason")
+
+ details.append("diagnosis_mail_blacklist_website")
+
+ yield dict(meta={"test": "mail_blacklist", "item": item,
+ "blacklist": blacklist["dns_server"]},
+ data={'blacklist_name': blacklist['name'],
+ 'blacklist_website': blacklist['website'],
+ 'reason': reason},
+ status="ERROR",
+ summary='diagnosis_mail_blacklist_listed_by',
+ details=details)
+
+ def check_queue(self):
+ """
+ Check mail queue is not filled with hundreds of email pending
+ """
+
+ command = 'postqueue -p | grep -v "Mail queue is empty" | grep -c "^[A-Z0-9]" || true'
+ try:
+ output = check_output(command).strip()
+ pending_emails = int(output)
+ except (ValueError, CalledProcessError) as e:
+ yield dict(meta={"test": "mail_queue"},
+ data={"error": str(e)},
+ status="ERROR",
+ summary="diagnosis_mail_queue_unavailable",
+ details="diagnosis_mail_queue_unavailable_details")
+ else:
+ if pending_emails > 100:
+ yield dict(meta={"test": "mail_queue"},
+ data={'nb_pending': pending_emails},
+ status="WARNING",
+ summary="diagnosis_mail_queue_too_big")
+ else:
+ yield dict(meta={"test": "mail_queue"},
+ data={'nb_pending': pending_emails},
+ status="SUCCESS",
+ summary="diagnosis_mail_queue_ok")
+
+
+ def get_ips_checked(self):
+ outgoing_ipversions = []
+ outgoing_ips = []
+ ipv4 = Diagnoser.get_cached_report("ip", {"test": "ipv4"}) or {}
+ if ipv4.get("status") == "SUCCESS":
+ outgoing_ipversions.append(4)
+ global_ipv4 = ipv4.get("data", {}).get("global", {})
+ if global_ipv4:
+ outgoing_ips.append(global_ipv4)
+
+ if settings_get("smtp.allow_ipv6"):
+ ipv6 = Diagnoser.get_cached_report("ip", {"test": "ipv6"}) or {}
+ if ipv6.get("status") == "SUCCESS":
+ outgoing_ipversions.append(6)
+ global_ipv6 = ipv6.get("data", {}).get("global", {})
+ if global_ipv6:
+ outgoing_ips.append(global_ipv6)
+ return (outgoing_ipversions, outgoing_ips)
+
+def main(args, env, loggers):
+ return MailDiagnoser(args, env, loggers).diagnose()
diff --git a/data/hooks/diagnosis/30-services.py b/data/hooks/diagnosis/30-services.py
new file mode 100644
index 000000000..6217d89d3
--- /dev/null
+++ b/data/hooks/diagnosis/30-services.py
@@ -0,0 +1,40 @@
+#!/usr/bin/env python
+
+import os
+
+from yunohost.diagnosis import Diagnoser
+from yunohost.service import service_status
+
+class ServicesDiagnoser(Diagnoser):
+
+ id_ = os.path.splitext(os.path.basename(__file__))[0].split("-")[1]
+ cache_duration = 300
+ dependencies = []
+
+ def run(self):
+
+ all_result = service_status()
+
+ for service, result in sorted(all_result.items()):
+
+ item = dict(meta={"service": service},
+ data={"status": result["status"], "configuration": result["configuration"]})
+
+ if result["status"] != "running":
+ item["status"] = "ERROR"
+ item["summary"] = "diagnosis_services_bad_status"
+ item["details"] = ["diagnosis_services_bad_status_tip"]
+
+ elif result["configuration"] == "broken":
+ item["status"] = "WARNING"
+ item["summary"] = "diagnosis_services_conf_broken"
+ item["details"] = result["configuration-details"]
+
+ else:
+ item["status"] = "SUCCESS"
+ item["summary"] = "diagnosis_services_running"
+
+ yield item
+
+def main(args, env, loggers):
+ return ServicesDiagnoser(args, env, loggers).diagnose()
diff --git a/data/hooks/diagnosis/50-systemresources.py b/data/hooks/diagnosis/50-systemresources.py
new file mode 100644
index 000000000..417b88ae7
--- /dev/null
+++ b/data/hooks/diagnosis/50-systemresources.py
@@ -0,0 +1,121 @@
+#!/usr/bin/env python
+import os
+import psutil
+
+from yunohost.diagnosis import Diagnoser
+
+class SystemResourcesDiagnoser(Diagnoser):
+
+ id_ = os.path.splitext(os.path.basename(__file__))[0].split("-")[1]
+ cache_duration = 300
+ dependencies = []
+
+ def run(self):
+
+ MB = 1024**2
+ GB = MB*1024
+
+ #
+ # RAM
+ #
+
+ ram = psutil.virtual_memory()
+ ram_available_percent = 100 * ram.available / ram.total
+ item = dict(meta={"test": "ram"},
+ data={"total": human_size(ram.total),
+ "available": human_size(ram.available),
+ "available_percent": round_(ram_available_percent)})
+
+ if ram.available < 100 * MB or ram_available_percent < 5:
+ item["status"] = "ERROR"
+ item["summary"] = "diagnosis_ram_verylow"
+ elif ram.available < 200 * MB or ram_available_percent < 10:
+ item["status"] = "WARNING"
+ item["summary"] = "diagnosis_ram_low"
+ else:
+ item["status"] = "SUCCESS"
+ item["summary"] = "diagnosis_ram_ok"
+ yield item
+
+ #
+ # Swap
+ #
+
+ swap = psutil.swap_memory()
+ item = dict(meta={"test": "swap"},
+ data={"total": human_size(swap.total), "recommended": "512 MiB"})
+ if swap.total <= 1 * MB:
+ item["status"] = "ERROR"
+ item["summary"] = "diagnosis_swap_none"
+ elif swap.total <= 512 * MB:
+ item["status"] = "WARNING"
+ item["summary"] = "diagnosis_swap_notsomuch"
+ else:
+ item["status"] = "SUCCESS"
+ item["summary"] = "diagnosis_swap_ok"
+ yield item
+
+ # FIXME : add a check that swapiness is low if swap is on a sdcard...
+
+ #
+ # Disks usage
+ #
+
+ disk_partitions = psutil.disk_partitions()
+
+ for disk_partition in disk_partitions:
+ device = disk_partition.device
+ mountpoint = disk_partition.mountpoint
+
+ usage = psutil.disk_usage(mountpoint)
+ free_percent = round_(100 - usage.percent)
+
+ item = dict(meta={"test": "diskusage", "mountpoint": mountpoint},
+ data={"device": device, "total": human_size(usage.total), "free": human_size(usage.free), "free_percent": free_percent})
+
+ # Special checks for /boot partition because they sometimes are
+ # pretty small and that's kind of okay... (for example on RPi)
+ if mountpoint.startswith("/boot"):
+ if usage.free < 10 * MB or free_percent < 10:
+ item["status"] = "ERROR"
+ item["summary"] = "diagnosis_diskusage_verylow"
+ elif usage.free < 20 * MB or free_percent < 20:
+ item["status"] = "WARNING"
+ item["summary"] = "diagnosis_diskusage_low"
+ else:
+ item["status"] = "SUCCESS"
+ item["summary"] = "diagnosis_diskusage_ok"
+ else:
+ if usage.free < 1 * GB or free_percent < 5:
+ item["status"] = "ERROR"
+ item["summary"] = "diagnosis_diskusage_verylow"
+ elif usage.free < 2 * GB or free_percent < 10:
+ item["status"] = "WARNING"
+ item["summary"] = "diagnosis_diskusage_low"
+ else:
+ item["status"] = "SUCCESS"
+ item["summary"] = "diagnosis_diskusage_ok"
+
+
+ yield item
+
+
+def human_size(bytes_):
+ # Adapted from https://stackoverflow.com/a/1094933
+ for unit in ['','ki','Mi','Gi','Ti','Pi','Ei','Zi']:
+ if abs(bytes_) < 1024.0:
+ return "%s %sB" % (round_(bytes_), unit)
+ bytes_ /= 1024.0
+ return "%s %sB" % (round_(bytes_), 'Yi')
+
+
+def round_(n):
+ # round_(22.124) -> 22
+ # round_(9.45) -> 9.4
+ n = round(n, 1)
+ if n > 10:
+ n = int(round(n))
+ return n
+
+def main(args, env, loggers):
+ return SystemResourcesDiagnoser(args, env, loggers).diagnose()
diff --git a/data/hooks/diagnosis/70-regenconf.py b/data/hooks/diagnosis/70-regenconf.py
new file mode 100644
index 000000000..0dff71b5b
--- /dev/null
+++ b/data/hooks/diagnosis/70-regenconf.py
@@ -0,0 +1,42 @@
+#!/usr/bin/env python
+
+import os
+
+import subprocess
+from yunohost.diagnosis import Diagnoser
+from yunohost.regenconf import _get_regenconf_infos, _calculate_hash
+
+
+class RegenconfDiagnoser(Diagnoser):
+
+ id_ = os.path.splitext(os.path.basename(__file__))[0].split("-")[1]
+ cache_duration = 300
+ dependencies = []
+
+ def run(self):
+
+ regenconf_modified_files = list(self.manually_modified_files())
+
+ if not regenconf_modified_files:
+ yield dict(meta={"test": "regenconf"},
+ status="SUCCESS",
+ summary="diagnosis_regenconf_allgood"
+ )
+ else:
+ for f in regenconf_modified_files:
+ yield dict(meta={"test": "regenconf", "category": f['category'], "file": f['path']},
+ status="WARNING",
+ summary="diagnosis_regenconf_manually_modified",
+ details=["diagnosis_regenconf_manually_modified_details"]
+ )
+
+ def manually_modified_files(self):
+
+ for category, infos in _get_regenconf_infos().items():
+ for path, hash_ in infos["conffiles"].items():
+ if hash_ != _calculate_hash(path):
+ yield {"path": path, "category": category}
+
+
+def main(args, env, loggers):
+ return RegenconfDiagnoser(args, env, loggers).diagnose()
diff --git a/data/hooks/diagnosis/90-security.py b/data/hooks/diagnosis/90-security.py
new file mode 100644
index 000000000..d281042b0
--- /dev/null
+++ b/data/hooks/diagnosis/90-security.py
@@ -0,0 +1,98 @@
+#!/usr/bin/env python
+
+import os
+import json
+import subprocess
+
+from yunohost.diagnosis import Diagnoser
+from moulinette.utils.filesystem import read_json, write_to_json
+
+
+class SecurityDiagnoser(Diagnoser):
+
+ id_ = os.path.splitext(os.path.basename(__file__))[0].split("-")[1]
+ cache_duration = 3600
+ dependencies = []
+
+ def run(self):
+
+ "CVE-2017-5754"
+
+ if self.is_vulnerable_to_meltdown():
+ yield dict(meta={"test": "meltdown"},
+ status="ERROR",
+ summary="diagnosis_security_vulnerable_to_meltdown",
+ details=["diagnosis_security_vulnerable_to_meltdown_details"]
+ )
+ else:
+ yield dict(meta={},
+ status="SUCCESS",
+ summary="diagnosis_security_all_good"
+ )
+
+
+ def is_vulnerable_to_meltdown(self):
+ # meltdown CVE: https://security-tracker.debian.org/tracker/CVE-2017-5754
+
+ # We use a cache file to avoid re-running the script so many times,
+ # which can be expensive (up to around 5 seconds on ARM)
+ # and make the admin appear to be slow (c.f. the calls to diagnosis
+ # from the webadmin)
+ #
+ # The cache is in /tmp and shall disappear upon reboot
+ # *or* we compare it to dpkg.log modification time
+ # such that it's re-ran if there was package upgrades
+ # (e.g. from yunohost)
+ cache_file = "/tmp/yunohost-meltdown-diagnosis"
+ dpkg_log = "/var/log/dpkg.log"
+ if os.path.exists(cache_file):
+ if not os.path.exists(dpkg_log) or os.path.getmtime(cache_file) > os.path.getmtime(dpkg_log):
+ self.logger_debug("Using cached results for meltdown checker, from %s" % cache_file)
+ return read_json(cache_file)[0]["VULNERABLE"]
+
+ # script taken from https://github.com/speed47/spectre-meltdown-checker
+ # script commit id is store directly in the script
+ SCRIPT_PATH = "/usr/lib/moulinette/yunohost/vendor/spectre-meltdown-checker/spectre-meltdown-checker.sh"
+
+ # '--variant 3' corresponds to Meltdown
+ # example output from the script:
+ # [{"NAME":"MELTDOWN","CVE":"CVE-2017-5754","VULNERABLE":false,"INFOS":"PTI mitigates the vulnerability"}]
+ try:
+ self.logger_debug("Running meltdown vulnerability checker")
+ call = subprocess.Popen("bash %s --batch json --variant 3" %
+ SCRIPT_PATH, shell=True,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+
+ # TODO / FIXME : here we are ignoring error messages ...
+ # in particular on RPi2 and other hardware, the script complains about
+ # "missing some kernel info (see -v), accuracy might be reduced"
+ # Dunno what to do about that but we probably don't want to harass
+ # users with this warning ...
+ output, err = call.communicate()
+ assert call.returncode in (0, 2, 3), "Return code: %s" % call.returncode
+
+ # If there are multiple lines, sounds like there was some messages
+ # in stdout that are not json >.> ... Try to get the actual json
+ # stuff which should be the last line
+ output = output.strip()
+ if "\n" in output:
+ self.logger_debug("Original meltdown checker output : %s" % output)
+ output = output.split("\n")[-1]
+
+ CVEs = json.loads(output)
+ assert len(CVEs) == 1
+ assert CVEs[0]["NAME"] == "MELTDOWN"
+ except Exception as e:
+ import traceback
+ traceback.print_exc()
+ self.logger_warning("Something wrong happened when trying to diagnose Meltdown vunerability, exception: %s" % e)
+ raise Exception("Command output for failed meltdown check: '%s'" % output)
+
+ self.logger_debug("Writing results from meltdown checker to cache file, %s" % cache_file)
+ write_to_json(cache_file, CVEs)
+ return CVEs[0]["VULNERABLE"]
+
+
+def main(args, env, loggers):
+ return SecurityDiagnoser(args, env, loggers).diagnose()
diff --git a/data/hooks/restore/05-conf_ldap b/data/hooks/restore/05-conf_ldap
index eb6824993..74093136d 100644
--- a/data/hooks/restore/05-conf_ldap
+++ b/data/hooks/restore/05-conf_ldap
@@ -5,7 +5,7 @@ if [[ $EUID -ne 0 ]]; then
# We need to execute this script as root, since the ldap
# service will be shut down during the operation (and sudo
# won't be available)
- sudo /bin/bash $(readlink -f $0) $1
+ /bin/bash $(readlink -f $0) $1
else
diff --git a/data/hooks/restore/08-conf_ssh b/data/hooks/restore/08-conf_ssh
index 0c0f9bf9b..4b69d1696 100644
--- a/data/hooks/restore/08-conf_ssh
+++ b/data/hooks/restore/08-conf_ssh
@@ -1,8 +1,8 @@
backup_dir="$1/conf/ssh"
if [ -d /etc/ssh/ ]; then
- sudo cp -a $backup_dir/. /etc/ssh
- sudo service ssh restart
+ cp -a $backup_dir/. /etc/ssh
+ service ssh restart
else
echo "SSH is not installed"
fi
diff --git a/data/hooks/restore/11-conf_ynh_mysql b/data/hooks/restore/11-conf_ynh_mysql
index 24cdb1e79..f54641d6f 100644
--- a/data/hooks/restore/11-conf_ynh_mysql
+++ b/data/hooks/restore/11-conf_ynh_mysql
@@ -9,15 +9,15 @@ service mysql status >/dev/null 2>&1 \
# retrieve current and new password
[ -f /etc/yunohost/mysql ] \
- && curr_pwd=$(sudo cat /etc/yunohost/mysql)
-new_pwd=$(sudo cat "${backup_dir}/root_pwd" || sudo cat "${backup_dir}/mysql")
+ && curr_pwd=$(cat /etc/yunohost/mysql)
+new_pwd=$(cat "${backup_dir}/root_pwd" || cat "${backup_dir}/mysql")
[ -z "$curr_pwd" ] && curr_pwd="yunohost"
[ -z "$new_pwd" ] && {
new_pwd=$(ynh_string_random 10)
}
# attempt to change it
-sudo mysqladmin -s -u root -p"$curr_pwd" password "$new_pwd" || {
+mysqladmin -s -u root -p"$curr_pwd" password "$new_pwd" || {
echo "It seems that you have already configured MySQL." \
"YunoHost needs to have a root access to MySQL to runs its" \
@@ -25,18 +25,18 @@ sudo mysqladmin -s -u root -p"$curr_pwd" password "$new_pwd" || {
"You can find this new password in /etc/yunohost/mysql." >&2
# set new password with debconf
- sudo debconf-set-selections << EOF
+ debconf-set-selections << EOF
$MYSQL_PKG mysql-server/root_password password $new_pwd
$MYSQL_PKG mysql-server/root_password_again password $new_pwd
EOF
# reconfigure Debian package
- sudo dpkg-reconfigure -freadline -u "$MYSQL_PKG" 2>&1
+ dpkg-reconfigure -freadline -u "$MYSQL_PKG" 2>&1
}
# store new root password
-echo "$new_pwd" | sudo tee /etc/yunohost/mysql
-sudo chmod 400 /etc/yunohost/mysql
+echo "$new_pwd" | tee /etc/yunohost/mysql
+chmod 400 /etc/yunohost/mysql
# reload the grant tables
-sudo mysqladmin -s -u root -p"$new_pwd" reload
+mysqladmin -s -u root -p"$new_pwd" reload
diff --git a/data/hooks/restore/14-conf_ssowat b/data/hooks/restore/14-conf_ssowat
index 01ac787ee..71a011488 100644
--- a/data/hooks/restore/14-conf_ssowat
+++ b/data/hooks/restore/14-conf_ssowat
@@ -1,3 +1,3 @@
backup_dir="$1/conf/ssowat"
-sudo cp -a $backup_dir/. /etc/ssowat
+cp -a $backup_dir/. /etc/ssowat
diff --git a/data/hooks/restore/17-data_home b/data/hooks/restore/17-data_home
index a7ba2733c..6226eab6d 100644
--- a/data/hooks/restore/17-data_home
+++ b/data/hooks/restore/17-data_home
@@ -1,3 +1,3 @@
backup_dir="$1/data/home"
-sudo cp -a $backup_dir/. /home
+cp -a $backup_dir/. /home
diff --git a/data/hooks/restore/20-conf_ynh_firewall b/data/hooks/restore/20-conf_ynh_firewall
index c0ee18818..1789aed1e 100644
--- a/data/hooks/restore/20-conf_ynh_firewall
+++ b/data/hooks/restore/20-conf_ynh_firewall
@@ -1,4 +1,4 @@
backup_dir="$1/conf/ynh/firewall"
-sudo cp -a $backup_dir/. /etc/yunohost
-sudo yunohost firewall reload
+cp -a $backup_dir/. /etc/yunohost
+yunohost firewall reload
diff --git a/data/hooks/restore/21-conf_ynh_certs b/data/hooks/restore/21-conf_ynh_certs
index d1eb532ed..983bfb5a1 100644
--- a/data/hooks/restore/21-conf_ynh_certs
+++ b/data/hooks/restore/21-conf_ynh_certs
@@ -1,8 +1,7 @@
backup_dir="$1/conf/ynh/certs"
-sudo mkdir -p /etc/yunohost/certs/
+mkdir -p /etc/yunohost/certs/
-sudo cp -a $backup_dir/. /etc/yunohost/certs/
-sudo yunohost app ssowatconf
-sudo service nginx reload
-sudo service metronome reload
+cp -a $backup_dir/. /etc/yunohost/certs/
+service nginx reload
+service metronome reload
diff --git a/data/hooks/restore/23-data_mail b/data/hooks/restore/23-data_mail
index 81b9b923f..f9fd6e699 100644
--- a/data/hooks/restore/23-data_mail
+++ b/data/hooks/restore/23-data_mail
@@ -1,8 +1,8 @@
backup_dir="$1/data/mail"
-sudo cp -a $backup_dir/. /var/mail/ || echo 'No mail found'
-sudo chown -R vmail:mail /var/mail/
+cp -a $backup_dir/. /var/mail/ || echo 'No mail found'
+chown -R vmail:mail /var/mail/
# Restart services to use migrated certs
-sudo service postfix restart
-sudo service dovecot restart
+service postfix restart
+service dovecot restart
diff --git a/data/hooks/restore/26-conf_xmpp b/data/hooks/restore/26-conf_xmpp
index 61692b316..a300a7268 100644
--- a/data/hooks/restore/26-conf_xmpp
+++ b/data/hooks/restore/26-conf_xmpp
@@ -1,7 +1,7 @@
backup_dir="$1/conf/xmpp"
-sudo cp -a $backup_dir/etc/. /etc/metronome
-sudo cp -a $backup_dir/var/. /var/lib/metronome
+cp -a $backup_dir/etc/. /etc/metronome
+cp -a $backup_dir/var/. /var/lib/metronome
# Restart to apply new conf and certs
-sudo service metronome restart
+service metronome restart
diff --git a/data/hooks/restore/29-conf_nginx b/data/hooks/restore/29-conf_nginx
index 0795f53df..7288f52f3 100644
--- a/data/hooks/restore/29-conf_nginx
+++ b/data/hooks/restore/29-conf_nginx
@@ -1,7 +1,7 @@
backup_dir="$1/conf/nginx"
# Copy all conf except apps specific conf located in DOMAIN.d
-sudo find $backup_dir/ -mindepth 1 -maxdepth 1 -name '*.d' -or -exec sudo cp -a {} /etc/nginx/conf.d/ \;
+find $backup_dir/ -mindepth 1 -maxdepth 1 -name '*.d' -or -exec cp -a {} /etc/nginx/conf.d/ \;
# Restart to use new conf and certs
-sudo service nginx restart
+service nginx restart
diff --git a/data/hooks/restore/32-conf_cron b/data/hooks/restore/32-conf_cron
index 68657963e..59a2bde61 100644
--- a/data/hooks/restore/32-conf_cron
+++ b/data/hooks/restore/32-conf_cron
@@ -1,6 +1,6 @@
backup_dir="$1/conf/cron"
-sudo cp -a $backup_dir/. /etc/cron.d
+cp -a $backup_dir/. /etc/cron.d
# Restart just in case
-sudo service cron restart
+service cron restart
diff --git a/data/hooks/restore/40-conf_ynh_currenthost b/data/hooks/restore/40-conf_ynh_currenthost
index a0bdf94d3..700e806b4 100644
--- a/data/hooks/restore/40-conf_ynh_currenthost
+++ b/data/hooks/restore/40-conf_ynh_currenthost
@@ -1,3 +1,3 @@
backup_dir="$1/conf/ynh"
-sudo cp -a "${backup_dir}/current_host" /etc/yunohost/current_host
+cp -a "${backup_dir}/current_host" /etc/yunohost/current_host
diff --git a/data/other/dnsbl_list.yml b/data/other/dnsbl_list.yml
new file mode 100644
index 000000000..839aeaab6
--- /dev/null
+++ b/data/other/dnsbl_list.yml
@@ -0,0 +1,184 @@
+# Used by GAFAM
+- name: Spamhaus ZEN
+ dns_server: zen.spamhaus.org
+ website: https://www.spamhaus.org/zen/
+ ipv4: true
+ ipv6: true
+ domain: false
+- name: Barracuda Reputation Block List
+ dns_server: b.barracudacentral.org
+ website: https://barracudacentral.org/rbl/
+ ipv4: true
+ ipv6: false
+ domain: false
+- name: Hostkarma
+ dns_server: hostkarma.junkemailfilter.com
+ website: https://ipadmin.junkemailfilter.com/remove.php
+ ipv4: true
+ ipv6: false
+ domain: false
+- name: ImproWare IP based spamlist
+ dns_server: spamrbl.imp.ch
+ website: https://antispam.imp.ch/
+ ipv4: true
+ ipv6: false
+ domain: false
+- name: ImproWare IP based wormlist
+ dns_server: wormrbl.imp.ch
+ website: https://antispam.imp.ch/
+ ipv4: true
+ ipv6: false
+ domain: false
+- name: Backscatterer.org
+ dns_server: ips.backscatterer.org
+ website: http://www.backscatterer.org/
+ ipv4: true
+ ipv6: false
+ domain: false
+- name: inps.de
+ dns_server: dnsbl.inps.de
+ website: http://dnsbl.inps.de/
+ ipv4: true
+ ipv6: false
+ domain: false
+- name: LASHBACK
+ dns_server: ubl.unsubscore.com
+ website: https://blacklist.lashback.com/
+ ipv4: true
+ ipv6: false
+ domain: false
+- name: Mailspike.org
+ dns_server: bl.mailspike.net
+ website: http://www.mailspike.net/
+ ipv4: true
+ ipv6: false
+ domain: false
+- name: NiX Spam
+ dns_server: ix.dnsbl.manitu.net
+ website: http://www.dnsbl.manitu.net/
+ ipv4: true
+ ipv6: false
+ domain: false
+- name: REDHAWK
+ dns_server: access.redhawk.org
+ website: https://www.redhawk.org/SpamHawk/query.php
+ ipv4: true
+ ipv6: false
+ domain: false
+- name: SORBS Open SMTP relays
+ dns_server: smtp.dnsbl.sorbs.net
+ website: http://www.sorbs.net/
+ ipv4: true
+ ipv6: false
+ domain: false
+- name: SORBS Spamhost (last 28 days)
+ dns_server: recent.spam.dnsbl.sorbs.net
+ website: http://www.sorbs.net/
+ ipv4: true
+ ipv6: false
+ domain: false
+- name: SORBS Spamhost (last 48 hours)
+ dns_server: new.spam.dnsbl.sorbs.net
+ website: http://www.sorbs.net/
+ ipv4: true
+ ipv6: false
+ domain: false
+- name: SpamCop Blocking List
+ dns_server: bl.spamcop.net
+ website: https://www.spamcop.net/bl.shtml
+ ipv4: true
+ ipv6: false
+ domain: false
+- name: Spam Eating Monkey SEM-BACKSCATTER
+ dns_server: backscatter.spameatingmonkey.net
+ website: https://spameatingmonkey.com/services
+ ipv4: true
+ ipv6: false
+ domain: false
+- name: Spam Eating Monkey SEM-BLACK
+ dns_server: bl.spameatingmonkey.net
+ website: https://spameatingmonkey.com/services
+ ipv4: true
+ ipv6: false
+ domain: false
+- name: Spam Eating Monkey SEM-IPV6BL
+ dns_server: bl.ipv6.spameatingmonkey.net
+ website: https://spameatingmonkey.com/services
+ ipv4: false
+ ipv6: true
+ domain: false
+- name: SpamRATS! all
+ dns_server: all.spamrats.com
+ website: http://www.spamrats.com/
+ ipv4: true
+ ipv6: false
+ domain: false
+- name: PSBL (Passive Spam Block List)
+ dns_server: psbl.surriel.com
+ website: http://psbl.surriel.com/
+ ipv4: true
+ ipv6: false
+ domain: false
+- name: SWINOG
+ dns_server: dnsrbl.swinog.ch
+ website: https://antispam.imp.ch/
+ ipv4: true
+ ipv6: false
+ domain: false
+- name: GBUdb Truncate
+ dns_server: truncate.gbudb.net
+ website: http://www.gbudb.com/truncate/index.jsp
+ ipv4: true
+ ipv6: false
+ domain: false
+- name: Weighted Private Block List
+ dns_server: db.wpbl.info
+ website: http://www.wpbl.info/
+ ipv4: true
+ ipv6: false
+ domain: false
+# Used by GAFAM
+- name: Composite Blocking List
+ dns_server: cbl.abuseat.org
+ website: cbl.abuseat.org
+ ipv4: true
+ ipv6: false
+ domain: false
+# Used by GAFAM
+- name: SenderScore Blacklist
+ dns_server: bl.score.senderscore.com
+ website: https://senderscore.com
+ ipv4: true
+ ipv6: false
+ domain: false
+- name: Invaluement
+ dns_server: sip.invaluement.com
+ website: https://www.invaluement.com/
+ ipv4: true
+ ipv6: false
+ domain: false
+# Added cause it supports IPv6
+- name: AntiCaptcha.NET IPv6
+ dns_server: dnsbl6.anticaptcha.net
+ website: http://anticaptcha.net/
+ ipv4: false
+ ipv6: true
+ domain: false
+- name: SPFBL.net RBL
+ dns_server: dnsbl.spfbl.net
+ website: https://spfbl.net/en/dnsbl/
+ ipv4: true
+ ipv6: true
+ domain: true
+- name: Suomispam Blacklist
+ dns_server: bl.suomispam.net
+ website: http://suomispam.net/
+ ipv4: true
+ ipv6: true
+ domain: false
+- name: NordSpam
+ dns_server: bl.nordspam.com
+ website: https://www.nordspam.com/
+ ipv4: true
+ ipv6: true
+ domain: false
diff --git a/data/other/ldap_scheme.yml b/data/other/ldap_scheme.yml
index 11504bbe8..660d6fbb5 100644
--- a/data/other/ldap_scheme.yml
+++ b/data/other/ldap_scheme.yml
@@ -57,18 +57,24 @@ children:
objectClass:
- posixGroup
- groupOfNamesYnh
+ cn=visitors,ou=groups:
+ cn: visitors
+ gidNumber: "4003"
+ objectClass:
+ - posixGroup
+ - groupOfNamesYnh
depends_children:
- cn=main.mail,ou=permission:
- cn: main.mail
+ cn=mail.main,ou=permission:
+ cn: mail.main
gidNumber: "5001"
objectClass:
- posixGroup
- permissionYnh
groupPermission:
- "cn=all_users,ou=groups,dc=yunohost,dc=org"
- cn=main.metronome,ou=permission:
- cn: main.metronome
+ cn=xmpp.main,ou=permission:
+ cn: xmpp.main
gidNumber: "5002"
objectClass:
- posixGroup
diff --git a/data/templates/dnsmasq/plain/resolv.dnsmasq.conf b/data/templates/dnsmasq/plain/resolv.dnsmasq.conf
index 6b3bb95d3..ce8515054 100644
--- a/data/templates/dnsmasq/plain/resolv.dnsmasq.conf
+++ b/data/templates/dnsmasq/plain/resolv.dnsmasq.conf
@@ -32,7 +32,6 @@ nameserver 85.214.20.141
nameserver 195.160.173.53
# (DE) AS250
nameserver 194.150.168.168
-nameserver 2001:4ce8::53
# (DE) Ideal-Hosting
nameserver 84.200.69.80
nameserver 2001:1608:10:25::1c04:b12f
diff --git a/data/templates/dovecot/dovecot-ldap.conf b/data/templates/dovecot/dovecot-ldap.conf
index c7c9785fd..3a80ba47f 100644
--- a/data/templates/dovecot/dovecot-ldap.conf
+++ b/data/templates/dovecot/dovecot-ldap.conf
@@ -3,7 +3,7 @@ auth_bind = yes
ldap_version = 3
base = ou=users,dc=yunohost,dc=org
user_attrs = uidNumber=500,gidNumber=8,mailuserquota=quota_rule=*:bytes=%$
-user_filter = (&(objectClass=inetOrgPerson)(uid=%n)(permission=cn=main.mail,ou=permission,dc=yunohost,dc=org))
-pass_filter = (&(objectClass=inetOrgPerson)(uid=%n)(permission=cn=main.mail,ou=permission,dc=yunohost,dc=org))
+user_filter = (&(objectClass=inetOrgPerson)(uid=%n)(permission=cn=mail.main,ou=permission,dc=yunohost,dc=org))
+pass_filter = (&(objectClass=inetOrgPerson)(uid=%n)(permission=cn=mail.main,ou=permission,dc=yunohost,dc=org))
default_pass_scheme = SSHA
diff --git a/data/templates/dovecot/dovecot.conf b/data/templates/dovecot/dovecot.conf
index 116bb2db7..8fc0e75ae 100644
--- a/data/templates/dovecot/dovecot.conf
+++ b/data/templates/dovecot/dovecot.conf
@@ -8,15 +8,29 @@ mail_home = /var/mail/%n
mail_location = maildir:/var/mail/%n
mail_uid = 500
-protocols = imap sieve
+protocols = imap sieve {% if pop3_enabled == "True" %}pop3{% endif %}
mail_plugins = $mail_plugins quota
+###############################################################################
+
+# generated 2020-04-03, Mozilla Guideline v5.4, Dovecot 2.2.27, OpenSSL 1.1.0l, intermediate configuration
+# https://ssl-config.mozilla.org/#server=dovecot&version=2.2.27&config=intermediate&openssl=1.1.0l&guideline=5.4
+
+ssl = required
-ssl = yes
ssl_cert = openssl dhparam -out /etc/ssl/private/dh2048.pem -outform PEM -2 2048
+#ssl_dhparam /etc/ssl/private/dh2048.pem;
+
+# Follows the Web Security Directives from the Mozilla Dev Lab and the Mozilla Obervatory + Partners
+# https://wiki.mozilla.org/Security/Guidelines/Web_Security
+# https://observatory.mozilla.org/
+more_set_headers "Content-Security-Policy : upgrade-insecure-requests";
+more_set_headers "Content-Security-Policy-Report-Only : default-src https: data: 'unsafe-inline' 'unsafe-eval'";
+more_set_headers "X-Content-Type-Options : nosniff";
+more_set_headers "X-XSS-Protection : 1; mode=block";
+more_set_headers "X-Download-Options : noopen";
+more_set_headers "X-Permitted-Cross-Domain-Policies : none";
+more_set_headers "X-Frame-Options : SAMEORIGIN";
+
+# Disable gzip to protect against BREACH
+# Read https://trac.nginx.org/nginx/ticket/1720 (text/html cannot be disabled!)
+gzip off;
diff --git a/data/templates/nginx/server.tpl.conf b/data/templates/nginx/server.tpl.conf
index 4a5e91557..29af9f532 100644
--- a/data/templates/nginx/server.tpl.conf
+++ b/data/templates/nginx/server.tpl.conf
@@ -6,17 +6,23 @@ map $http_upgrade $connection_upgrade {
server {
listen 80;
listen [::]:80;
- server_name {{ domain }};
+ server_name {{ domain }} xmpp-upload.{{ domain }};
access_by_lua_file /usr/share/ssowat/access.lua;
+ include /etc/nginx/conf.d/acme-challenge.conf.inc;
+
include /etc/nginx/conf.d/{{ domain }}.d/*.conf;
location /yunohost/admin {
return 301 https://$http_host$request_uri;
}
- location /.well-known/autoconfig/mail/ {
+ location ^~ '/.well-known/ynh-diagnosis/' {
+ alias /tmp/.well-known/ynh-diagnosis/;
+ }
+
+ location ^~ '/.well-known/autoconfig/mail/' {
alias /var/www/.well-known/{{ domain }}/autoconfig/mail/;
}
@@ -29,47 +35,14 @@ server {
listen [::]:443 ssl http2;
server_name {{ domain }};
+ include /etc/nginx/conf.d/security.conf.inc;
+
ssl_certificate /etc/yunohost/certs/{{ domain }}/crt.pem;
ssl_certificate_key /etc/yunohost/certs/{{ domain }}/key.pem;
- ssl_session_timeout 5m;
- ssl_session_cache shared:SSL:50m;
- {% if compatibility == "modern" %}
- # Ciphers with modern compatibility
- # https://mozilla.github.io/server-side-tls/ssl-config-generator/?server=nginx-1.6.2&openssl=1.0.1t&hsts=yes&profile=modern
- # The following configuration use modern ciphers, but remove compatibility with some old clients (android < 5.0, Internet Explorer < 10, ...)
- ssl_protocols TLSv1.2;
- ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256';
- ssl_prefer_server_ciphers on;
- {% else %}
- # As suggested by Mozilla : https://wiki.mozilla.org/Security/Server_Side_TLS and https://en.wikipedia.org/wiki/Curve25519
- ssl_ecdh_curve secp521r1:secp384r1:prime256v1;
- ssl_prefer_server_ciphers on;
-
- # Ciphers with intermediate compatibility
- # https://mozilla.github.io/server-side-tls/ssl-config-generator/?server=nginx-1.6.2&openssl=1.0.1t&hsts=yes&profile=intermediate
- ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
- ssl_ciphers 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS';
-
- # Uncomment the following directive after DH generation
- # > openssl dhparam -out /etc/ssl/private/dh2048.pem -outform PEM -2 2048
- #ssl_dhparam /etc/ssl/private/dh2048.pem;
- {% endif %}
-
- # Follows the Web Security Directives from the Mozilla Dev Lab and the Mozilla Obervatory + Partners
- # https://wiki.mozilla.org/Security/Guidelines/Web_Security
- # https://observatory.mozilla.org/
{% if domain_cert_ca != "Self-signed" %}
more_set_headers "Strict-Transport-Security : max-age=63072000; includeSubDomains; preload";
{% endif %}
- more_set_headers "Content-Security-Policy : upgrade-insecure-requests";
- more_set_headers "Content-Security-Policy-Report-Only : default-src https: data: 'unsafe-inline' 'unsafe-eval'";
- more_set_headers "X-Content-Type-Options : nosniff";
- more_set_headers "X-XSS-Protection : 1; mode=block";
- more_set_headers "X-Download-Options : noopen";
- more_set_headers "X-Permitted-Cross-Domain-Policies : none";
- more_set_headers "X-Frame-Options : SAMEORIGIN";
-
{% if domain_cert_ca == "Let's Encrypt" %}
# OCSP settings
ssl_stapling on;
@@ -79,9 +52,9 @@ server {
resolver_timeout 5s;
{% endif %}
- # Disable gzip to protect against BREACH
- # Read https://trac.nginx.org/nginx/ticket/1720 (text/html cannot be disabled!)
- gzip off;
+ location ^~ '/.well-known/autoconfig/mail/' {
+ alias /var/www/.well-known/{{ domain }}/autoconfig/mail/;
+ }
access_by_lua_file /usr/share/ssowat/access.lua;
@@ -93,3 +66,46 @@ server {
access_log /var/log/nginx/{{ domain }}-access.log;
error_log /var/log/nginx/{{ domain }}-error.log;
}
+
+# vhost dedicated to XMPP http_upload
+server {
+ listen 443 ssl http2;
+ listen [::]:443 ssl http2;
+ server_name xmpp-upload.{{ domain }};
+ root /dev/null;
+
+ location /upload/ {
+ alias /var/xmpp-upload/{{ domain }}/upload/;
+ # Pass all requests to metronome, except for GET and HEAD requests.
+ limit_except GET HEAD {
+ proxy_pass http://localhost:5290;
+ }
+
+ include proxy_params;
+ add_header 'Access-Control-Allow-Origin' '*';
+ add_header 'Access-Control-Allow-Methods' 'HEAD, GET, PUT, OPTIONS';
+ add_header 'Access-Control-Allow-Headers' 'Authorization';
+ add_header 'Access-Control-Allow-Credentials' 'true';
+ client_max_body_size 105M; # Choose a value a bit higher than the max upload configured in XMPP server
+ }
+
+ include /etc/nginx/conf.d/security.conf.inc;
+
+ ssl_certificate /etc/yunohost/certs/{{ domain }}/crt.pem;
+ ssl_certificate_key /etc/yunohost/certs/{{ domain }}/key.pem;
+
+ {% if domain_cert_ca != "Self-signed" %}
+ more_set_headers "Strict-Transport-Security : max-age=63072000; includeSubDomains; preload";
+ {% endif %}
+ {% if domain_cert_ca == "Let's Encrypt" %}
+ # OCSP settings
+ ssl_stapling on;
+ ssl_stapling_verify on;
+ ssl_trusted_certificate /etc/yunohost/certs/{{ domain }}/crt.pem;
+ resolver 127.0.0.1 127.0.1.1 valid=300s;
+ resolver_timeout 5s;
+ {% endif %}
+
+ access_log /var/log/nginx/xmpp-upload.{{ domain }}-access.log;
+ error_log /var/log/nginx/xmpp-upload.{{ domain }}-error.log;
+}
diff --git a/data/templates/nginx/yunohost_admin.conf b/data/templates/nginx/yunohost_admin.conf
index e0d9f6bb1..3df838c4a 100644
--- a/data/templates/nginx/yunohost_admin.conf
+++ b/data/templates/nginx/yunohost_admin.conf
@@ -15,48 +15,14 @@ server {
listen 443 ssl http2 default_server;
listen [::]:443 ssl http2 default_server;
+ include /etc/nginx/conf.d/security.conf.inc;
+
ssl_certificate /etc/yunohost/certs/yunohost.org/crt.pem;
ssl_certificate_key /etc/yunohost/certs/yunohost.org/key.pem;
- ssl_session_timeout 5m;
- ssl_session_cache shared:SSL:50m;
- {% if compatibility == "modern" %}
- # Ciphers with modern compatibility
- # https://mozilla.github.io/server-side-tls/ssl-config-generator/?server=nginx-1.6.2&openssl=1.0.1t&hsts=yes&profile=modern
- # Uncomment the following to use modern ciphers, but remove compatibility with some old clients (android < 5.0, Internet Explorer < 10, ...)
- ssl_protocols TLSv1.2;
- ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256';
- ssl_prefer_server_ciphers on;
- {% else %}
- # As suggested by Mozilla : https://wiki.mozilla.org/Security/Server_Side_TLS and https://en.wikipedia.org/wiki/Curve25519
- ssl_ecdh_curve secp521r1:secp384r1:prime256v1;
- ssl_prefer_server_ciphers on;
-
- # Ciphers with intermediate compatibility
- # https://mozilla.github.io/server-side-tls/ssl-config-generator/?server=nginx-1.6.2&openssl=1.0.1t&hsts=yes&profile=intermediate
- ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
- ssl_ciphers 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS';
-
- # Uncomment the following directive after DH generation
- # > openssl dhparam -out /etc/ssl/private/dh2048.pem -outform PEM -2 2048
- #ssl_dhparam /etc/ssl/private/dh2048.pem;
- {% endif %}
-
- # Follows the Web Security Directives from the Mozilla Dev Lab and the Mozilla Obervatory + Partners
- # https://wiki.mozilla.org/Security/Guidelines/Web_Security
- # https://observatory.mozilla.org/
more_set_headers "Strict-Transport-Security : max-age=63072000; includeSubDomains; preload";
more_set_headers "Referrer-Policy : 'same-origin'";
more_set_headers "Content-Security-Policy : upgrade-insecure-requests; object-src 'none'; script-src https: 'unsafe-eval'";
- more_set_headers "X-Content-Type-Options : nosniff";
- more_set_headers "X-XSS-Protection : 1; mode=block";
- more_set_headers "X-Download-Options : noopen";
- more_set_headers "X-Permitted-Cross-Domain-Policies : none";
- more_set_headers "X-Frame-Options : SAMEORIGIN";
-
- # Disable gzip to protect against BREACH
- # Read https://trac.nginx.org/nginx/ticket/1720 (text/html cannot be disabled!)
- gzip off;
location / {
return 302 https://$http_host/yunohost/admin;
diff --git a/data/templates/postfix/main.cf b/data/templates/postfix/main.cf
index 045b8edd0..61cbfa2e6 100644
--- a/data/templates/postfix/main.cf
+++ b/data/templates/postfix/main.cf
@@ -18,35 +18,45 @@ append_dot_mydomain = no
readme_directory = no
# -- TLS for incoming connections
-# By default, TLS is disabled in the Postfix SMTP server, so no difference to
-# plain Postfix is visible. Explicitly switch it on with "smtpd_tls_security_level = may".
-smtpd_tls_security_level=may
+###############################################################################
+# generated 2020-04-03, Mozilla Guideline v5.4, Postfix 3.1.14, OpenSSL 1.1.0l, intermediate configuration
+# https://ssl-config.mozilla.org/#server=postfix&version=3.1.14&config=intermediate&openssl=1.1.0l&guideline=5.4
-# Sending AUTH data over an unencrypted channel poses a security risk.
-# When TLS layer encryption is optional ("smtpd_tls_security_level = may"), it
-# may however still be useful to only offer AUTH when TLS is active. To maintain
-# compatibility with non-TLS clients, the default is to accept AUTH without
-# encryption. In order to change this behavior, we set "smtpd_tls_auth_only = yes".
-smtpd_tls_auth_only=yes
+# (No modern conf support until we're on buster...)
+# {% if compatibility == "intermediate" %} {% else %} {% endif %}
+
+smtpd_use_tls = yes
+
+smtpd_tls_security_level = may
+smtpd_tls_auth_only = yes
smtpd_tls_cert_file = /etc/yunohost/certs/{{ main_domain }}/crt.pem
smtpd_tls_key_file = /etc/yunohost/certs/{{ main_domain }}/key.pem
-smtpd_tls_exclude_ciphers = aNULL, MD5, DES, ADH, RC4, 3DES
-smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache
-smtpd_tls_loglevel=1
-{% if compatibility == "intermediate" %}
-smtpd_tls_mandatory_protocols=!SSLv2,!SSLv3
-{% else %}
-smtpd_tls_mandatory_protocols=!SSLv2,!SSLv3,!TLSv1,!TLSv1.1
-{% endif %}
+smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
+smtpd_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
+# smtpd_tls_mandatory_ciphers = medium # (c.f. below)
+
+# curl https://ssl-config.mozilla.org/ffdhe2048.txt > /path/to/dhparam.pem
+# not actually 1024 bits, this applies to all DHE >= 1024 bits
+# smtpd_tls_dh1024_param_file = /path/to/dhparam.pem
+
+# This custom medium cipherlist recommendation only works if we have a DH ... which we don't, c.f. https://github.com/YunoHost/issues/issues/93
+# On the other hand, the postfix doc strongly discourage tweaking this list ... So whatever, let's keep the mandatory_ciphers to high like we did before applying the Mozilla recommendation ...
+#tls_medium_cipherlist = ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
+tls_preempt_cipherlist = no
+
+# Custom Yunohost stuff ... because we can't use the recommendation about medium cipher list ...
smtpd_tls_mandatory_ciphers=high
smtpd_tls_eecdh_grade = ultra
+###############################################################################
+smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache
+smtpd_tls_loglevel=1
# -- TLS for outgoing connections
# Use TLS if this is supported by the remote SMTP server, otherwise use plaintext.
smtp_tls_security_level=may
smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache
-smtp_tls_exclude_ciphers = $smtpd_tls_exclude_ciphers
-smtp_tls_mandatory_ciphers= $smtpd_tls_mandatory_ciphers
+smtp_tls_exclude_ciphers = aNULL, MD5, DES, ADH, RC4, 3DES
+smtp_tls_mandatory_ciphers= high
smtp_tls_loglevel=1
# Configure Root CA certificates
@@ -167,4 +177,4 @@ default_destination_rate_delay = 5s
# By default it's possible to detect if the email adress exist
# So it's easly possible to scan a server to know which email adress is valid
# and after to send spam
-disable_vrfy_command = yes
\ No newline at end of file
+disable_vrfy_command = yes
diff --git a/data/templates/postfix/plain/ldap-accounts.cf b/data/templates/postfix/plain/ldap-accounts.cf
index 9f6f94e6d..75f38cf58 100644
--- a/data/templates/postfix/plain/ldap-accounts.cf
+++ b/data/templates/postfix/plain/ldap-accounts.cf
@@ -1,5 +1,5 @@
server_host = localhost
server_port = 389
search_base = dc=yunohost,dc=org
-query_filter = (&(objectClass=mailAccount)(mail=%s)(permission=cn=main.mail,ou=permission,dc=yunohost,dc=org))
+query_filter = (&(objectClass=mailAccount)(mail=%s)(permission=cn=mail.main,ou=permission,dc=yunohost,dc=org))
result_attribute = uid
diff --git a/data/templates/postfix/plain/ldap-aliases.cf b/data/templates/postfix/plain/ldap-aliases.cf
index 5e7d3a6c1..46563ae22 100644
--- a/data/templates/postfix/plain/ldap-aliases.cf
+++ b/data/templates/postfix/plain/ldap-aliases.cf
@@ -1,5 +1,5 @@
server_host = localhost
server_port = 389
search_base = dc=yunohost,dc=org
-query_filter = (&(objectClass=mailAccount)(mail=%s)(permission=cn=main.mail,ou=permission,dc=yunohost,dc=org))
+query_filter = (&(objectClass=mailAccount)(mail=%s)(permission=cn=mail.main,ou=permission,dc=yunohost,dc=org))
result_attribute = maildrop
diff --git a/data/templates/slapd/slapd.conf b/data/templates/slapd/slapd.conf
index 76f249060..0750b43aa 100644
--- a/data/templates/slapd/slapd.conf
+++ b/data/templates/slapd/slapd.conf
@@ -75,6 +75,7 @@ index cn,mail eq
index gidNumber,uidNumber eq
index member,memberUid,uniqueMember eq
index virtualdomain eq
+index permission eq
# Save the time that the entry gets modified, for database #1
lastmod on
diff --git a/data/templates/slapd/systemd-override.conf b/data/templates/slapd/systemd-override.conf
new file mode 100644
index 000000000..afa821bd4
--- /dev/null
+++ b/data/templates/slapd/systemd-override.conf
@@ -0,0 +1,9 @@
+[Service]
+# Prevent slapd from getting killed by oom reaper as much as possible
+OOMScoreAdjust=-1000
+# If slapd exited (for instance if got killed) the service should not be
+# considered as active anymore...
+RemainAfterExit=no
+# Automatically restart the service if the service gets down
+Restart=always
+RestartSec=3
diff --git a/data/templates/ssl/openssl.cnf b/data/templates/ssl/openssl.cnf
index fa5d19fa3..3ef7d80c3 100644
--- a/data/templates/ssl/openssl.cnf
+++ b/data/templates/ssl/openssl.cnf
@@ -192,7 +192,7 @@ authorityKeyIdentifier=keyid,issuer
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
-subjectAltName=DNS:yunohost.org,DNS:www.yunohost.org,DNS:ns.yunohost.org
+subjectAltName=DNS:yunohost.org,DNS:www.yunohost.org,DNS:ns.yunohost.org,DNS:xmpp-upload.yunohost.org
[ v3_ca ]
diff --git a/data/templates/yunohost/etckeeper.conf b/data/templates/yunohost/etckeeper.conf
deleted file mode 100644
index 2d11c3dc6..000000000
--- a/data/templates/yunohost/etckeeper.conf
+++ /dev/null
@@ -1,43 +0,0 @@
-# The VCS to use.
-#VCS="hg"
-VCS="git"
-#VCS="bzr"
-#VCS="darcs"
-
-# Options passed to git commit when run by etckeeper.
-GIT_COMMIT_OPTIONS="--quiet"
-
-# Options passed to hg commit when run by etckeeper.
-HG_COMMIT_OPTIONS=""
-
-# Options passed to bzr commit when run by etckeeper.
-BZR_COMMIT_OPTIONS=""
-
-# Options passed to darcs record when run by etckeeper.
-DARCS_COMMIT_OPTIONS="-a"
-
-# Uncomment to avoid etckeeper committing existing changes
-# to /etc automatically once per day.
-#AVOID_DAILY_AUTOCOMMITS=1
-
-# Uncomment the following to avoid special file warning
-# (the option is enabled automatically by cronjob regardless).
-#AVOID_SPECIAL_FILE_WARNING=1
-
-# Uncomment to avoid etckeeper committing existing changes to
-# /etc before installation. It will cancel the installation,
-# so you can commit the changes by hand.
-#AVOID_COMMIT_BEFORE_INSTALL=1
-
-# The high-level package manager that's being used.
-# (apt, pacman-g2, yum, zypper etc)
-HIGHLEVEL_PACKAGE_MANAGER=apt
-
-# The low-level package manager that's being used.
-# (dpkg, rpm, pacman, pacman-g2, etc)
-LOWLEVEL_PACKAGE_MANAGER=dpkg
-
-# To push each commit to a remote, put the name of the remote here.
-# (eg, "origin" for git). Space-separated lists of multiple remotes
-# also work (eg, "origin gitlab github" for git).
-PUSH_REMOTE=""
diff --git a/data/templates/yunohost/services.yml b/data/templates/yunohost/services.yml
index 0d79b182f..e1dd57e55 100644
--- a/data/templates/yunohost/services.yml
+++ b/data/templates/yunohost/services.yml
@@ -1,37 +1,56 @@
-nginx:
- log: /var/log/nginx
-avahi-daemon:
- log: /var/log/daemon.log
-dnsmasq:
- log: /var/log/daemon.log
-fail2ban:
- log: /var/log/fail2ban.log
+avahi-daemon: {}
+dnsmasq: {}
dovecot:
log: [/var/log/mail.log,/var/log/mail.err]
-postfix:
- log: [/var/log/mail.log,/var/log/mail.err]
-rspamd:
- log: /var/log/rspamd/rspamd.log
-redis-server:
- log: /var/log/redis/redis-server.log
-mysql:
- log: [/var/log/mysql.log,/var/log/mysql.err]
- alternates: ['mariadb']
-glances: {}
-ssh:
- log: /var/log/auth.log
+ needs_exposed_ports: [993]
+ category: email
+fail2ban:
+ log: /var/log/fail2ban.log
+ category: security
metronome:
log: [/var/log/metronome/metronome.log,/var/log/metronome/metronome.err]
-slapd:
- log: /var/log/syslog
+ needs_exposed_ports: [5222, 5269]
+ category: xmpp
+mysql:
+ log: [/var/log/mysql.log,/var/log/mysql.err,/var/log/mysql/error.log]
+ actual_systemd_service: mariadb
+ category: database
+nginx:
+ log: /var/log/nginx
+ test_conf: nginx -t
+ needs_exposed_ports: [80, 443]
+ category: web
+nslcd: {}
php7.0-fpm:
log: /var/log/php7.0-fpm.log
+ test_conf: php-fpm7.0 --test
+ category: web
+postfix:
+ log: [/var/log/mail.log,/var/log/mail.err]
+ actual_systemd_service: postfix@-
+ needs_exposed_ports: [25, 587]
+ category: email
+redis-server:
+ log: /var/log/redis/redis-server.log
+ category: database
+rspamd:
+ log: /var/log/rspamd/rspamd.log
+ category: email
+slapd:
+ category: database
+ssh:
+ log: /var/log/auth.log
+ test_conf: sshd -t
+ needs_exposed_ports: [22]
+ category: admin
yunohost-api:
log: /var/log/yunohost/yunohost-api.log
+ category: admin
yunohost-firewall:
need_lock: true
-nslcd:
- log: /var/log/syslog
+ test_status: iptables -S | grep "^-A INPUT" | grep " --dport" | grep -q ACCEPT
+ category: security
+glances: null
nsswitch: null
ssl: null
yunohost: null
diff --git a/debian/changelog b/debian/changelog
index 3eb347456..b3f607c9f 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,281 @@
+yunohost (3.8.1.1) testing; urgency=low
+
+ - [fix] Stupid issue about path in debian/install ...
+
+ -- Alexandre Aubin Sun, 19 Apr 2020 07:04:00 +0000
+
+yunohost (3.8.1) testing; urgency=low
+
+ ## Helpers (PHP, apt)
+
+ - New helpers for extra apt repo, PHP version install, and PHP fpm (#881, #928, #929)
+ - Pave the way to migration to php7.3 and future ones (#880, #926)
+ - Option in PHP helper to use a dedicated php service (#915)
+
+ ## Diagnosis
+
+ - Many improvements in diagnosis mechanism (#923, #921, #940)
+
+ ## Misc fixes, improvements
+ - custom_portal and custom_overlay redirect (#925)
+ - Improve systemd settings for slapd (#933)
+ - Spelling and typo corrections (#931)
+ - Improve translations for French, German, Catalan
+
+ Thanks to all contributors <3 ! (Kay0u, Maniack Crudelis, ljf, E.Gaspar,
+ xaloc33)
+
+ -- Alexandre Aubin Sun, 19 Apr 2020 06:20:00 +0000
+
+yunohost (3.8.0) testing; urgency=low
+
+ # Major stuff
+
+ - [enh] New diagnosis system (#534, #872, #919, a416044, a354425, 4ab3653, decb372, e686dc6, b5d18d6, 69bc124, 937d339, cc2288c, aaa9805, 526a3a2)
+ - [enh] App categories (#778, #853)
+ - [enh] Support XMPP http upload (#831)
+ - [enh] Many small improvements in the way we manage services (#838, fa5c0e9, dd92a34, c97a839)
+ - [enh] Add subcategories management in bash completion (#839)
+ - [mod] Add conflict with apache2 and bind9, other minor changes in Depends (#909, 3bd6a7a, 0a482fd)
+ - [enh] Setting to enable POP3 in email stack (#791)
+ - [enh] Better UX for CLI/API to change maindomain (#796)
+
+ # Misc technical
+
+ - Update ciphers for nginx, postfix and dovecot according to new Mozilla recommendation (#913, #914)
+ - Get rid of domain-specific acme-challenge snippet, use a single snippet included in every conf (#917)
+ - [enh] Persist cookies between multiple ynh_local_curl calls for the same app (#884, #903)
+ - [fix] ynh_find_port didn't detect port already used on UDP (#827, #907)
+ - [fix] prevent firefox to mix CA and server certificate (#857)
+ - [enh] add operation logger for config panel (#869)
+ - [fix] psql helpers: Revoke sessions before dropping tables (#895)
+ - [fix] moulinette logs were never displayed #lol (#758)
+
+ # Tests, cleaning, refactoring
+
+ - Add core CI, improve/fix tests (#856, #863, 6eb8efb, c4590ab, 711cc35, 6c24755)
+ - Refactoring (#805, 101d3be, #784)
+ - Drop some very-old deprecated app helpers (though still somewhat supporting them through hacky patching) (#780)
+ - Drop glances and the old monitoring system (#821)
+ - Drop app_debug (#824)
+ - Drop app's status.json (#834)
+ - Drop ynh_add_skipped/(un)protected_uris helpers (#910)
+ - Use a common security.conf.inc instead of having cipher setting in each nginx's domain file (1285776, 4d99cbe, be8427d, 22b9565)
+ - Don't add weird tmp redirected_urls after postinstall (#902)
+ - Don't do weird stuff with yunohost-firewall during debian's postinst (978d9d5)
+
+ # i18n, messaging
+
+ - Unit tests / lint / cleaning for translation files (#901)
+ - Improve message wording, spelling (8b0c9e5, 9fe43b1, f69ab4c, 0decb64, 986f38f, 8d40c73, 8fe343a, 1d84f17)
+ - Improve translations for French, Catalan, Bengali (Bangladesh), Italian, Dutch, Norwegian Bokmål, Chinese, Occitan, Spanish, Esperanto, German, Nepali, Portuguese, Arabic, Russian, Hungarian, Hindi, Polish, Greek
+
+ Thanks to all contributors <3 ! (Aeris One, Aleks, Allan N., Alvaro, Armando F., Arthur L., Augustin T., Bram, ButterflyOfFire, Damien P., Gustavo M., Jeroen F., Jimmy M., Josué, Kay0u, Maniack Crudelis, Mario, Matthew D., Mélanie C., Patrick B., Quentí, Yasss Gurl, amirale qt, Elie G., ljf, pitchum, Romain R., tituspijean, xaloc33, yalh76)
+
+ -- Kay0u Thu, 09 Apr 2020 19:59:18 +0000
+
+yunohost (3.7.1.2) stable; urgency=low
+
+ - [fix] Be more robust against some situation where some archives are corrupted
+ - [fix] Make nginx regen-conf more robust against broken config or service failing to start, show info to help debugging
+ - [fix] Force-flush the regen-conf for nginx domain conf when adding/removing a domain...
+ - [fix] app_map : Make sure to return / and not empty string for stuff on domain root
+ - [fix] Improve ynh_systemd_action to wait for fail2ban to reload
+ - [fix] Improper use of logger.exception in app.py leading to infamous weird "KeyError: label"
+
+ -- Alexandre Aubin Mon, 27 Apr 2020 23:50:00 +0000
+
+yunohost (3.7.1.1) stable; urgency=low
+
+ - [fix] lxc uid number is limited to 65536 by default (0c9a4509)
+ - [fix] also invalidate group cache when creating users (aaabf8c7)
+ - [fix] Make sure to have a path that include sbin for stupid cron jobs (f03bb82a)
+
+ -- Alexandre Aubin Sun, 12 Apr 2020 23:15:00 +0000
+
+yunohost (3.7.1) stable; urgency=low
+
+ - [enh] Add ynh_permission_has_user helper (#905)
+ - [mod] Change behavior of ynh_setting_delete to try to make migrating away from legacy permissions easier (#906)
+ - [fix] app_config_apply should also return 'app' info (#918)
+ - [fix] uid/gid conflicts in user_create because of inconsistent comparison (#924)
+ - [fix] Ensure metronome owns its directories (1f623830, 031f8a6e)
+ - [mod] Remove useless sudos in helpers (be88a283)
+ - [enh] Improve message wording for services (3c844292)
+ - [enh] Attempt to anonymize data pasted to paste.yunohost.org (f56f4724)
+ - [enh] Lazy load yunohost.certificate to possibly improve perfs (af8981e4)
+ - [fix] Improve logging / debugging (1eef9b67, 7d323814, d17fcaf9, 210d5f3f)
+
+ Thanks to all contributors <3 ! (Bram, Kay0u, Maniack, Matthew D.)
+
+ -- Alexandre Aubin Thu, 9 Apr 2020 14:52:00 +0000
+
+yunohost (3.7.0.12) stable; urgency=low
+
+ - Fix previous buggy hotfix about deleting existing primary groups ...
+
+ -- Alexandre Aubin Sat, 28 Mar 2020 14:52:00 +0000
+
+yunohost (3.7.0.11) stable; urgency=low
+
+ - [fix] Mess due to automatic translation tools ~_~
+
+ -- Kay0u Fri, 27 Mar 2020 23:49:45 +0000
+
+yunohost (3.7.0.10) stable; urgency=low
+
+ - [fix] On some weird setup, this folder and content ain't readable by group ... gotta make sure to make rx for group other slapd will explode
+
+ -- Alexandre Aubin Fri, 27 Mar 2020 21:45:00 +0000
+
+yunohost (3.7.0.9) stable; urgency=low
+
+ - [fix] Automatically remove existing system group if it exists when creating primary groups
+ - [fix] Require moulinette and ssowat to be at least 3.7 to avoid funky situations where regen-conf fails because moulinette ain't upgraded yet
+ - [i18n] Improve translations for Arabic, Bengali, Catalan, Chinese, Dutch, Esperanto, French, German, Greek, Hindi, Hungarian, Italian, Norwegian Bokmål, Occitan, Polish, Portuguese, Russian, Spanish
+
+ Thanks to all contributors <3 ! (Aeris One, Allan N., Alvaro, amirale qt, Armando F., ButterflyOfFire, Elie G., Gustavo M., Jeroen F., Kayou, Mario, Mélanie C., Patrick B., Quentí, tituspijean, xaloc33, yalh76, Yasss Gurl)
+
+ -- Alexandre Aubin Fri, 27 Mar 2020 21:00:00 +0000
+
+yunohost (3.7.0.8) stable; urgency=low
+
+ - [fix] App_setting delete add if the key doesn't exist
+
+ -- Kay0u Fri, 27 Mar 2020 00:36:46 +0000
+
+yunohost (3.7.0.7) stable; urgency=low
+
+ - [fix] Allow public apps with no sso tile (#894)
+ - [fix] Slapd now index permission to avoid log error
+
+ Thanks to all contributors <3 ! (Aleks, Kay0u)
+
+ -- Kay0u Thu, 26 Mar 2020 21:53:22 +0000
+
+yunohost (3.7.0.6) testing; urgency=low
+
+ - [fix] Make sure the group permission update contains unique elements
+
+ Thanks to all contributors <3 ! (Aleks)
+
+ -- Kay0u Sun, 15 Mar 2020 22:34:27 +0000
+
+yunohost (3.7.0.5) testing; urgency=low
+
+ - [fix] Permission url (#871)
+ - [fix] DNS resolver (#859)
+ - [fix] Legacy permission management (#868, #855)
+ - [enh] More informations in hooks permission (#877)
+
+ Thanks to all contributors <3 ! (Bram, ljf, Aleks, Josué, Maniack, Kay0u)
+
+ -- Kay0u Sun, 15 Mar 2020 15:07:24 +0000
+
+yunohost (3.7.0.4) testing; urgency=low
+
+ - [fix] Also add all_users when allowing visitors (#855)
+ - [fix] Fix handling of skipped_uris (c.f. also SSOwat#149)
+ - [i18n] Improve translations for Catalan
+
+ -- Alexandre Aubin Mon, 2 Dec 2019 20:44:00 +0000
+
+yunohost (3.7.0.3) testing; urgency=low
+
+ - [mod] Some refactoring for permissions create/update/reset (#837)
+ - [fix] Fix some edge cases for ynh_secure_remove and ynh_clean_check_starting
+ - [i18n] Improve translations for French, Catalan
+
+ -- Alexandre Aubin Sat, 23 Nov 2019 19:30:00 +0000
+
+yunohost (3.7.0.2) testing; urgency=low
+
+ - [fix] Make sure the users actually exists when migrating legacy custom permissions
+ - [mod] Move debug log dump from ynh_exit_properly to the core after failed app operation (#833)
+ - [enh] Improve app_upgrade error management (#832)
+ - [mod] Refactor group permission (#837)
+ - [enh] Add permission name in permission callback when adding/removing allowed users (#836)
+ - [enh] Improve permission helpers (#840)
+ - [i18n] Improve translations for German, Catalan, Swedish, Spanish, Turkish, Basque, French, Esperanto, Occitan
+
+ -- Alexandre Aubin Fri, 15 Nov 2019 16:45:00 +0000
+
+yunohost (3.7.0.1) testing; urgency=low
+
+ - Hotfix to avoid having a shitload of warnings displayed during the permission migration
+
+ -- Alexandre Aubin Thu, 31 Oct 2019 20:35:00 +0000
+
+yunohost (3.7.0) testing; urgency=low
+
+ # ~ Major stuff
+
+ - [enh] Add group and permission mechanism (YunoHost#585, YunoHost#763, YunoHost#789, YunoHost#790, YunoHost#795, YunoHost#797, SSOwat#147, Moulinette#189, YunoHost-admin#257)
+ - [mod] Rework migration system to have independent migrations (YunoHost#768, YunoHost#774, YunoHost-admin#258)
+ - [enh] Many improvements in the way app action failures are handled (YunoHost#769, YunoHost#811)
+ - [enh] Improve checks for system anomalies after app operations (YunoHost#785)
+ - [mod] Spookier warnings for dangerous app installs (YunoHost#814, Moulinette/808f620)
+ - [enh] Support app manifests in toml (YunoHost#748, Moulinette#204, Moulinette/55515cb)
+ - [mod] Get rid of etckeeper (YunoHost#803)
+ - [enh] Quite a lot of messages improvements, string cleaning, language rework... (YunoHost#793, YunoHost#799, YunoHost#823, SSOwat#143, YunoHost#766, YunoHost#767, YunoHost/fd99ef0, YunoHost/92a6315, YunoHost-admin/10ea04a, Moulinette/599bec3, Moulinette#208, Moulinette#213, Moulinette/b7d415d, Moulinette/a8966b8, Moulinette/fdf9a71, Moulinette/d895ae3, Moulinette/bdf0a1c, YunoHost#817, YunoHost#823, YunoHost/79627d7, YunoHost/9ee3d23, YunoHost-admin#265)
+ - [i18n] Improved translations for Catalan, Occitan, French, Esperanto, Arabic, German, Spanish, Norwegian Bokmål, Portuguese
+
+ # Smaller or pretty technical fix/enh
+
+ - [enh] Add unit/functional tests for apps + improve other tests (YunoHost#779, YunoHost#808)
+ - [enh] Preparations for moulinette Python3 migration (Tox, Pytest and unit tests) (Moulinette#203, Moulinette#206, Moulinette#207, Moulinette#210, Moulinette#211 Moulinette#212, Moulinette/2403ee1, Moulinette/69b0d49, Moulinette/49c749c, Moulinette/2c84ee1, Moulinette/cef72f7, YunoHost/6365a26)
+ - [enh] Support python hooks (YunoHost#747)
+ - [enh] Upgrade n version + compatibility with arm64 (YunoHost#753)
+ - [enh] Add OpenLDAP TLS support (YunoHost#755, YunoHost/0a2d1c7, YunoHost/2dc8095)
+ - [enh] Improve PostgreSQL password security (YunoHost#762)
+ - [enh] Integrate actions/config-panel into operation logs (YunoHost#764)
+ - [mod] Assume that apps without any 'path' setting defined aren't webapps (YunoHost#765)
+ - [fix] Set dpkg vendor to YunoHost (YunoHost#749, YunoHost#772)
+ - [enh] Adding variable 'token' to data to redact from logs (YunoHost#783)
+ - [enh] Add --force and --dry-run options to 'yunohost dyndns update' (YunoHost#786)
+ - [fix] Don't throw a fatal error if we can't change the hostname (YunoHost/fe3ecd7)
+ - [enh] Dynamically evaluate proper mariadb-server- (YunoHost/f0440fb)
+ - [fix] Bad format for backup info.json ... (YunoHost/7d0119a)
+ - [fix] Inline buttons responsiveness on migration screen (YunoHost-admin#259)
+ - [enh] Add debug logs to SSOwat (SSOwat#145)
+ - [enh] Add a write_to_yaml utility similar to write_to_json (Moulinette/2e2e627)
+ - [enh] Warn the user about long locks (Moulinette#205)
+ - [mod] Tweak stuff about setuptools and moulinette deps? (Moulinette/b739f27, Moulinette/da00fc9, Moulinette/d8cbbb0)
+ - [fix] Misc micro bugfixes or improvements (YunoHost#743, YunoHost#792, YunoHost/6f48d1d, YunoHost/d516cf8, YunoHost#819, Moulinette/83d9e77, YunoHost/63d364e, YunoHost/68e9724, YunoHost/0849adb, YunoHost/19dbe87, YunoHost/61931f2, YunoHost/6dc720f, YunoHost/4def4df, SSOwat#140, SSOwat#141, YunoHost#829)
+ - [doc] Fix doc building + add doc build tests with Tox (Moulinette/f1ac5b8, Moulinette/df7d478, Moulinette/74c8f79, Moulinette/bcf92c7, Moulinette/af2c80c, Moulinette/d52a574, Moulinette/307f660, Moulinette/dced104, Moulinette/ed3823b)
+ - [enh] READMEs improvements (YunoHost/b3398e7, SSOwat/ee67b6f, Moulinette/1541b74, Moulinette/ad1eeef, YunoHost/25afdd4, YunoHost/73741f6)
+
+ Thanks to all contributors <3 ! (accross all repo: Yunohost, Moulinette, SSOwat, Yunohost-admin) : advocatux, Aksel K., Aleks, Allan N., amirale qt, Armin P., Bram, ButterflyOfFire, Carles S. A., chema o. r., decentral1se, Emmanuel V., Etienne M., Filip B., Geoff M., htsr, Jibec, Josué, Julien J., Kayou, liberodark, ljf, lucaskev, Lukas D., madtibo, Martin D., Mélanie C., nr 458 h, pitfd, ppr, Quentí, sidddy, troll, tufek yamero, xaloc33, yalh76
+
+ -- Alexandre Aubin Thu, 31 Oct 2019 18:00:00 +0000
+
+yunohost (3.6.5.3) stable; urgency=low
+
+ - [fix] More general grep for the php/sury dependency nightmare fix (followup of #809)
+
+ -- Alexandre Aubin Tue, 29 Oct 2019 03:48:00 +0000
+
+yunohost (3.6.5.2) stable; urgency=low
+
+ - [fix] Alex was drunk and released an epic stupid bug in stable (2623d385)
+
+ -- Alexandre Aubin Thu, 10 Oct 2019 01:00:00 +0000
+
+yunohost (3.6.5.1) stable; urgency=low
+
+ - [mod] Change maxretry of fail2ban from 6 to 10 (fe8fd1b)
+
+ -- Alexandre Aubin Tue, 08 Oct 2019 20:00:00 +0000
+
+yunohost (3.6.5) stable; urgency=low
+
+ - [enh] Detect and warn early about unavailable full domains... (#798)
+ - [mod] Change maxretry of fail2ban from 6 to 10 (#802)
+ - [fix] Epicly ugly workaround for the goddamn dependency nighmare about sury fucking up php7.0 dependencies (#809)
+ - [fix] Support logfiles not ending with .log in logrotate ... (#810)
+
+ -- Alexandre Aubin Tue, 08 Oct 2019 19:00:00 +0000
+
yunohost (3.6.4.6) stable; urgency=low
- [fix] Hopefully fix the issue about corrupted logs metadata files (d507d447, 1cec9d78)
@@ -91,7 +369,7 @@ yunohost (3.6.1.2) testing; urgency=low
yunohost (3.6.1.1) testing; urgency=low
- [fix] Weird issue in slapd triggered by indexing uidNumber / gidNumber
-
+
-- Alexandre Aubin Tue, 04 Jun 2019 15:10:00 +0000
yunohost (3.6.1) testing; urgency=low
@@ -561,19 +839,19 @@ yunohost (3.0.0~beta1.2) testing; urgency=low
Removing http2 also from yunohost_admin.conf since there still are some
issues with wordpress ?
-
+
-- Alexandre Aubin Tue, 08 May 2018 05:52:00 +0000
yunohost (3.0.0~beta1.1) testing; urgency=low
Fixes in the postgresql migration
-
+
-- Alexandre Aubin Sun, 06 May 2018 03:06:00 +0000
yunohost (3.0.0~beta1) testing; urgency=low
Beta release for Stretch
-
+
-- Alexandre Aubin Thu, 03 May 2018 03:04:45 +0000
yunohost (2.7.14) stable; urgency=low
@@ -612,7 +890,7 @@ yunohost (2.7.13.4) testing; urgency=low
* Increase backup filename length
(Fixes by Bram <3)
-
+
-- Alexandre Aubin Tue, 05 Jun 2018 18:22:00 +0000
yunohost (2.7.13.3) testing; urgency=low
@@ -703,7 +981,7 @@ yunohost (2.7.11) testing; urgency=low
* [helpers] Allow for 'or' in dependencies (#381)
* [helpers] Tweak the usage of BACKUP_CORE_ONLY (#398)
* [helpers] Tweak systemd config helpers (optional service name and template name) (#425)
- * [i18n] Improve translations for Arabic, French, German, Occitan, Spanish
+ * [i18n] Improve translations for Arabic, French, German, Occitan, Spanish
Thanks to all contributors (ariasuni, ljf, JimboJoe, frju365, Maniack, J-B Lescher, Josue, Aleks, Bram, jibec) and the several translators (ButterflyOfFire, Eric G., Cedric, J. Keerl, beyercenter, P. Gatzka, Quenti, bjarkan) <3 !
@@ -794,11 +1072,11 @@ yunohost (2.7.3) testing; urgency=low
Major changes :
* [fix] Refactor/clean madness related to DynDNS (#353)
- * [i18n] Improve french translation (#355)
+ * [i18n] Improve french translation (#355)
* [fix] Use cryptorandom to generate password (#358)
* [enh] Support for single app upgrade from the webadmin (#359)
* [enh] Be able to give lock to son processes detached by systemctl (#367)
- * [enh] Make MySQL dumps with a single transaction to ensure backup consistency (#370)
+ * [enh] Make MySQL dumps with a single transaction to ensure backup consistency (#370)
Misc fixes/improvements :
@@ -806,7 +1084,7 @@ yunohost (2.7.3) testing; urgency=low
* [fix] Allow dash at the beginning of app settings value (#357)
* [enh] Handle root path in nginx conf (#361)
* [enh] Add debugging in ldap init (#365)
- * [fix] Fix app_upgrade_string with missing key
+ * [fix] Fix app_upgrade_string with missing key
* [fix] Fix for change_url path normalizing with root url (#368)
* [fix] Missing 'ask_path' string (#369)
* [enh] Remove date from sql dump (#371)
@@ -853,7 +1131,7 @@ yunohost (2.7.1) testing; urgency=low
* [fix] Make read-only mount bind actually read-only (#343) (ljf)
### dyndns
* Regen dnsmasq conf if it's not up to date :| (Alexandre Aubin)
- * [fix] timeout on request to avoid blocking process (Laurent Peuch)
+ * [fix] timeout on request to avoid blocking process (Laurent Peuch)
* Put request url in an intermediate variable (Alexandre Aubin)
### other
* clean users.py (Laurent Peuch)
@@ -1587,7 +1865,7 @@ yunohost (2.3.12) testing; urgency=low
* [fix] Check for tty in root_handlers before remove it in bin/yunohost
* [fix] Use dyndns.yunohost.org instead of dynhost.yunohost.org
* [fix] Set found private key and don't validate it in dyndns_update
- * [fix] Update first registered domain with DynDNS instead of current_host
+ * [fix] Update first registered domain with DynDNS instead of current_host
* [i18n] Rename app_requirements_failed err named variable
* [i18n] Update translations from Weblate
@@ -1595,7 +1873,7 @@ yunohost (2.3.12) testing; urgency=low
* [enh] Better message during service regenconf.
* [enh] Display hook path on error message.
* [enh] Use named arguments when calling m18n in service.py
- * [enh] Use named arguments with m18n.
+ * [enh] Use named arguments with m18n.
* [enh] Use named arguments for user_unknown string.
-- Jérôme Lebleu Sat, 09 Apr 2016 12:13:10 +0200
diff --git a/debian/control b/debian/control
index 64c7cd31d..5bcd78491 100644
--- a/debian/control
+++ b/debian/control
@@ -11,27 +11,28 @@ Package: yunohost
Essential: yes
Architecture: all
Depends: ${python:Depends}, ${misc:Depends}
- , moulinette (>= 2.7.1), ssowat (>= 2.7.1)
+ , moulinette (>= 3.7), ssowat (>= 3.7)
, python-psutil, python-requests, python-dnspython, python-openssl
, python-apt, python-miniupnpc, python-dbus, python-jinja2
, python-toml
- , glances, apt-transport-https
- , dnsutils, bind9utils, unzip, git, curl, cron, wget, jq
- , ca-certificates, netcat-openbsd, iproute
+ , apt, apt-transport-https
+ , nginx, nginx-extras (>=1.6.2)
+ , php-fpm, php-ldap, php-intl
, mariadb-server, php-mysql | php-mysqlnd
+ , openssh-server, iptables, fail2ban, dnsutils, bind9utils
+ , openssl, ca-certificates, netcat-openbsd, iproute2
, slapd, ldap-utils, sudo-ldap, libnss-ldapd, unscd, libpam-ldapd
- , postfix-ldap, postfix-policyd-spf-perl, postfix-pcre, procmail, mailutils, postsrsd
- , dovecot-ldap, dovecot-lmtpd, dovecot-managesieved
- , dovecot-antispam, fail2ban, iptables
- , nginx-extras (>=1.6.2), php-fpm, php-ldap, php-intl
- , dnsmasq, openssl, avahi-daemon, libnss-mdns, resolvconf, libnss-myhostname
+ , dnsmasq, avahi-daemon, libnss-mdns, resolvconf, libnss-myhostname
+ , postfix, postfix-ldap, postfix-policyd-spf-perl, postfix-pcre
+ , dovecot-core, dovecot-ldap, dovecot-lmtpd, dovecot-managesieved, dovecot-antispam
+ , rspamd (>= 1.6.0), opendkim-tools, postsrsd, procmail, mailutils
+ , redis-server
, metronome
- , rspamd (>= 1.6.0), redis-server, opendkim-tools
- , haveged, fake-hwclock
- , equivs, lsof
+ , git, curl, wget, cron, unzip, jq
+ , lsb-release, haveged, fake-hwclock, equivs, lsof
Recommends: yunohost-admin
- , openssh-server, ntp, inetutils-ping | iputils-ping
- , bash-completion, rsyslog, etckeeper
+ , ntp, inetutils-ping | iputils-ping
+ , bash-completion, rsyslog
, php-gd, php-curl, php-gettext, php-mcrypt
, python-pip
, unattended-upgrades
@@ -43,6 +44,7 @@ Conflicts: iptables-persistent
, yunohost-config-dovecot, yunohost-config-slapd
, yunohost-config-nginx, yunohost-config-amavis
, yunohost-config-mysql, yunohost-predepends
+ , apache2, bind9
Replaces: moulinette-yunohost, yunohost-config
, yunohost-config-others, yunohost-config-postfix
, yunohost-config-dovecot, yunohost-config-slapd
diff --git a/debian/copyright b/debian/copyright
index 8dd627ca5..59483b81e 100644
--- a/debian/copyright
+++ b/debian/copyright
@@ -1,5 +1,5 @@
Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
-Source: https://github.com/YunoHost/moulinette-yunohost
+Source: https://github.com/YunoHost/yunohost
Files: *
Copyright: 2015 YUNOHOST.ORG
diff --git a/debian/install b/debian/install
index e0743cdd1..a814d1617 100644
--- a/debian/install
+++ b/debian/install
@@ -7,6 +7,7 @@ data/hooks/* /usr/share/yunohost/hooks/
data/other/yunoprompt.service /etc/systemd/system/
data/other/password/* /usr/share/yunohost/other/password/
data/other/dpkg-origins/yunohost /etc/dpkg/origins
+data/other/dnsbl_list.yml /usr/share/yunohost/other/
data/other/* /usr/share/yunohost/yunohost-config/moulinette/
data/templates/* /usr/share/yunohost/templates/
data/helpers /usr/share/yunohost/
diff --git a/debian/postinst b/debian/postinst
index 7b7080513..9c78c8432 100644
--- a/debian/postinst
+++ b/debian/postinst
@@ -14,17 +14,14 @@ do_configure() {
echo "Regenerating configuration, this might take a while..."
yunohost tools regen-conf --output-as none
- echo "Launching migrations.."
+ echo "Launching migrations..."
yunohost tools migrations migrate --auto
- # restart yunohost-firewall if it's running
- service yunohost-firewall status >/dev/null \
- && restart_yunohost_firewall \
- || echo "yunohost-firewall service is not running, you should " \
- "consider to start it by doing 'service yunohost-firewall start'."
+ echo "Re-diagnosing server health..."
+ yunohost diagnosis run --force
fi
-
- # Change dpkg vendor
+
+ # Change dpkg vendor
# see https://wiki.debian.org/Derivatives/Guidelines#Vendor
readlink -f /etc/dpkg/origins/default | grep -q debian \
&& rm -f /etc/dpkg/origins/default \
@@ -39,24 +36,6 @@ do_configure() {
pam-auth-update --package
}
-restart_yunohost_firewall() {
- echo "Restarting YunoHost firewall..."
-
- deb-systemd-helper unmask yunohost-firewall.service >/dev/null || true
- if deb-systemd-helper --quiet was-enabled yunohost-firewall.service; then
- deb-systemd-helper enable yunohost-firewall.service >/dev/null || true
- else
- deb-systemd-helper update-state yunohost-firewall.service >/dev/null || true
- fi
-
- if [ -x /etc/init.d/yunohost-firewall ]; then
- update-rc.d yunohost-firewall enable >/dev/null
- if [ -n "$2" ]; then
- invoke-rc.d yunohost-firewall restart >/dev/null || exit $?
- fi
- fi
-}
-
# summary of how this script can be called:
# * `configure'
# * `abort-upgrade'
diff --git a/locales/ar.json b/locales/ar.json
index 46f9315af..9c1e67fe0 100644
--- a/locales/ar.json
+++ b/locales/ar.json
@@ -1,389 +1,110 @@
{
"action_invalid": "إجراء غير صالح '{action:s}'",
"admin_password": "كلمة السر الإدارية",
- "admin_password_change_failed": "تعذرت عملية تعديل كلمة السر",
+ "admin_password_change_failed": "لا يمكن تعديل الكلمة السرية",
"admin_password_changed": "تم تعديل الكلمة السرية الإدارية",
"app_already_installed": "{app:s} تم تنصيبه مِن قبل",
- "app_already_installed_cant_change_url": "",
"app_already_up_to_date": "{app:s} تم تحديثه مِن قَبل",
- "app_argument_choice_invalid": "",
- "app_argument_invalid": "",
"app_argument_required": "المُعامِل '{name:s}' مطلوب",
- "app_change_no_change_url_script": "إنّ التطبيق {app_name:s} لا يدعم تغيير الرابط، مِن الممكن أنه يتوجب عليكم تحدثيه.",
- "app_change_url_failed_nginx_reload": "فشلت عملية إعادة تشغيل nginx. ها هي نتيجة الأمر 'nginx -t':\n{nginx_errors:s}",
- "app_change_url_identical_domains": "The old and new domain/url_path are identical ('{domain:s}{path:s}'), nothing to do.",
- "app_change_url_no_script": "This application '{app_name:s}' doesn't support url modification yet. Maybe you should upgrade the application.",
- "app_change_url_success": "Successfully changed {app:s} url to {domain:s}{path:s}",
+ "app_change_url_failed_nginx_reload": "فشلت عملية إعادة تشغيل NGINX. ها هي نتيجة الأمر 'nginx -t':\n{nginx_errors:s}",
"app_extraction_failed": "تعذر فك الضغط عن ملفات التنصيب",
- "app_id_invalid": "Invalid app id",
- "app_incompatible": "إن التطبيق {app} غير متوافق مع إصدار واي يونوهوست YunoHost الخاص بك",
"app_install_files_invalid": "ملفات التنصيب خاطئة",
- "app_location_already_used": "The app '{app}' is already installed on that location ({path})",
- "app_make_default_location_already_used": "Can't make the app '{app}' the default on the domain {domain} is already used by the other app '{other_app}'",
- "app_location_install_failed": "Unable to install the app in this location because it conflit with the app '{other_app}' already installed on '{other_path}'",
- "app_location_unavailable": "This url is not available or conflicts with an already installed app",
- "app_manifest_invalid": "Invalid app manifest: {error}",
- "app_no_upgrade": "ليس هناك أي تطبيق بحاجة إلى تحديث",
"app_not_correctly_installed": "يبدو أن التطبيق {app:s} لم يتم تنصيبه بشكل صحيح",
"app_not_installed": "إنّ التطبيق {app:s} غير مُنصَّب",
"app_not_properly_removed": "لم يتم حذف تطبيق {app:s} بشكلٍ جيّد",
- "app_package_need_update": "The app {app} package needs to be updated to follow YunoHost changes",
"app_removed": "تمت إزالة تطبيق {app:s}",
"app_requirements_checking": "جار فحص الحزم اللازمة لـ {app}…",
- "app_requirements_failed": "Unable to meet requirements for {app}: {error}",
- "app_requirements_unmeet": "Requirements are not met for {app}, the package {pkgname} ({version}) must be {spec}",
"app_sources_fetch_failed": "تعذرت عملية جلب مصادر الملفات",
"app_unknown": "برنامج مجهول",
- "app_unsupported_remote_type": "Unsupported remote type used for the app",
"app_upgrade_app_name": "جارٍ تحديث تطبيق {app}…",
"app_upgrade_failed": "تعذرت عملية ترقية {app:s}",
"app_upgrade_some_app_failed": "تعذرت عملية ترقية بعض التطبيقات",
"app_upgraded": "تم تحديث التطبيق {app:s}",
- "appslist_corrupted_json": "Could not load the application lists. It looks like {filename:s} is corrupted.",
- "appslist_could_not_migrate": "Could not migrate app list {appslist:s} ! Unable to parse the url... The old cron job has been kept in {bkp_file:s}.",
- "appslist_fetched": "تم جلب قائمة تطبيقات {appslist:s}",
- "appslist_migrating": "Migrating application list {appslist:s} …",
- "appslist_name_already_tracked": "There is already a registered application list with name {name:s}.",
- "appslist_removed": "تم حذف قائمة التطبيقات {appslist:s}",
- "appslist_retrieve_bad_format": "Retrieved file for application list {appslist:s} is not valid",
- "appslist_retrieve_error": "Unable to retrieve the remote application list {appslist:s}: {error:s}",
- "appslist_unknown": "قائمة التطبيقات {appslist:s} مجهولة.",
- "appslist_url_already_tracked": "There is already a registered application list with url {url:s}.",
- "ask_current_admin_password": "كلمة السر الإدارية الحالية",
"ask_email": "عنوان البريد الإلكتروني",
"ask_firstname": "الإسم",
"ask_lastname": "اللقب",
- "ask_list_to_remove": "القائمة المختارة للحذف",
"ask_main_domain": "النطاق الرئيسي",
"ask_new_admin_password": "كلمة السر الإدارية الجديدة",
"ask_password": "كلمة السر",
- "ask_path": "المسار",
- "backup_abstract_method": "This backup method hasn't yet been implemented",
- "backup_action_required": "You must specify something to save",
- "backup_app_failed": "Unable to back up the app '{app:s}'",
- "backup_applying_method_borg": "Sending all files to backup into borg-backup repository…",
"backup_applying_method_copy": "جارٍ نسخ كافة الملفات إلى النسخة الإحتياطية …",
- "backup_applying_method_custom": "Calling the custom backup method '{method:s}'…",
- "backup_applying_method_tar": "جارٍ إنشاء ملف tar للنسخة الاحتياطية…",
- "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_mount_failed": "Mounting the backup archive failed",
- "backup_archive_name_exists": "The backup's archive name already exists",
- "backup_archive_name_unknown": "Unknown local backup archive named '{name:s}'",
- "backup_archive_open_failed": "Unable to open the backup archive",
- "backup_archive_system_part_not_available": "System part '{part:s}' not available in this backup",
- "backup_archive_writing_error": "Unable to add files to backup into the compressed archive",
- "backup_ask_for_copying_if_needed": "Some files couldn't be prepared to be backuped using the method that avoid to temporarily waste space on the system. To perform the backup, {size:s}MB should be used temporarily. Do you agree?",
- "backup_borg_not_implemented": "Borg backup method is not yet implemented",
- "backup_cant_mount_uncompress_archive": "Unable to mount in readonly mode the uncompress archive directory",
- "backup_cleaning_failed": "Unable to clean-up the temporary backup directory",
- "backup_copying_to_organize_the_archive": "Copying {size:s}MB to organize the archive",
- "backup_couldnt_bind": "Couldn't bind {src:s} to {dest:s}.",
+ "backup_applying_method_tar": "جارٍ إنشاء ملف TAR للنسخة الاحتياطية…",
"backup_created": "تم إنشاء النسخة الإحتياطية",
- "backup_creating_archive": "جارٍ إنشاء ملف النسخة الاحتياطية…",
- "backup_creation_failed": "Backup creation failed",
- "backup_csv_addition_failed": "Unable to add files to backup into the CSV file",
- "backup_csv_creation_failed": "Unable to create the CSV file needed for future restore operations",
- "backup_custom_backup_error": "Custom backup method failure on 'backup' step",
- "backup_custom_mount_error": "Custom backup method failure on 'mount' step",
- "backup_custom_need_mount_error": "Custom backup method failure on 'need_mount' step",
- "backup_delete_error": "Unable to delete '{path:s}'",
- "backup_deleted": "The backup has been deleted",
- "backup_extracting_archive": "Extracting the backup archive…",
- "backup_hook_unknown": "Backup hook '{hook:s}' unknown",
"backup_invalid_archive": "نسخة إحتياطية غير صالحة",
- "backup_method_borg_finished": "Backup into borg finished",
"backup_method_copy_finished": "إنتهت عملية النسخ الإحتياطي",
- "backup_method_custom_finished": "Custom backup method '{method:s}' finished",
- "backup_method_tar_finished": "Backup tar archive created",
- "backup_no_uncompress_archive_dir": "Uncompress archive directory doesn't exist",
"backup_nothings_done": "ليس هناك أي شيء للحفظ",
- "backup_output_directory_forbidden": "Forbidden output directory. Backups can't be created in /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var or /home/yunohost.backup/archives sub-folders",
- "backup_output_directory_not_empty": "The output directory is not empty",
"backup_output_directory_required": "يتوجب عليك تحديد مجلد لتلقي النسخ الإحتياطية",
- "backup_output_symlink_dir_broken": "You have a broken symlink instead of your archives directory '{path:s}'. You may have a specific setup to backup your data on an other filesystem, in this case you probably forgot to remount or plug your hard dirve or usb key.",
- "backup_running_app_script": "Running backup script of app '{app:s}'...",
- "backup_running_hooks": "Running backup hooks…",
- "backup_system_part_failed": "Unable to backup the '{part:s}' system part",
- "backup_unable_to_organize_files": "Unable to organize files in the archive with the quick method",
- "backup_with_no_backup_script_for_app": "App {app:s} has no backup script. Ignoring.",
- "backup_with_no_restore_script_for_app": "App {app:s} has no restore script, you won't be able to automatically restore the backup of this app.",
- "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_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_attempt_to_replace_valid_cert": "You are attempting to overwrite a good and valid certificate for domain {domain:s}! (Use --force to bypass)",
- "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": "تمت عملية تنصيب شهادة Let's Encrypt بنجاح على النطاق {domain:s} !",
- "certmanager_cert_install_success_selfsigned": "نجحت عملية تثبيت الشهادة الموقعة ذاتيا الخاصة بالنطاق {domain:s}!",
+ "certmanager_cert_install_success_selfsigned": "نجحت عملية تثبيت الشهادة الموقعة ذاتيا الخاصة بالنطاق {domain:s}",
"certmanager_cert_renew_success": "نجحت عملية تجديد شهادة Let's Encrypt الخاصة باسم النطاق {domain:s} !",
"certmanager_cert_signing_failed": "فشل إجراء توقيع الشهادة الجديدة",
- "certmanager_certificate_fetching_or_enabling_failed": "Sounds like enabling the new certificate for {domain:s} failed somehow…",
- "certmanager_conflicting_nginx_file": "Unable to prepare domain for ACME challenge: the nginx configuration file {filepath:s} is conflicting and should be removed first",
- "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_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_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_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_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_domain_unknown": "النطاق مجهول {domain:s}",
- "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_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_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 issue or the firewall/router ahead of your server is misconfigured.",
"certmanager_no_cert_file": "تعذرت عملية قراءة شهادة نطاق {domain:s} (الملف : {file:s})",
- "certmanager_old_letsencrypt_app_detected": "",
- "certmanager_self_ca_conf_file_not_found": "Configuration file not found for self-signing authority (file: {file:s})",
- "certmanager_unable_to_parse_self_CA_name": "Unable to parse name of self-signing authority (file: {file:s})",
- "custom_app_url_required": "You must provide a URL to upgrade your custom app {app:s}",
- "custom_appslist_name_required": "You must provide a name for your custom app list",
- "diagnosis_debian_version_error": "لم نتمكن من العثور على إصدار ديبيان : {error}",
- "diagnosis_kernel_version_error": "Can't retrieve kernel version: {error}",
- "diagnosis_monitor_disk_error": "Can't monitor disks: {error}",
- "diagnosis_monitor_network_error": "Can't monitor network: {error}",
- "diagnosis_monitor_system_error": "Can't monitor system: {error}",
- "diagnosis_no_apps": "لم تقم بتنصيب أية تطبيقات بعد",
- "dnsmasq_isnt_installed": "dnsmasq does not seem to be installed, please run 'apt-get remove bind9 && apt-get install dnsmasq'",
- "domain_cannot_remove_main": "Cannot remove main domain. Set a new main domain first",
- "domain_cert_gen_failed": "Unable to generate certificate",
"domain_created": "تم إنشاء النطاق",
"domain_creation_failed": "تعذرت عملية إنشاء النطاق",
"domain_deleted": "تم حذف النطاق",
- "domain_deletion_failed": "Unable to delete domain",
- "domain_dns_conf_is_just_a_recommendation": "This command shows you what is the *recommended* configuration. It does not actually set up the DNS configuration for you. It is your responsability to configure your DNS zone in your registrar according to this recommendation.",
- "domain_dyndns_already_subscribed": "You've already subscribed to a DynDNS domain",
- "domain_dyndns_dynette_is_unreachable": "Unable to reach YunoHost dynette, either your YunoHost is not correctly connected to the internet or the dynette server is down. Error: {error}",
- "domain_dyndns_invalid": "Invalid domain to use with DynDNS",
- "domain_dyndns_root_unknown": "Unknown DynDNS root domain",
"domain_exists": "اسم النطاق موجود مِن قبل",
- "domain_hostname_failed": "Failed to set new hostname",
- "domain_uninstall_app_first": "One or more apps are installed on this domain. Please uninstall them before proceeding to domain removal",
"domain_unknown": "النطاق مجهول",
- "domain_zone_exists": "ملف منطقة أسماء النطاقات موجود مِن قبل",
- "domain_zone_not_found": "DNS zone file not found for domain {:s}",
"domains_available": "النطاقات المتوفرة :",
"done": "تم",
"downloading": "عملية التنزيل جارية …",
- "dyndns_could_not_check_provide": "Could not check if {provider:s} can provide {domain:s}.",
- "dyndns_cron_installed": "The DynDNS cron job has been installed",
- "dyndns_cron_remove_failed": "Unable to remove the DynDNS cron job",
- "dyndns_cron_removed": "The DynDNS cron job has been removed",
- "dyndns_ip_update_failed": "Unable to update IP address on DynDNS",
"dyndns_ip_updated": "لقد تم تحديث عنوان الإيبي الخاص بك على نظام أسماء النطاقات الديناميكي",
"dyndns_key_generating": "عملية توليد مفتاح نظام أسماء النطاقات جارية. يمكن للعملية أن تستغرق بعضا من الوقت…",
"dyndns_key_not_found": "لم يتم العثور على مفتاح DNS الخاص باسم النطاق هذا",
- "dyndns_no_domain_registered": "No domain has been registered with DynDNS",
- "dyndns_registered": "The DynDNS domain has been registered",
- "dyndns_registration_failed": "Unable to register DynDNS domain: {error:s}",
- "dyndns_domain_not_provided": "Dyndns provider {provider:s} cannot provide domain {domain:s}.",
- "dyndns_unavailable": "Domain {domain:s} is not available.",
- "executing_command": "Executing command '{command:s}'…",
- "executing_script": "Executing script '{script:s}'…",
"extracting": "عملية فك الضغط جارية …",
- "field_invalid": "Invalid field '{:s}'",
- "firewall_reload_failed": "Unable to reload the firewall",
- "firewall_reloaded": "The firewall has been reloaded",
- "firewall_rules_cmd_failed": "Some firewall rules commands have failed. For more information, see the log.",
- "format_datetime_short": "%m/%d/%Y %I:%M %p",
- "global_settings_bad_choice_for_enum": "Bad value for setting {setting:s}, received {received_type:s}, except {expected_type:s}",
- "global_settings_bad_type_for_setting": "Bad type for setting {setting:s}, received {received_type:s}, except {expected_type:s}",
- "global_settings_cant_open_settings": "Failed to open settings file, reason: {reason:s}",
- "global_settings_cant_serialize_settings": "Failed to serialize settings data, reason: {reason:s}",
- "global_settings_cant_write_settings": "Failed to write settings file, reason: {reason:s}",
- "global_settings_key_doesnt_exists": "The key '{settings_key:s}' doesn't exists in the global settings, you can see all the available keys by doing 'yunohost settings list'",
- "global_settings_reset_success": "Success. Your previous settings have been backuped in {path:s}",
- "global_settings_setting_example_bool": "Example boolean option",
- "global_settings_setting_example_enum": "Example enum option",
- "global_settings_setting_example_int": "Example int option",
- "global_settings_setting_example_string": "Example string option",
- "global_settings_unknown_setting_from_settings_file": "Unknown key in settings: '{setting_key:s}', discarding it and save it in /etc/yunohost/unkown_settings.json",
- "global_settings_unknown_type": "Unexpected situation, the setting {setting:s} appears to have the type {unknown_type:s} but it's not a type supported by the system.",
- "hook_exec_failed": "Script execution failed: {path:s}",
- "hook_exec_not_terminated": "Script execution hasn’t terminated: {path:s}",
- "hook_list_by_invalid": "Invalid property to list hook by",
- "hook_name_unknown": "Unknown hook name '{name:s}'",
"installation_complete": "إكتملت عملية التنصيب",
- "installation_failed": "Installation failed",
- "invalid_url_format": "Invalid URL format",
- "ip6tables_unavailable": "You cannot play with ip6tables here. You are either in a container or your kernel does not support it",
- "iptables_unavailable": "You cannot play with iptables here. You are either in a container or your kernel does not support it",
- "ldap_init_failed_to_create_admin": "LDAP initialization failed to create admin user",
- "ldap_initialized": "LDAP has been initialized",
- "license_undefined": "undefined",
- "mail_alias_remove_failed": "Unable to remove mail alias '{mail:s}'",
- "mail_domain_unknown": "Unknown mail address domain '{domain:s}'",
- "mail_forward_remove_failed": "Unable to remove mail forward '{mail:s}'",
- "mailbox_used_space_dovecot_down": "Dovecot mailbox service need to be up, if you want to get mailbox used space",
- "maindomain_change_failed": "Unable to change the main domain",
- "maindomain_changed": "The main domain has been changed",
- "migrate_tsig_end": "Migration to hmac-sha512 finished",
- "migrate_tsig_failed": "Migrating the dyndns domain {domain} to hmac-sha512 failed, rolling back. Error: {error_code} - {error}",
- "migrate_tsig_start": "Not secure enough key algorithm detected for TSIG signature of domain '{domain}', initiating migration to the more secure one hmac-sha512",
- "migrate_tsig_wait": "لننتظر الآن 3 دقائق ريثما يأخذ خادم أسماء النطاقات الديناميكية بعين الاعتبار المفتاح الجديد…",
+ "main_domain_change_failed": "تعذّر تغيير النطاق الأساسي",
+ "main_domain_changed": "تم تغيير النطاق الأساسي",
+ "migrate_tsig_wait": "لننتظر الآن ثلاثة دقائق ريثما يأخذ خادم أسماء النطاقات الديناميكية بعين الاعتبار المفتاح الجديد…",
"migrate_tsig_wait_2": "دقيقتين …",
"migrate_tsig_wait_3": "دقيقة واحدة …",
"migrate_tsig_wait_4": "30 ثانية …",
- "migrate_tsig_not_needed": "You do not appear to use a dyndns domain, so no migration is needed !",
- "migrations_backward": "Migrating backward.",
- "migrations_bad_value_for_target": "Invalid number for target argument, available migrations numbers are 0 or {}",
- "migrations_cant_reach_migration_file": "Can't access migrations files at path %s",
- "migrations_current_target": "Migration target is {}",
- "migrations_error_failed_to_load_migration": "ERROR: failed to load migration {number} {name}",
- "migrations_forward": "Migrating forward",
- "migrations_loading_migration": "Loading migration {number} {name}…",
- "migrations_migration_has_failed": "Migration {number} {name} has failed with exception {exception}, aborting",
- "migrations_no_migrations_to_run": "No migrations to run",
- "migrations_show_currently_running_migration": "Running migration {number} {name}…",
- "migrations_show_last_migration": "Last ran migration is {}",
"migrations_skip_migration": "جارٍ تجاهل التهجير {id}…",
- "monitor_disabled": "The server monitoring has been disabled",
- "monitor_enabled": "The server monitoring has been enabled",
- "monitor_glances_con_failed": "Unable to connect to Glances server",
- "monitor_not_enabled": "Server monitoring is not enabled",
- "monitor_period_invalid": "Invalid time period",
- "monitor_stats_file_not_found": "Statistics file not found",
- "monitor_stats_no_update": "No monitoring statistics to update",
- "monitor_stats_period_unavailable": "No available statistics for the period",
- "mountpoint_unknown": "Unknown mountpoint",
- "mysql_db_creation_failed": "MySQL database creation failed",
- "mysql_db_init_failed": "MySQL database init failed",
- "mysql_db_initialized": "The MySQL database has been initialized",
- "network_check_mx_ko": "DNS MX record is not set",
- "network_check_smtp_ko": "Outbound mail (SMTP port 25) seems to be blocked by your network",
- "network_check_smtp_ok": "Outbound mail (SMTP port 25) is not blocked",
- "new_domain_required": "You must provide the new main domain",
- "no_appslist_found": "No app list found",
- "no_internet_connection": "Server is not connected to the Internet",
- "no_ipv6_connectivity": "IPv6 connectivity is not available",
- "no_restore_script": "No restore script found for the app '{app:s}'",
- "not_enough_disk_space": "Not enough free disk space on '{path:s}'",
- "package_not_installed": "Package '{pkgname}' is not installed",
- "package_unexpected_error": "An unexpected error occurred processing the package '{pkgname}'",
- "package_unknown": "Unknown package '{pkgname}'",
- "packages_no_upgrade": "لا يوجد هناك أية حزمة بحاجة إلى تحديث",
- "packages_upgrade_critical_later": "Critical packages ({packages:s}) will be upgraded later",
- "packages_upgrade_failed": "Unable to upgrade all of the packages",
- "path_removal_failed": "Unable to remove path {:s}",
- "pattern_backup_archive_name": "Must be a valid filename with max 30 characters, and alphanumeric and -_. characters only",
"pattern_domain": "يتوجب أن يكون إسم نطاق صالح (مثل my-domain.org)",
"pattern_email": "يتوجب أن يكون عنوان بريد إلكتروني صالح (مثل someone@domain.org)",
- "pattern_firstname": "Must be a valid first name",
- "pattern_lastname": "Must be a valid last name",
- "pattern_listname": "Must be alphanumeric and underscore characters only",
- "pattern_mailbox_quota": "Must be a size with b/k/M/G/T suffix or 0 to disable the quota",
"pattern_password": "يتوجب أن تكون مكونة من 3 حروف على الأقل",
- "pattern_port": "يجب أن يكون رقم منفذ صالح (مثال 0-65535)",
- "pattern_port_or_range": "Must be a valid port number (i.e. 0-65535) or range of ports (e.g. 100:200)",
"pattern_positive_number": "يجب أن يكون عددا إيجابيا",
- "pattern_username": "Must be lower-case alphanumeric and underscore characters only",
- "port_already_closed": "Port {port:d} is already closed for {ip_version:s} connections",
- "port_already_opened": "Port {port:d} is already opened for {ip_version:s} connections",
- "port_available": "المنفذ {port:d} متوفر",
- "port_unavailable": "Port {port:d} is not available",
- "restore_action_required": "You must specify something to restore",
- "restore_already_installed_app": "An app is already installed with the id '{app:s}'",
- "restore_app_failed": "Unable to restore the app '{app:s}'",
- "restore_cleaning_failed": "Unable to clean-up the temporary restoration directory",
- "restore_complete": "Restore complete",
- "restore_confirm_yunohost_installed": "Do you really want to restore an already installed system? [{answers:s}]",
"restore_extracting": "جارٍ فك الضغط عن الملفات التي نحتاجها من النسخة الاحتياطية…",
- "restore_failed": "Unable to restore the system",
- "restore_hook_unavailable": "Restoration script for '{part:s}' not available on your system and not in the archive either",
- "restore_may_be_not_enough_disk_space": "Your system seems not to have enough disk space (freespace: {free_space:d} B, needed space: {needed_space:d} B, security margin: {margin:d} B)",
- "restore_mounting_archive": "تنصيب النسخة الإحتياطية على المسار '{path:s}'",
- "restore_not_enough_disk_space": "Not enough disk space (freespace: {free_space:d} B, needed space: {needed_space:d} B, security margin: {margin:d} B)",
- "restore_nothings_done": "Nothing has been restored",
- "restore_removing_tmp_dir_failed": "Unable to remove an old temporary directory",
- "restore_running_app_script": "Running restore script of app '{app:s}'…",
- "restore_running_hooks": "Running restoration hooks…",
- "restore_system_part_failed": "Unable to restore the '{part:s}' system part",
"server_shutdown": "سوف ينطفئ الخادوم",
"server_shutdown_confirm": "سوف ينطفئ الخادوم حالا. متأكد ؟ [{answers:s}]",
"server_reboot": "سيعاد تشغيل الخادوم",
"server_reboot_confirm": "سيعاد تشغيل الخادوم في الحين. هل أنت متأكد ؟ [{answers:s}]",
"service_add_failed": "تعذرت إضافة خدمة '{service:s}'",
- "service_added": "The service '{service:s}' has been added",
- "service_already_started": "Service '{service:s}' has already been started",
"service_already_stopped": "إنّ خدمة '{service:s}' متوقفة مِن قبلُ",
- "service_cmd_exec_failed": "Unable to execute command '{command:s}'",
- "service_conf_file_backed_up": "The configuration file '{conf}' has been backed up to '{backup}'",
- "service_conf_file_copy_failed": "Unable to copy the new configuration file '{new}' to '{conf}'",
- "service_conf_file_kept_back": "The configuration file '{conf}' is expected to be deleted by service {service} but has been kept back.",
- "service_conf_file_manually_modified": "The configuration file '{conf}' has been manually modified and will not be updated",
- "service_conf_file_manually_removed": "The configuration file '{conf}' has been manually removed and will not be created",
- "service_conf_file_remove_failed": "Unable to remove the configuration file '{conf}'",
- "service_conf_file_removed": "The configuration file '{conf}' has been removed",
- "service_conf_file_updated": "The configuration file '{conf}' has been updated",
- "service_conf_new_managed_file": "The configuration file '{conf}' is now managed by the service {service}.",
- "service_conf_up_to_date": "The configuration is already up-to-date for service '{service}'",
- "service_conf_updated": "The configuration has been updated for service '{service}'",
- "service_conf_would_be_updated": "The configuration would have been updated for service '{service}'",
- "service_disable_failed": "",
- "service_disabled": "تم تعطيل خدمة '{service:s}'",
- "service_enable_failed": "",
+ "service_disabled": "لن يتم إطلاق خدمة '{service:s}' أثناء بداية تشغيل النظام.",
"service_enabled": "تم تنشيط خدمة '{service:s}'",
- "service_no_log": "ليس لخدمة '{service:s}' أي سِجلّ للعرض",
- "service_regenconf_dry_pending_applying": "Checking pending configuration which would have been applied for service '{service}'...",
- "service_regenconf_failed": "Unable to regenerate the configuration for service(s): {services}",
- "service_regenconf_pending_applying": "Applying pending configuration for service '{service}'...",
- "service_remove_failed": "Unable to remove service '{service:s}'",
"service_removed": "تمت إزالة خدمة '{service:s}'",
- "service_start_failed": "",
"service_started": "تم إطلاق تشغيل خدمة '{service:s}'",
- "service_status_failed": "Unable to determine status of service '{service:s}'",
- "service_stop_failed": "",
"service_stopped": "تمّ إيقاف خدمة '{service:s}'",
- "service_unknown": "Unknown service '{service:s}'",
- "ssowat_conf_generated": "The SSOwat configuration has been generated",
- "ssowat_conf_updated": "The SSOwat configuration has been updated",
- "ssowat_persistent_conf_read_error": "Error while reading SSOwat persistent configuration: {error:s}. Edit /etc/ssowat/conf.json.persistent file to fix the JSON syntax",
- "ssowat_persistent_conf_write_error": "Error while saving SSOwat persistent configuration: {error:s}. Edit /etc/ssowat/conf.json.persistent file to fix the JSON syntax",
"system_upgraded": "تمت عملية ترقية النظام",
- "system_username_exists": "Username already exists in the system users",
- "unbackup_app": "App '{app:s}' will not be saved",
- "unexpected_error": "An unexpected error occured",
- "unit_unknown": "Unknown unit '{unit:s}'",
"unlimit": "دون تحديد الحصة",
- "unrestore_app": "App '{app:s}' will not be restored",
- "update_cache_failed": "Unable to update APT cache",
"updating_apt_cache": "جارٍ جلب قائمة حُزم النظام المحدّثة المتوفرة…",
- "upgrade_complete": "إكتملت عملية الترقية و التحديث",
+ "upgrade_complete": "اكتملت عملية الترقية و التحديث",
"upgrading_packages": "عملية ترقية الحُزم جارية …",
- "upnp_dev_not_found": "No UPnP device found",
"upnp_disabled": "تم تعطيل UPnP",
- "upnp_enabled": "UPnP has been enabled",
- "upnp_port_open_failed": "Unable to open UPnP ports",
"user_created": "تم إنشاء المستخدم",
- "user_creation_failed": "Unable to create user",
"user_deleted": "تم حذف المستخدم",
"user_deletion_failed": "لا يمكن حذف المستخدم",
- "user_home_creation_failed": "Unable to create user home folder",
- "user_info_failed": "Unable to retrieve user information",
"user_unknown": "المستخدم {user:s} مجهول",
"user_update_failed": "لا يمكن تحديث المستخدم",
"user_updated": "تم تحديث المستخدم",
- "yunohost_already_installed": "YunoHost is already installed",
"yunohost_ca_creation_failed": "تعذرت عملية إنشاء هيئة الشهادات",
"yunohost_ca_creation_success": "تم إنشاء هيئة الشهادات المحلية.",
- "yunohost_configured": "YunoHost has been configured",
"yunohost_installing": "عملية تنصيب يونوهوست جارية …",
"yunohost_not_installed": "إنَّ واي يونوهوست ليس مُنَصَّب أو هو مثبت حاليا بشكل خاطئ. قم بتنفيذ الأمر 'yunohost tools postinstall'",
"migration_description_0003_migrate_to_stretch": "تحديث النظام إلى ديبيان ستريتش و واي يونوهوست 3.0",
"migration_0003_patching_sources_list": "عملية تصحيح ملف المصادر sources.lists جارية…",
"migration_0003_main_upgrade": "بداية عملية التحديث الأساسية…",
- "migration_0003_fail2ban_upgrade": "بداية عملية تحديث fail2ban…",
+ "migration_0003_fail2ban_upgrade": "بداية عملية تحديث Fail2Ban…",
"migration_0003_not_jessie": "إن توزيعة ديبيان الحالية تختلف عن جيسي !",
"migration_description_0002_migrate_to_tsig_sha256": "يقوم بتحسين أمان TSIG لنظام أسماء النطاقات الديناميكة باستخدام SHA512 بدلًا مِن MD5",
- "migration_0003_backward_impossible": "لا يُمكن إلغاء عملية الإنتقال إلى ستريتش.",
"migration_0003_system_not_fully_up_to_date": "إنّ نظامك غير مُحدَّث بعدُ لذا يرجى القيام بتحديث عادي أولا قبل إطلاق إجراء الإنتقال إلى نظام ستريتش.",
"migrations_list_conflict_pending_done": "لا يمكنك استخدام --previous و --done معًا على نفس سطر الأوامر.",
"service_description_avahi-daemon": "يسمح لك بالنفاذ إلى خادومك عبر الشبكة المحلية باستخدام yunohost.local",
- "service_description_glances": "يقوم بمراقبة معلومات النظام على خادومك",
"service_description_metronome": "يُدير حسابات الدردشة الفورية XMPP",
"service_description_nginx": "يقوم بتوفير النفاذ و السماح بالوصول إلى كافة مواقع الويب المستضافة على خادومك",
- "service_description_php5-fpm": "يقوم بتشغيل تطبيقات الـ PHP مع خادوم الويب nginx",
"service_description_postfix": "يقوم بإرسال و تلقي الرسائل البريدية الإلكترونية",
"service_description_yunohost-api": "يقوم بإدارة التفاعلات ما بين واجهة الويب لواي يونوهوست و النظام",
"log_category_404": "فئةالسجل '{category}' لا وجود لها",
- "log_app_fetchlist": "إضافة قائمة للتطبيقات",
- "log_app_removelist": "حذف قائمة للتطبيقات",
"log_app_change_url": "تعديل رابط تطبيق '{}'",
"log_app_install": "تنصيب تطبيق '{}'",
"log_app_remove": "حذف تطبيق '{}'",
@@ -400,11 +121,10 @@
"log_letsencrypt_cert_install": "تنصيب شهادة Let’s Encrypt على النطاق '{}'",
"log_selfsigned_cert_install": "تنصيب شهادة موقَّعَة ذاتيا على اسم النطاق '{}'",
"log_letsencrypt_cert_renew": "تجديد شهادة Let's Encrypt لـ '{}'",
- "log_service_enable": "تنشيط خدمة '{}'",
"log_user_create": "إضافة المستخدم '{}'",
"log_user_delete": "حذف المستخدم '{}'",
"log_user_update": "تحديث معلومات المستخدم '{}'",
- "log_tools_maindomain": "جعل '{}' كنطاق أساسي",
+ "log_domain_main_domain": "جعل '{}' كنطاق أساسي",
"log_tools_upgrade": "تحديث حُزم ديبيان",
"log_tools_shutdown": "إطفاء الخادم",
"log_tools_reboot": "إعادة تشغيل الخادم",
@@ -412,11 +132,11 @@
"service_description_dnsmasq": "مُكلَّف بتحليل أسماء النطاقات (DNS)",
"service_description_mysql": "يقوم بتخزين بيانات التطبيقات (قواعد بيانات SQL)",
"service_description_rspamd": "يقوم بتصفية البريد المزعج و إدارة ميزات أخرى للبريد",
- "service_description_yunohost-firewall": "يُدير فتح وإغلاق منافذ الإتصال إلى الخدمات",
+ "service_description_yunohost-firewall": "يُدير فتح وإغلاق منافذ الاتصال إلى الخدمات",
"users_available": "المستخدمون المتوفرون:",
"aborting": "إلغاء.",
"admin_password_too_long": "يرجى اختيار كلمة سرية أقصر مِن 127 حرف",
- "app_not_upgraded": "لم يتم تحديث التطبيقات التالية: {apps}",
+ "app_not_upgraded": "",
"app_start_install": "جارٍ تثبيت التطبيق {app}…",
"app_start_remove": "جارٍ حذف التطبيق {app}…",
"app_start_restore": "جارٍ استرجاع التطبيق {app}…",
@@ -425,23 +145,32 @@
"ask_new_path": "مسار جديد",
"global_settings_setting_security_password_admin_strength": "قوة الكلمة السرية الإدارية",
"global_settings_setting_security_password_user_strength": "قوة الكلمة السرية للمستخدم",
- "log_app_addaccess": "إضافة ترخيص بالنفاذ إلى '{}'",
"password_too_simple_1": "يجب أن يكون طول الكلمة السرية على الأقل 8 حروف",
- "service_description_php7.0-fpm": "يُشغّل التطبيقات المكتوبة بلغة الـ PHP على Nginx",
- "updating_app_lists": "جارٍ جلب التحديثات المتوفرة الخاصة بالتطبيقات…",
- "already_up_to_date": "كل شيء على ما يرام! ليس هناك ما يتطلّب تحديثًا!",
+ "service_description_php7.0-fpm": "يُشغّل التطبيقات المكتوبة بلغة الـ PHP على NGINX",
+ "already_up_to_date": "كل شيء على ما يرام. ليس هناك ما يتطلّب تحديثًا.",
"service_description_nslcd": "يدير اتصال متسخدمي واي يونوهوست عبر طرفية سطر الأوامر",
"service_description_slapd": "يخزّن المستخدمين والنطاقات والمعلومات المتعلقة بها",
- "service_reloaded": "تم إعادة تحميل خدمة '{service:s}'",
+ "service_reloaded": "تم إعادة تشغيل خدمة '{service:s}'",
"service_restarted": "تم إعادة تشغيل خدمة '{service:s}'",
"group_unknown": "الفريق {group:s} مجهول",
- "group_deletion_failed": "فشلت عملية حذف الفريق '{group}'",
+ "group_deletion_failed": "فشلت عملية حذف الفريق '{group}': {error}",
"group_deleted": "تم حذف الفريق '{group}'",
- "group_created": "تم إنشاء الفريق '{group}' بنجاح",
- "group_name_already_exist": "الفريق {name:s} موجود بالفعل",
- "error_when_removing_sftpuser_group": "حدث خطأ أثناء محاولة حذف فريق sftpusers",
+ "group_created": "تم إنشاء الفريق '{group}'",
"dyndns_could_not_check_available": "لا يمكن التحقق مِن أنّ {domain:s} متوفر على {provider:s}.",
"backup_mount_archive_for_restore": "جارٍ تهيئة النسخة الاحتياطية للاسترجاع…",
"root_password_replaced_by_admin_password": "لقد تم استبدال كلمة سر الجذر root بالكلمة الإدارية لـ admin.",
- "app_upgrade_stopped": "لقد تم إلغاء تحديث كافة التطبيقات لتجنب حادث بسبب فشل تحديث التطبيق السابق"
+ "app_action_broke_system": "يبدو أنّ هذا الإجراء أدّى إلى تحطيم هذه الخدمات المهمة: {services}",
+ "diagnosis_basesystem_host": "هذا الخادم يُشغّل ديبيان {debian_version}",
+ "diagnosis_basesystem_kernel": "هذا الخادم يُشغّل نواة لينكس {kernel_version}",
+ "diagnosis_basesystem_ynh_single_version": "{package} الإصدار: {version} ({repo})",
+ "diagnosis_basesystem_ynh_main_version": "هذا الخادم يُشغّل YunoHost {main_version} ({repo})",
+ "diagnosis_everything_ok": "كل شيء على ما يرام في {category}!",
+ "diagnosis_ip_connected_ipv4": "الخادم مُتّصل بالإنترنت عبر IPv4!",
+ "diagnosis_ip_connected_ipv6": "الخادم مُتّصل بالإنترنت عبر IPv6!",
+ "diagnosis_ip_not_connected_at_all": "يبدو أنّ الخادم غير مُتّصل بتاتا بالإنترنت!؟",
+ "app_install_failed": "لا يمكن تنصيب {app}: {error}",
+ "apps_already_up_to_date": "كافة التطبيقات مُحدّثة",
+ "app_remove_after_failed_install": "جارٍ حذف التطبيق بعدما فشل تنصيبها…",
+ "apps_catalog_updating": "جارٍ تحديث فهرس التطبيقات…",
+ "apps_catalog_update_success": "تم تحديث فهرس التطبيقات!"
}
diff --git a/locales/bn_BD.json b/locales/bn_BD.json
index 0967ef424..c912ef50a 100644
--- a/locales/bn_BD.json
+++ b/locales/bn_BD.json
@@ -1 +1,3 @@
-{}
+{
+ "password_too_simple_1": "পাসওয়ার্ডটি কমপক্ষে 8 টি অক্ষরের দীর্ঘ হওয়া দরকার"
+}
\ No newline at end of file
diff --git a/locales/br.json b/locales/br.json
index 0967ef424..9e26dfeeb 100644
--- a/locales/br.json
+++ b/locales/br.json
@@ -1 +1 @@
-{}
+{}
\ No newline at end of file
diff --git a/locales/ca.json b/locales/ca.json
index f5c040670..ff7045d0a 100644
--- a/locales/ca.json
+++ b/locales/ca.json
@@ -1,113 +1,88 @@
{
"action_invalid": "Acció '{action:s}' invàlida",
"admin_password": "Contrasenya d'administració",
- "admin_password_change_failed": "No s'ha pogut canviar la contrasenya",
+ "admin_password_change_failed": "No es pot canviar la contrasenya",
"admin_password_changed": "S'ha canviat la contrasenya d'administració",
"app_already_installed": "{app:s} ja està instal·lada",
"app_already_installed_cant_change_url": "Aquesta aplicació ja està instal·lada. La URL no és pot canviar únicament amb aquesta funció. Mireu a \"app changeurl\" si està disponible.",
"app_already_up_to_date": "{app:s} ja està actualitzada",
- "app_argument_choice_invalid": "Aquesta opció no és vàlida per l'argument '{name:s}', ha de ser una de {choices:s}",
- "app_argument_invalid": "Valor invàlid per l'argument '{name:s}':{error:s}",
+ "app_argument_choice_invalid": "Utilitzeu una de les opcions «{choices:s}» per l'argument «{name:s}»",
+ "app_argument_invalid": "Escolliu un valor vàlid per l'argument «{name:s}»: {error:s}",
"app_argument_required": "Es necessita l'argument '{name:s}'",
- "app_change_no_change_url_script": "L'aplicació {app_name:s} encara no permet canviar la seva URL, es possible que s'hagi d'actualitzar.",
- "app_change_url_failed_nginx_reload": "No s'ha pogut tornar a carregar nginx. Aquí teniu el resultat de \"nginx -t\":\n{nginx_errors:s}",
+ "app_change_url_failed_nginx_reload": "No s'ha pogut tornar a carregar NGINX. Aquí teniu el resultat de \"nginx -t\":\n{nginx_errors:s}",
"app_change_url_identical_domains": "L'antic i el nou domini/camí són idèntics ('{domain:s}{path:s}'), no hi ha res per fer.",
- "app_change_url_no_script": "Aquesta aplicació '{app_name:s}' encara no permet modificar la URL. Potser s'ha d'actualitzar l'aplicació.",
- "app_change_url_success": "La URL de {app:s} s'ha canviat correctament a {domain:s}{path:s}",
+ "app_change_url_no_script": "L'aplicació '{app_name:s}' encara no permet modificar la URL. Potser s'ha d'actualitzar.",
+ "app_change_url_success": "La URL de {app:s} ara és {domain:s}{path:s}",
"app_extraction_failed": "No s'han pogut extreure els fitxers d'instal·lació",
- "app_id_invalid": "Id de l'aplicació incorrecte",
- "app_incompatible": "L'aplicació {app} no és compatible amb la teva versió de YunoHost",
- "app_install_files_invalid": "Fitxers d'instal·lació invàlids",
- "app_location_already_used": "L'aplicació '{app}' ja està instal·lada en aquest camí ({path})",
- "app_make_default_location_already_used": "No es pot fer l'aplicació '{app}' per defecte en el domini {domain} ja que ja és utilitzat per una altra aplicació '{other_app}'",
- "app_location_install_failed": "No s'ha pogut instal·lar l'aplicació en aquest camí ja que entra en conflicte amb l'aplicació '{other_app}' ja instal·lada a '{other_path}'",
- "app_location_unavailable": "Aquesta url no està disponible o entra en conflicte amb aplicacions ja instal·lades:\n{apps:s}",
- "app_manifest_invalid": "Manifest d'aplicació incorrecte: {error}",
- "app_no_upgrade": "No hi ha cap aplicació per actualitzar",
+ "app_id_invalid": "ID de l'aplicació incorrecte",
+ "app_install_files_invalid": "Aquests fitxers no es poden instal·lar",
+ "app_make_default_location_already_used": "No es pot fer l'aplicació '{app}' per defecte en el domini «{domain}» ja que ja és utilitzat per una altra aplicació '{other_app}'",
+ "app_location_unavailable": "Aquesta URL no està disponible o entra en conflicte amb aplicacions ja instal·lades:\n{apps:s}",
+ "app_manifest_invalid": "Hi ha algun error amb el manifest de l'aplicació: {error}",
"app_not_correctly_installed": "{app:s} sembla estar mal instal·lada",
- "app_not_installed": "L'aplicació «{app:s}» no està instal·lada. Aquí hi ha la llista d'aplicacions instal·lades: {all_apps}",
+ "app_not_installed": "No s'ha trobat l'aplicació «{app:s}» en la llista d'aplicacions instal·lades: {all_apps}",
"app_not_properly_removed": "{app:s} no s'ha pogut suprimir correctament",
- "app_package_need_update": "El paquet de l'aplicació {app} ha de ser actualitzat per poder seguir els canvis de YunoHost",
"app_removed": "{app:s} ha estat suprimida",
"app_requirements_checking": "Verificació dels paquets requerits per {app}…",
- "app_requirements_failed": "No es poden satisfer els requeriments per {app}: {error}",
"app_requirements_unmeet": "No es compleixen els requeriments per {app}, el paquet {pkgname} ({version}) ha de ser {spec}",
"app_sources_fetch_failed": "No s'han pogut carregar els fitxers font, l'URL és correcta?",
"app_unknown": "Aplicació desconeguda",
"app_unsupported_remote_type": "El tipus remot utilitzat per l'aplicació no està suportat",
- "app_upgrade_app_name": "Actualitzant l'aplicació {app}…",
- "app_upgrade_failed": "No s'ha pogut actualitzar {app:s}",
+ "app_upgrade_app_name": "Actualitzant {app}…",
+ "app_upgrade_failed": "No s'ha pogut actualitzar {app:s}: {error}",
"app_upgrade_some_app_failed": "No s'han pogut actualitzar algunes aplicacions",
- "app_upgraded": "{app:s} ha estat actualitzada",
- "appslist_corrupted_json": "No s'han pogut carregar les llistes d'aplicacions. Sembla que {filename:s} està danyat.",
- "appslist_could_not_migrate": "No s'ha pogut migrar la llista d'aplicacions {appslist:s}! No s'ha pogut analitzar la URL... L'antic cronjob s'ha guardat a {bkp_file:s}.",
- "appslist_fetched": "S'ha descarregat la llista d'aplicacions {appslist:s} correctament",
- "appslist_migrating": "Migrant la llista d'aplicacions {appslist:s}…",
- "appslist_name_already_tracked": "Ja hi ha una llista d'aplicacions registrada amb el nom {name:s}.",
- "appslist_removed": "S'ha eliminat la llista d'aplicacions {appslist:s}",
- "appslist_retrieve_bad_format": "L'arxiu obtingut per la llista d'aplicacions {appslist:s} no és vàlid",
- "appslist_retrieve_error": "No s'ha pogut obtenir la llista d'aplicacions remota {appslist:s}: {error:s}",
- "appslist_unknown": "La llista d'aplicacions {appslist:s} es desconeguda.",
- "appslist_url_already_tracked": "Ja hi ha una llista d'aplicacions registrada amb al URL {url:s}.",
- "ask_current_admin_password": "Contrasenya d'administrador actual",
- "ask_email": "Correu electrònic",
+ "app_upgraded": "S'ha actualitzat {app:s}",
+ "ask_email": "Adreça de correu electrònic",
"ask_firstname": "Nom",
"ask_lastname": "Cognom",
- "ask_list_to_remove": "Llista per a suprimir",
"ask_main_domain": "Domini principal",
"ask_new_admin_password": "Nova contrasenya d'administrador",
"ask_password": "Contrasenya",
- "ask_path": "Camí",
- "backup_abstract_method": "Encara no s'ha implementat aquest mètode de copia de seguretat",
- "backup_action_required": "S'ha d'especificar què s'ha de guardar",
+ "backup_abstract_method": "Encara està per implementar aquest mètode de còpia de seguretat",
"backup_app_failed": "No s'ha pogut fer la còpia de seguretat de l'aplicació \"{app:s}\"",
"backup_applying_method_borg": "Enviant tots els fitxers de la còpia de seguretat al repositori borg-backup…",
"backup_applying_method_copy": "Còpia de tots els fitxers a la còpia de seguretat…",
"backup_applying_method_custom": "Crida del mètode de còpia de seguretat personalitzat \"{method:s}\"…",
- "backup_applying_method_tar": "Creació de l'arxiu tar de la còpia de seguretat…",
- "backup_archive_app_not_found": "L'aplicació \"{app:s}\" no es troba dins l'arxiu de la còpia de seguretat",
+ "backup_applying_method_tar": "Creació de l'arxiu TAR de la còpia de seguretat…",
+ "backup_archive_app_not_found": "No s'ha pogut trobar l'aplicació «{app:s}» dins l'arxiu de la còpia de seguretat",
"backup_archive_broken_link": "No s'ha pogut accedir a l'arxiu de la còpia de seguretat (enllaç invàlid cap a {path:s})",
- "backup_archive_mount_failed": "No s'ha pogut carregar l'arxiu de la còpia de seguretat",
- "backup_archive_name_exists": "Ja hi ha una còpia de seguretat amb aquest nom",
+ "backup_archive_name_exists": "Ja hi ha una còpia de seguretat amb aquest nom.",
"backup_archive_name_unknown": "Còpia de seguretat local \"{name:s}\" desconeguda",
"backup_archive_open_failed": "No s'ha pogut obrir l'arxiu de la còpia de seguretat",
- "backup_archive_system_part_not_available": "La part \"{part:s}\" del sistema no està disponible en aquesta copia de seguretat",
- "backup_archive_writing_error": "No es poden afegir arxius a l'arxiu comprimit de la còpia de seguretat",
- "backup_ask_for_copying_if_needed": "Alguns fitxers no s'han pogut preparar per la còpia de seguretat utilitzant el mètode que evita malgastar espai del sistema temporalment. Per fer la còpia de seguretat, s'han d'utilitzar {size:s}MB temporalment. Hi esteu d'acord?",
+ "backup_archive_system_part_not_available": "La part «{part:s}» del sistema no està disponible en aquesta copia de seguretat",
+ "backup_archive_writing_error": "No es poden afegir els arxius «{source:s}» (anomenats en l'arxiu «{dest:s}») a l'arxiu comprimit de la còpia de seguretat «{archive:s}»",
+ "backup_ask_for_copying_if_needed": "Voleu fer la còpia de seguretat utilitzant {size:s} MB temporalment? (S'utilitza aquest mètode ja que alguns dels fitxers no s'han pogut preparar utilitzar un mètode més eficient.)",
"backup_borg_not_implemented": "El mètode de còpia de seguretat Borg encara no està implementat",
- "backup_cant_mount_uncompress_archive": "No es pot carregar en mode de lectura només el directori de l'arxiu descomprimit",
+ "backup_cant_mount_uncompress_archive": "No es pot carregar l'arxiu descomprimit com a protegit contra escriptura",
"backup_cleaning_failed": "No s'ha pogut netejar el directori temporal de la còpia de seguretat",
"backup_copying_to_organize_the_archive": "Copiant {size:s}MB per organitzar l'arxiu",
"backup_couldnt_bind": "No es pot lligar {src:s} amb {dest:s}.",
"backup_created": "S'ha creat la còpia de seguretat",
- "backup_creating_archive": "Creant l'arxiu de la còpia de seguretat…",
"aborting": "Avortant.",
- "app_not_upgraded": "Les següents aplicacions no s'han actualitzat: {apps}",
- "app_start_install": "instal·lant l'aplicació {app}…",
- "app_start_remove": "Eliminant l'aplicació {app}…",
- "app_start_backup": "Recuperant els fitxers pels que s'ha de fer una còpia de seguretat per {app}…",
- "app_start_restore": "Recuperant l'aplicació {app}…",
+ "app_not_upgraded": "L'aplicació «{failed_app}» no s'ha pogut actualitzar, i com a conseqüència l'actualització de les següents aplicacions ha estat cancel·lada: {apps}",
+ "app_start_install": "instal·lant l'aplicació «{app}»…",
+ "app_start_remove": "Eliminant l'aplicació «{app}»…",
+ "app_start_backup": "Recuperant els fitxers pels que s'ha de fer una còpia de seguretat per «{app}»…",
+ "app_start_restore": "Recuperant l'aplicació «{app}»…",
"app_upgrade_several_apps": "S'actualitzaran les següents aplicacions: {apps}",
"ask_new_domain": "Nou domini",
"ask_new_path": "Nou camí",
- "backup_actually_backuping": "S'està creant un arxiu de còpia de seguretat a partir dels fitxers recuperats…",
- "backup_creation_failed": "Ha fallat la creació de la còpia de seguretat",
+ "backup_actually_backuping": "Creant un arxiu de còpia de seguretat a partir dels fitxers recuperats…",
+ "backup_creation_failed": "No s'ha pogut crear l'arxiu de la còpia de seguretat",
"backup_csv_addition_failed": "No s'han pogut afegir fitxers per a fer-ne la còpia de seguretat al fitxer CSV",
- "backup_csv_creation_failed": "No s'ha pogut crear el fitxer CSV necessari per a futures operacions de recuperació",
- "backup_custom_backup_error": "El mètode de còpia de seguretat personalitzat ha fallat a l'etapa \"backup\"",
- "backup_custom_mount_error": "El mètode de còpia de seguretat personalitzat ha fallat a l'etapa \"mount\"",
- "backup_custom_need_mount_error": "El mètode de còpia de seguretat personalitzat ha fallat a l'etapa \"need_mount\"",
- "backup_delete_error": "No s'ha pogut suprimir \"{path:s}\"",
+ "backup_csv_creation_failed": "No s'ha pogut crear el fitxer CSV necessari per a la restauració",
+ "backup_custom_backup_error": "El mètode de còpia de seguretat personalitzat ha fallat a l'etapa «backup»",
+ "backup_custom_mount_error": "El mètode de còpia de seguretat personalitzat ha fallat a l'etapa «mount»",
+ "backup_delete_error": "No s'ha pogut suprimir «{path:s}»",
"backup_deleted": "S'ha suprimit la còpia de seguretat",
- "backup_extracting_archive": "Extraient l'arxiu de la còpia de seguretat…",
- "backup_hook_unknown": "Script de còpia de seguretat \"{hook:s}\" desconegut",
- "backup_invalid_archive": "Arxiu de còpia de seguretat no vàlid",
- "backup_method_borg_finished": "La còpia de seguretat a borg ha acabat",
+ "backup_hook_unknown": "Script de còpia de seguretat «{hook:s}» desconegut",
+ "backup_invalid_archive": "Aquest no és un arxiu de còpia de seguretat",
+ "backup_method_borg_finished": "La còpia de seguretat a Borg ha acabat",
"backup_method_copy_finished": "La còpia de la còpia de seguretat ha acabat",
"backup_method_custom_finished": "El mètode de còpia de seguretat personalitzat \"{method:s}\" ha acabat",
- "backup_method_tar_finished": "S'ha creat l'arxiu de còpia de seguretat tar",
+ "backup_method_tar_finished": "S'ha creat l'arxiu de còpia de seguretat TAR",
"backup_mount_archive_for_restore": "Preparant l'arxiu per la restauració…",
- "good_practices_about_user_password": "Esteu a punt de definir una nova contrasenya d'usuari. La contrasenya ha de tenir un mínim de 8 caràcters ; tot i que és de bona pràctica utilitzar una contrasenya més llarga (és a dir una frase de contrasenya) i/o utilitzar diferents tipus de caràcters (majúscules, minúscules, dígits i caràcters especials).",
+ "good_practices_about_user_password": "Esteu a punt de definir una nova contrasenya d'usuari. La contrasenya ha de tenir un mínim de 8 caràcters; tot i que és de bona pràctica utilitzar una contrasenya més llarga (és a dir una frase de contrasenya) i/o utilitzar diferents tipus de caràcters (majúscules, minúscules, dígits i caràcters especials).",
"password_listed": "Aquesta contrasenya és una de les més utilitzades en el món. Si us plau utilitzeu-ne una més única.",
"password_too_simple_1": "La contrasenya ha de tenir un mínim de 8 caràcters",
"password_too_simple_2": "La contrasenya ha de tenir un mínim de 8 caràcters i ha de contenir dígits, majúscules i minúscules",
@@ -115,71 +90,58 @@
"password_too_simple_4": "La contrasenya ha de tenir un mínim de 12 caràcters i tenir dígits, majúscules, minúscules i caràcters especials",
"backup_no_uncompress_archive_dir": "El directori de l'arxiu descomprimit no existeix",
"backup_nothings_done": "No hi ha res a guardar",
- "backup_output_directory_forbidden": "Directori de sortida no permès. Les còpies de seguretat no es poden crear ni dins els directoris /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var ni dins els subdirectoris /home/yunohost.backup/archives",
- "backup_output_directory_not_empty": "El directori de sortida no està buit",
+ "backup_output_directory_forbidden": "Escolliu un directori de sortida different. Les còpies de seguretat no es poden crear ni dins els directoris /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var ni dins els subdirectoris /home/yunohost.backup/archives",
+ "backup_output_directory_not_empty": "Heu d'escollir un directori de sortida buit",
"backup_output_directory_required": "Heu d'especificar un directori de sortida per la còpia de seguretat",
- "backup_output_symlink_dir_broken": "Teniu un enllaç simbòlic trencat en lloc del directori dels arxius '{path:s}'. Pot ser teniu una configuració per la còpia de seguretat específica en un altre sistema de fitxers, si és el cas segurament heu oblidat muntar o connectar el disc dur o la clau USB.",
- "backup_php5_to_php7_migration_may_fail": "No s'ha pogut convertir l'arxiu per suportar php7, la restauració de les vostres aplicacions pot fallar (raó: {error:s})",
+ "backup_output_symlink_dir_broken": "El directori del arxiu «{path:s}» es un enllaç simbòlic trencat. Pot ser heu oblidat muntar, tornar a muntar o connectar el mitja d'emmagatzematge al que apunta.",
+ "backup_php5_to_php7_migration_may_fail": "No s'ha pogut convertir l'arxiu per suportar PHP 7, pot ser que no es puguin restaurar les vostres aplicacions PHP (raó: {error:s})",
"backup_running_hooks": "Executant els scripts de la còpia de seguretat…",
"backup_system_part_failed": "No s'ha pogut fer la còpia de seguretat de la part \"{part:s}\" del sistema",
- "backup_unable_to_organize_files": "No s'han pogut organitzar els fitxers dins de l'arxiu amb el mètode ràpid",
- "backup_with_no_backup_script_for_app": "L'aplicació {app:s} no té un script de còpia de seguretat. Serà ignorat.",
- "backup_with_no_restore_script_for_app": "L'aplicació {app:s} no té un script de restauració, no podreu restaurar automàticament la còpia de seguretat d'aquesta aplicació.",
- "certmanager_acme_not_configured_for_domain": "El certificat pel domini {domain:s} sembla que no està instal·lat correctament. Si us plau executeu primer cert-install per aquest domini.",
- "certmanager_attempt_to_renew_nonLE_cert": "El certificat pel domini {domain:s} no ha estat emès per Let's Encrypt. No es pot renovar automàticament!",
- "certmanager_attempt_to_renew_valid_cert": "El certificat pel domini {domain:s} està a punt de caducar! (Utilitzeu --force si sabeu el que esteu fent)",
+ "backup_unable_to_organize_files": "No s'ha pogut utilitzar el mètode ràpid per organitzar els fitxers dins de l'arxiu",
+ "backup_with_no_backup_script_for_app": "L'aplicació «{app:s}» no té un script de còpia de seguretat. Serà ignorat.",
+ "backup_with_no_restore_script_for_app": "L'aplicació «{app:s}» no té un script de restauració, no podreu restaurar automàticament la còpia de seguretat d'aquesta aplicació.",
+ "certmanager_acme_not_configured_for_domain": "El certificat pel domini «{domain:s}» sembla que no està instal·lat correctament. Si us plau executeu primer «cert-install» per aquest domini.",
+ "certmanager_attempt_to_renew_nonLE_cert": "El certificat pel domini «{domain:s}» no ha estat emès per Let's Encrypt. No es pot renovar automàticament!",
+ "certmanager_attempt_to_renew_valid_cert": "El certificat pel domini «{domain:s}» està a punt de caducar! (Utilitzeu --force si sabeu el que esteu fent)",
"certmanager_attempt_to_replace_valid_cert": "Esteu intentant sobreescriure un certificat correcte i vàlid pel domini {domain:s}! (Utilitzeu --force per ometre)",
"certmanager_cannot_read_cert": "S'ha produït un error al intentar obrir el certificat actual pel domini {domain:s} (arxiu: {file:s}), raó: {reason:s}",
- "certmanager_cert_install_success": "S'ha instal·lat correctament un certificat Let's Encrypt pel domini {domain:s}!",
- "certmanager_cert_install_success_selfsigned": "S'ha instal·lat correctament un certificat auto-signat pel domini {domain:s}!",
- "certmanager_cert_renew_success": "S'ha renovat correctament el certificat Let's Encrypt pel domini {domain:s}!",
+ "certmanager_cert_install_success": "S'ha instal·lat correctament un certificat Let's Encrypt pel domini «{domain:s}»",
+ "certmanager_cert_install_success_selfsigned": "S'ha instal·lat correctament un certificat auto-signat pel domini «{domain:s}»",
+ "certmanager_cert_renew_success": "S'ha renovat correctament el certificat Let's Encrypt pel domini «{domain:s}»",
"certmanager_cert_signing_failed": "No s'ha pogut firmar el nou certificat",
- "certmanager_certificate_fetching_or_enabling_failed": "Sembla que l'activació del nou certificat per {domain:s} ha fallat…",
- "certmanager_conflicting_nginx_file": "No s'ha pogut preparar el domini per al desafiament ACME: l'arxiu de configuració nginx {filepath:s} entra en conflicte i s'ha d'eliminar primer",
+ "certmanager_certificate_fetching_or_enabling_failed": "Sembla que utilitzar el nou certificat per {domain:s} ha fallat…",
+ "certmanager_conflicting_nginx_file": "No s'ha pogut preparar el domini per al desafiament ACME: l'arxiu de configuració NGINX {filepath:s} entra en conflicte i s'ha d'eliminar primer",
"certmanager_couldnt_fetch_intermediate_cert": "S'ha exhaurit el temps d'esperar al intentar recollir el certificat intermedi des de Let's Encrypt. La instal·lació/renovació del certificat s'ha cancel·lat - torneu a intentar-ho més tard.",
- "certmanager_domain_cert_not_selfsigned": "El certificat pel domini {domain:s} no és auto-signat Esteu segur de voler canviar-lo? (Utilitzeu --force per fer-ho)",
- "certmanager_domain_dns_ip_differs_from_public_ip": "El registre DNS \"A\" pel domini {domain:s} és diferent a l'adreça IP d'aquest servidor. Si heu modificat recentment el registre A, si us plau espereu a que es propagui (hi ha eines per verificar la propagació disponibles a internet). (Si sabeu el que esteu fent, podeu utilitzar --no-checks per desactivar aquestes comprovacions.)",
- "certmanager_domain_http_not_working": "Sembla que el domini {domain:s} no és accessible via HTTP. Si us plau verifiqueu que les configuracions DNS i nginx siguin correctes",
- "certmanager_domain_not_resolved_locally": "El domini {domain:s} no es pot resoldre dins del vostre servidor YunoHost. Això pot passar si heu modificat recentment el registre DNS. Si és així, si us plau espereu unes hores per a que es propagui. Si el problema continua, considereu afegir {domain:s} a /etc/hosts. (Si sabeu el que esteu fent, podeu utilitzar --no-checks per desactivar aquestes comprovacions.)",
- "certmanager_domain_unknown": "Domini desconegut {domain:s}",
- "certmanager_error_no_A_record": "No s'ha trobat cap registre DNS \"A\" per {domain:s}. Heu de fer que el vostre nom de domini apunti cap a la vostra màquina per tal de poder instal·lar un certificat Let's Encrypt! (Si sabeu el que esteu fent, podeu utilitzar --no-checks per desactivar aquestes comprovacions.)",
+ "certmanager_domain_cert_not_selfsigned": "El certificat pel domini {domain:s} no és auto-signat Esteu segur de voler canviar-lo? (Utilitzeu «--force» per fer-ho)",
+ "certmanager_domain_dns_ip_differs_from_public_ip": "El registre DNS \"A\" pel domini «{domain:s}» és diferent a l'adreça IP d'aquest servidor. Si heu modificat recentment el registre A, si us plau espereu a que es propagui (hi ha eines per verificar la propagació disponibles a internet). (Si sabeu el que esteu fent, podeu utilitzar «--no-checks» per desactivar aquestes comprovacions.)",
+ "certmanager_domain_http_not_working": "Sembla que el domini {domain:s} no és accessible via HTTP. Verifiqueu que les configuracions DNS i NGINX siguin correctes",
+ "certmanager_domain_unknown": "Domini desconegut «{domain:s}»",
+ "certmanager_error_no_A_record": "No s'ha trobat cap registre DNS «A» per «{domain:s}». Heu de fer que el vostre nom de domini apunti cap a la vostra màquina per tal de poder instal·lar un certificat Let's Encrypt. (Si sabeu el que esteu fent, podeu utilitzar «--no-checks» per desactivar aquestes comprovacions.)",
"certmanager_hit_rate_limit": "S'han emès massa certificats recentment per aquest mateix conjunt de dominis {domain:s}. Si us plau torneu-ho a intentar més tard. Consulteu https://letsencrypt.org/docs/rate-limits/ per obtenir més detalls",
- "certmanager_http_check_timeout": "S'ha exhaurit el temps d'espera quan el servidor ha intentat contactar amb ell mateix via HTTP utilitzant la seva adreça IP pública (domini domain:s} amb IP {ip:s}). Pot ser degut a hairpinning o a que el talla focs/router al que està connectat el servidor estan mal configurats.",
+ "certmanager_http_check_timeout": "S'ha exhaurit el temps d'espera quan el servidor ha intentat contactar amb ell mateix via HTTP utilitzant la seva adreça IP pública (domini «{domain:s}» amb IP «{ip:s}»). Pot ser degut a hairpinning o a que el talla focs/router al que està connectat el servidor estan mal configurats.",
"certmanager_no_cert_file": "No s'ha pogut llegir l'arxiu del certificat pel domini {domain:s} (fitxer: {file:s})",
"certmanager_self_ca_conf_file_not_found": "No s'ha trobat el fitxer de configuració per l'autoritat del certificat auto-signat (fitxer: {file:s})",
"certmanager_unable_to_parse_self_CA_name": "No s'ha pogut analitzar el nom de l'autoritat del certificat auto-signat (fitxer: {file:s})",
- "confirm_app_install_warning": "Atenció: aquesta aplicació funciona però no està ben integrada amb YunoHost. Algunes característiques com la autenticació única i la còpia de seguretat/restauració poden no estar disponibles. Voleu instal·lar-la de totes maneres? [{answers:s}] ",
- "confirm_app_install_danger": "ATENCIÓ! Aquesta aplicació encara és experimental (si no és que no funciona directament) i és probable que trenqui el sistema! No hauríeu d'instal·lar-la a no ser que sapigueu el que feu. Esteu segurs de voler córrer aquest risc? [{answers:s}] ",
- "confirm_app_install_thirdparty": "ATENCIÓ! La instal·lació d'aplicacions de terceres parts pot comprometre la integritat i seguretat del seu sistema. Faci-ho sota la seva responsabilitat.No hauríeu d'instal·lar-ne a no ser que sapigueu el que feu. Esteu segurs de voler córrer aquest risc? [{answers:s}] ",
+ "confirm_app_install_warning": "Atenció: Aquesta aplicació funciona, però no està ben integrada amb YunoHost. Algunes característiques com la autenticació única i la còpia de seguretat/restauració poden no estar disponibles. Voleu instal·lar-la de totes maneres? [{answers:s}] ",
+ "confirm_app_install_danger": "PERILL! Aquesta aplicació encara és experimental (si no és que no funciona directament)! No hauríeu d'instal·lar-la a no ser que sapigueu el que feu. No obtindreu CAP AJUDA si l'aplicació no funciona o trenca el sistema… Si accepteu el risc, escriviu «{answers:s}»",
+ "confirm_app_install_thirdparty": "PERILL! Aquesta aplicació no es part del catàleg d'aplicacions de YunoHost. La instal·lació d'aplicacions de terceres parts pot comprometre la integritat i seguretat del seu sistema. No hauríeu d'instal·lar-ne a no ser que sapigueu el que feu. No obtindreu CAP AJUDA si l'aplicació no funciona o trenca el sistema… Si accepteu el risc, escriviu «{answers:s}»",
"custom_app_url_required": "Heu de especificar una URL per actualitzar la vostra aplicació personalitzada {app:s}",
- "custom_appslist_name_required": "Heu d'especificar un nom per la vostra llista d'aplicacions personalitzada",
- "diagnosis_debian_version_error": "No s'ha pogut obtenir la versió Debian: {error}",
- "diagnosis_kernel_version_error": "No s'ha pogut obtenir la versió del nucli: {error}",
- "diagnosis_monitor_disk_error": "No es poden monitorar els discs: {error}",
- "diagnosis_monitor_network_error": "No es pot monitorar la xarxa: {error}",
- "diagnosis_monitor_system_error": "No es pot monitorar el sistema: {error}",
- "diagnosis_no_apps": "No hi ha cap aplicació instal·lada",
"admin_password_too_long": "Trieu una contrasenya de menys de 127 caràcters",
- "dpkg_is_broken": "No es pot fer això en aquest instant perquè dpkg/apt (els gestors de paquets del sistema) sembla estar mal configurat... Podeu intentar solucionar-ho connectant-vos per ssh i executant \"sudo dpkg --configure -a\".",
- "dnsmasq_isnt_installed": "sembla que dnsmasq no està instal·lat, executeu \"apt-get remove bind9 && apt-get install dnsmasq\"",
- "domain_cannot_remove_main": "No es pot eliminar el domini principal. S'ha d'establir un nou domini primer",
+ "dpkg_is_broken": "No es pot fer això en aquest instant perquè dpkg/APT (els gestors de paquets del sistema) sembla estar mal configurat… Podeu intentar solucionar-ho connectant-vos per SSH i executant «sudo dpkg --configure -a».",
+ "domain_cannot_remove_main": "No es pot eliminar «{domain:s}» ja que és el domini principal, primer s'ha d'establir un nou domini principal utilitzant «yunohost domain main-domain -n »; aquí hi ha una llista dels possibles dominis: {other_domains:s}",
"domain_cert_gen_failed": "No s'ha pogut generar el certificat",
"domain_created": "S'ha creat el domini",
- "domain_creation_failed": "No s'ha pogut crear el domini",
+ "domain_creation_failed": "No s'ha pogut crear el domini {domain}: {error}",
"domain_deleted": "S'ha eliminat el domini",
- "domain_deletion_failed": "No s'ha pogut eliminar el domini",
+ "domain_deletion_failed": "No s'ha pogut eliminar el domini {domain}: {error}",
"domain_exists": "El domini ja existeix",
- "app_action_cannot_be_ran_because_required_services_down": "Aquesta aplicació necessita serveis que estan aturats. Abans de continuar, hauríeu d'intentar arrancar de nou els serveis següents (i també investigar perquè estan aturats) : {services}",
+ "app_action_cannot_be_ran_because_required_services_down": "Aquests serveis necessaris haurien d'estar funcionant per poder executar aquesta acció: {services} Intenteu reiniciar-los per continuar (i possiblement investigar perquè estan aturats).",
"domain_dns_conf_is_just_a_recommendation": "Aquesta ordre mostra la configuració *recomanada*. En cap cas fa la configuració del DNS. És la vostra responsabilitat configurar la zona DNS en el vostre registrar en acord amb aquesta recomanació.",
"domain_dyndns_already_subscribed": "Ja us heu subscrit a un domini DynDNS",
- "domain_dyndns_dynette_is_unreachable": "No s'ha pogut abastar la dynette YunoHost, o bé YunoHost no està connectat a internet correctament o bé el servidor dynette està caigut. Error: {error}",
- "domain_dyndns_invalid": "Domini no vàlid per utilitzar amb DynDNS",
"domain_dyndns_root_unknown": "Domini DynDNS principal desconegut",
- "domain_hostname_failed": "No s'ha pogut establir un nou nom d'amfitrió. Això podria causar problemes més tard (no és segur ... podria no passar res).",
+ "domain_hostname_failed": "No s'ha pogut establir un nou nom d'amfitrió. Això podria causar problemes més tard (podria no passar res).",
"domain_uninstall_app_first": "Hi ha una o més aplicacions instal·lades en aquest domini. Desinstal·leu les abans d'eliminar el domini",
"domain_unknown": "Domini desconegut",
- "domain_zone_exists": "El fitxer de zona DNS ja existeix",
- "domain_zone_not_found": "No s'ha trobat el fitxer de zona DNS pel domini {:s}",
"domains_available": "Dominis disponibles:",
"done": "Fet",
"downloading": "Descarregant…",
@@ -187,68 +149,61 @@
"dyndns_could_not_check_available": "No s'ha pogut verificar la disponibilitat de {domain:s} a {provider:s}.",
"dyndns_ip_update_failed": "No s'ha pogut actualitzar l'adreça IP al DynDNS",
"dyndns_ip_updated": "S'ha actualitzat l'adreça IP al DynDNS",
- "dyndns_key_generating": "S'està generant la clau DNS, això pot trigar una estona…",
+ "dyndns_key_generating": "S'està generant la clau DNS… això pot trigar una estona.",
"dyndns_key_not_found": "No s'ha trobat la clau DNS pel domini",
"dyndns_no_domain_registered": "No hi ha cap domini registrat amb DynDNS",
"dyndns_registered": "S'ha registrat el domini DynDNS",
"dyndns_registration_failed": "No s'ha pogut registrar el domini DynDNS: {error:s}",
- "dyndns_domain_not_provided": "El proveïdor {provider:s} no pot oferir el domini {domain:s}.",
+ "dyndns_domain_not_provided": "El proveïdor de DynDNS {provider:s} no pot oferir el domini {domain:s}.",
"dyndns_unavailable": "El domini {domain:s} no està disponible.",
"executing_command": "Execució de l'ordre « {command:s} »…",
"executing_script": "Execució de l'script « {script:s} »…",
"extracting": "Extracció en curs…",
- "dyndns_cron_installed": "S'ha instal·lat la tasca cron pel DynDNS",
+ "dyndns_cron_installed": "S'ha creat la tasca cron pel DynDNS",
"dyndns_cron_remove_failed": "No s'ha pogut eliminar la tasca cron per a DynDNS: {error}",
"dyndns_cron_removed": "S'ha eliminat la tasca cron pel DynDNS",
- "experimental_feature": "Atenció: aquesta funcionalitat és experimental i no es considera estable, no s'ha d'utilitzar a excepció de saber el que esteu fent.",
+ "experimental_feature": "Atenció: Aquesta funcionalitat és experimental i no es considera estable, no s'ha d'utilitzar a excepció de saber el que esteu fent.",
"field_invalid": "Camp incorrecte « {:s} »",
"file_does_not_exist": "El camí {path:s} no existeix.",
- "firewall_reload_failed": "No s'ha pogut tornar a carregar el tallafoc",
- "firewall_reloaded": "S'ha tornat a carregar el tallafoc",
- "firewall_rules_cmd_failed": "No s'han pogut aplicar algunes regles del tallafoc. Mireu el registre per a més informació.",
- "format_datetime_short": "%d/%m/%Y %H:%M",
- "global_settings_bad_choice_for_enum": "Opció pel paràmetre {setting:s} incorrecta, s'ha rebut «{choice:s}» però les opcions disponibles són: {available_choices:s}",
+ "firewall_reload_failed": "No s'ha pogut tornar a carregar el tallafocs",
+ "firewall_reloaded": "S'ha tornat a carregar el tallafocs",
+ "firewall_rules_cmd_failed": "No s'han pogut aplicar algunes regles del tallafocs. Més informació en el registre.",
+ "global_settings_bad_choice_for_enum": "Opció pel paràmetre {setting:s} incorrecta, s'ha rebut «{choice:s}», però les opcions disponibles són: {available_choices:s}",
"global_settings_bad_type_for_setting": "El tipus del paràmetre {setting:s} és incorrecte. S'ha rebut {received_type:s}, però s'esperava {expected_type:s}",
"global_settings_cant_open_settings": "No s'ha pogut obrir el fitxer de configuració, raó: {reason:s}",
"global_settings_cant_serialize_settings": "No s'ha pogut serialitzar les dades de configuració, raó: {reason:s}",
"global_settings_cant_write_settings": "No s'ha pogut escriure el fitxer de configuració, raó: {reason:s}",
"global_settings_key_doesnt_exists": "La clau « {settings_key:s} » no existeix en la configuració global, podeu veure totes les claus disponibles executant « yunohost settings list »",
- "global_settings_reset_success": "Èxit. S'ha fet una còpia de seguretat de la configuració anterior a {path:s}",
+ "global_settings_reset_success": "S'ha fet una còpia de seguretat de la configuració anterior a {path:s}",
"global_settings_setting_example_bool": "Exemple d'opció booleana",
"global_settings_setting_example_enum": "Exemple d'opció de tipus enumeració",
"global_settings_setting_example_int": "Exemple d'opció de tipus enter",
"global_settings_setting_example_string": "Exemple d'opció de tipus cadena",
- "global_settings_setting_security_nginx_compatibility": "Solució de compromís entre compatibilitat i seguretat pel servidor web nginx. Afecta els criptògrafs (i altres aspectes relacionats amb la seguretat)",
+ "global_settings_setting_security_nginx_compatibility": "Solució de compromís entre compatibilitat i seguretat pel servidor web NGINX. Afecta els criptògrafs (i altres aspectes relacionats amb la seguretat)",
"global_settings_setting_security_password_admin_strength": "Robustesa de la contrasenya d'administrador",
"global_settings_setting_security_password_user_strength": "Robustesa de la contrasenya de l'usuari",
"global_settings_setting_security_ssh_compatibility": "Solució de compromís entre compatibilitat i seguretat pel servidor SSH. Afecta els criptògrafs (i altres aspectes relacionats amb la seguretat)",
- "global_settings_unknown_setting_from_settings_file": "Clau de configuració desconeguda: «{setting_key:s}», refusant-la i guardant-la a /etc/yunohost/settings-unknown.json",
+ "global_settings_unknown_setting_from_settings_file": "Clau de configuració desconeguda: «{setting_key:s}», refusada i guardada a /etc/yunohost/settings-unknown.json",
"global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "Permetre la clau d'hoste DSA (obsolet) per la configuració del servei SSH",
"global_settings_unknown_type": "Situació inesperada, la configuració {setting:s} sembla tenir el tipus {unknown_type:s} però no és un tipus reconegut pel sistema.",
- "good_practices_about_admin_password": "Esteu a punt de definir una nova contrasenya d'administrador. La contrasenya ha de tenir un mínim de 8 caràcters ; tot i que és de bona pràctica utilitzar una contrasenya més llarga (és a dir una frase de contrasenya) i/o utilitzar diferents tipus de caràcters (majúscules, minúscules, dígits i caràcters especials).",
- "hook_exec_failed": "No s'ha pogut executar l'script: {path:s}",
- "hook_exec_not_terminated": "L'execució de l'script « {path:s} » no s'ha acabat correctament",
- "hook_json_return_error": "No s'ha pogut llegir el retorn de l'script {path:s}. Error: {msg:s}. Contingut en brut: {raw_content}",
- "hook_list_by_invalid": "Propietat per llistar les accions invàlida",
+ "good_practices_about_admin_password": "Esteu a punt de definir una nova contrasenya d'administrador. La contrasenya ha de tenir un mínim de 8 caràcters; tot i que és de bona pràctica utilitzar una contrasenya més llarga (és a dir una frase de contrasenya) i/o utilitzar diferents tipus de caràcters (majúscules, minúscules, dígits i caràcters especials).",
+ "hook_exec_failed": "No s'ha pogut executar el script: {path:s}",
+ "hook_exec_not_terminated": "El script no s'ha acabat correctament: {path:s}",
+ "hook_json_return_error": "No s'ha pogut llegir el retorn del script {path:s}. Error: {msg:s}. Contingut en brut: {raw_content}",
+ "hook_list_by_invalid": "Aquesta propietat no es pot utilitzar per llistar els hooks",
"hook_name_unknown": "Nom de script « {name:s} » desconegut",
"installation_complete": "Instal·lació completada",
- "installation_failed": "Ha fallat la instal·lació",
- "invalid_url_format": "Format d'URL invàlid",
+ "installation_failed": "Ha fallat alguna cosa amb la instal·lació",
"ip6tables_unavailable": "No podeu modificar les ip6tables aquí. O bé sou en un contenidor o bé el vostre nucli no és compatible amb aquesta opció",
"iptables_unavailable": "No podeu modificar les iptables aquí. O bé sou en un contenidor o bé el vostre nucli no és compatible amb aquesta opció",
- "log_corrupted_md_file": "El fitxer de metadades yaml associat amb els registres està malmès: « {md_file} »\nError: {error}",
+ "log_corrupted_md_file": "El fitxer de metadades YAML associat amb els registres està malmès: « {md_file} »\nError: {error}",
"log_category_404": "La categoria de registres « {category} » no existeix",
"log_link_to_log": "El registre complet d'aquesta operació: «{desc}»",
"log_help_to_get_log": "Per veure el registre de l'operació « {desc} », utilitzeu l'ordre « yunohost log display {name} »",
- "log_link_to_failed_log": "L'operació « {dec} » ha fallat! Per obtenir ajuda, proveïu el registre complete de l'operació clicant aquí",
- "log_help_to_get_failed_log": "L'operació « {dec} » ha fallat! Per obtenir ajuda, compartiu el registre complete de l'operació utilitzant l'ordre « yunohost log display {name} --share »",
+ "log_link_to_failed_log": "No s'ha pogut completar l'operació « {desc} ». Per obtenir ajuda, proveïu el registre complete de l'operació clicant aquí",
+ "log_help_to_get_failed_log": "No s'ha pogut completar l'operació « {desc} ». Per obtenir ajuda, compartiu el registre complete de l'operació utilitzant l'ordre « yunohost log display {name} --share »",
"log_does_exists": "No hi ha cap registre per l'operació amb el nom« {log} », utilitzeu « yunohost log list » per veure tots els registre d'operació disponibles",
"log_operation_unit_unclosed_properly": "L'operació no s'ha tancat de forma correcta",
- "log_app_addaccess": "Afegir accés a « {} »",
- "log_app_removeaccess": "Suprimeix accés a « {} »",
- "log_app_clearaccess": "Suprimeix tots els accessos a « {} »",
- "log_app_fetchlist": "Afegeix una llista d'aplicacions",
- "log_app_removelist": "Elimina una llista d'aplicacions",
"log_app_change_url": "Canvia l'URL de l'aplicació « {} »",
"log_app_install": "Instal·la l'aplicació « {} »",
"log_app_remove": "Elimina l'aplicació « {} »",
@@ -263,139 +218,99 @@
"log_domain_remove": "Elimina el domini « {} » de la configuració del sistema",
"log_dyndns_subscribe": "Subscriure's a un subdomini YunoHost « {} »",
"log_dyndns_update": "Actualitza la IP associada al subdomini YunoHost « {} »",
- "log_letsencrypt_cert_install": "Instal·la el certificat Let's Encrypt al domini « {} »",
+ "log_letsencrypt_cert_install": "Instal·la un certificat Let's Encrypt al domini « {} »",
"log_selfsigned_cert_install": "Instal·la el certificat autosignat al domini « {} »",
"log_letsencrypt_cert_renew": "Renova el certificat Let's Encrypt de « {} »",
- "log_service_enable": "Activa el servei « {} »",
"log_regen_conf": "Regenera la configuració del sistema « {} »",
"log_user_create": "Afegeix l'usuari « {} »",
"log_user_delete": "Elimina l'usuari « {} »",
"log_user_update": "Actualitza la informació de l'usuari « {} »",
- "log_tools_maindomain": "Fes de « {} » el domini principal",
- "log_tools_migrations_migrate_forward": "Migrar",
- "log_tools_migrations_migrate_backward": "Migrar endarrera",
+ "log_domain_main_domain": "Fes de « {} » el domini principal",
+ "log_tools_migrations_migrate_forward": "Executa les migracions",
"log_tools_postinstall": "Fer la post instal·lació del servidor YunoHost",
"log_tools_upgrade": "Actualitza els paquets del sistema",
"log_tools_shutdown": "Apaga el servidor",
"log_tools_reboot": "Reinicia el servidor",
- "already_up_to_date": "No hi ha res a fer! Tot està al dia!",
+ "already_up_to_date": "No hi ha res a fer. Tot està actualitzat.",
"dpkg_lock_not_available": "No es pot utilitzar aquesta comanda en aquest moment ja que sembla que un altre programa està utilitzant el lock de dpkg (el gestor de paquets del sistema)",
"global_settings_setting_security_postfix_compatibility": "Solució de compromís entre compatibilitat i seguretat pel servidor Postfix. Afecta els criptògrafs (i altres aspectes relacionats amb la seguretat)",
"ldap_init_failed_to_create_admin": "La inicialització de LDAP no ha pogut crear l'usuari admin",
"ldap_initialized": "S'ha iniciat LDAP",
- "license_undefined": "indefinit",
- "mail_alias_remove_failed": "No s'han pogut eliminar els alias del correu «{mail:s}»",
- "mail_domain_unknown": "Domini d'adreça de correu «{domain:s}» desconegut",
+ "mail_alias_remove_failed": "No s'han pogut eliminar els àlies del correu «{mail:s}»",
+ "mail_domain_unknown": "El domini «{domain:s}» de l'adreça de correu no és vàlid. Utilitzeu un domini administrat per aquest servidor.",
"mail_forward_remove_failed": "No s'han pogut eliminar el reenviament de correu «{mail:s}»",
- "mailbox_used_space_dovecot_down": "S'ha d'engegar el servei de correu Dovecot per poder obtenir l'espai utilitzat per la bústia de correu",
- "mail_unavailable": "Aquesta adreça de correu esta reservada i ha de ser atribuïda automàticament el primer usuari",
- "maindomain_change_failed": "No s'ha pogut canviar el domini principal",
- "maindomain_changed": "S'ha canviat el domini principal",
- "migrate_tsig_end": "La migració cap a hmac-sha512 s'ha acabat",
- "migrate_tsig_failed": "Ha fallat la migració del domini dyndns {domain} cap a hmac-sha512, anul·lant les modificacions. Error: {error_code} - {error}",
- "migrate_tsig_start": "L'algoritme de generació de claus no es prou segur per a la signatura TSIG del domini «{domain}», començant la migració cap a un de més segur hmac-sha512",
- "migrate_tsig_wait": "Esperar 3 minuts per a que el servidor dyndns tingui en compte la nova clau…",
+ "mailbox_used_space_dovecot_down": "S'ha d'engegar el servei de correu Dovecot, per poder obtenir l'espai utilitzat per la bústia de correu",
+ "mail_unavailable": "Aquesta adreça de correu està reservada i ha de ser atribuïda automàticament el primer usuari",
+ "main_domain_change_failed": "No s'ha pogut canviar el domini principal",
+ "main_domain_changed": "S'ha canviat el domini principal",
+ "migrate_tsig_end": "La migració cap a HMAC-SHA-512 s'ha acabat",
+ "migrate_tsig_failed": "Ha fallat la migració del domini DynDNS «{domain}» cap a HMAC-SHA-512, anul·lant les modificacions. Error: {error_code}, {error}",
+ "migrate_tsig_start": "L'algoritme de generació de claus no es prou segur per a la signatura TSIG del domini «{domain}», començant la migració cap a un de més segur HMAC-SHA-512",
+ "migrate_tsig_wait": "Esperant tres minuts per a que el servidor DynDNS tingui en compte la nova clau…",
"migrate_tsig_wait_2": "2 minuts…",
"migrate_tsig_wait_3": "1 minut…",
"migrate_tsig_wait_4": "30 segons…",
- "migrate_tsig_not_needed": "Sembla que no s'utilitza cap domini dyndns, no és necessari fer cap migració!",
+ "migrate_tsig_not_needed": "Sembla que no s'utilitza cap domini DynDNS, no és necessari fer cap migració.",
"migration_description_0001_change_cert_group_to_sslcert": "Canvia els permisos del grup dels certificats de «metronome» a «ssl-cert»",
- "migration_description_0002_migrate_to_tsig_sha256": "Millora la seguretat de dyndns TSIG utilitzant SHA512 en lloc de MD5",
+ "migration_description_0002_migrate_to_tsig_sha256": "Millora la seguretat de DynDNS TSIG utilitzant SHA-512 en lloc de MD5",
"migration_description_0003_migrate_to_stretch": "Actualització del sistema a Debian Stretch i YunoHost 3.0",
"migration_description_0004_php5_to_php7_pools": "Tornar a configurar els pools PHP per utilitzar PHP 7 en lloc de PHP 5",
- "migration_description_0005_postgresql_9p4_to_9p6": "Migració de les bases de dades de postgresql 9.4 a 9.6",
+ "migration_description_0005_postgresql_9p4_to_9p6": "Migració de les bases de dades de PostgreSQL 9.4 a 9.6",
"migration_description_0006_sync_admin_and_root_passwords": "Sincronitzar les contrasenyes admin i root",
"migration_description_0007_ssh_conf_managed_by_yunohost_step1": "La configuració SSH serà gestionada per YunoHost (pas 1, automàtic)",
"migration_description_0008_ssh_conf_managed_by_yunohost_step2": "La configuració SSH serà gestionada per YunoHost (pas 2, manual)",
"migration_description_0009_decouple_regenconf_from_services": "Desvincula el mecanisme regen-conf dels serveis",
- "migration_description_0010_migrate_to_apps_json": "Elimina la appslists (desfasat) i utilitza la nova llista unificada «apps.json» en el seu lloc",
- "migration_0003_backward_impossible": "La migració Stretch no és reversible.",
+ "migration_description_0010_migrate_to_apps_json": "Elimina els catàlegs d'aplicacions obsolets i utilitza la nova llista unificada «apps.json» en el seu lloc (obsolet, substituït per la migració 13)",
"migration_0003_start": "Ha començat la migració a Stretch. Els registres estaran disponibles a {logfile}.",
"migration_0003_patching_sources_list": "Modificant el fitxer sources.lists…",
"migration_0003_main_upgrade": "Començant l'actualització principal…",
- "migration_0003_fail2ban_upgrade": "Començant l'actualització de fail2ban…",
+ "migration_0003_fail2ban_upgrade": "Començant l'actualització de Fail2Ban…",
"migration_0003_restoring_origin_nginx_conf": "El fitxer /etc/nginx/nginx.conf ha estat editat. La migració el tornarà al seu estat original... El fitxer anterior estarà disponible com a {backup_dest}.",
- "migration_0003_yunohost_upgrade": "Començant l'actualització del paquet yunohost... La migració acabarà, però l'actualització actual es farà just després. Després de completar aquesta operació, pot ser que us hagueu de tornar a connectar a la web d'administració.",
+ "migration_0003_yunohost_upgrade": "Començant l'actualització del paquet YunoHost... La migració acabarà, però l'actualització actual es farà just després. Després de completar aquesta operació, pot ser que us hagueu de tornar a connectar a la web d'administració.",
"migration_0003_not_jessie": "La distribució Debian actual no és Jessie!",
"migration_0003_system_not_fully_up_to_date": "El vostre sistema no està completament actualitzat. S'ha de fer una actualització normal abans de fer la migració a Stretch.",
- "migration_0003_still_on_jessie_after_main_upgrade": "Hi ha hagut un problema durant l'actualització principal: el sistema encara està amb Jessie!? Per investigar el problema, mireu el registres a {log}:s…",
- "migration_0003_general_warning": "Tingueu en compte que la migració és una operació delicada. Tot i que l'equip de YunoHost a fet els possibles per revisar-la i provar-la, la migració pot provocar errors en parts del sistema o aplicacions.\n\nPer tant, recomanem:\n - Fer una còpia de seguretat de les dades o aplicacions importants. Més informació a https://yunohost.org/backup;\n - Sigueu pacient un cop llençada la migració: en funció de la connexió a internet i el maquinari, pot trigar fins a unes hores per actualitzar-ho tot.\n\nD'altra banda, el port per SMTP, utilitzat per clients de correu externs (com Thunderbird o K9-Mail) ha canviat de 465 (SSL/TLS) a 587 (STARTTLS). L'antic port 465 serà tancat automàticament i el nou port 587 serà obert en el tallafocs. Tots els usuaris *hauran* d'adaptar la configuració dels clients de correu en acord amb aquests canvis!",
- "migration_0003_problematic_apps_warning": "Tingueu en compte que s'han detectat les aplicacions, possiblement, problemàtiques següents. Sembla que aquestes no s'han instal·lat des d'una applist o que no estan marcades com a «working». Per conseqüent, no podem garantir que segueixin funcionant després de l'actualització: {problematic_apps}",
+ "migration_0003_still_on_jessie_after_main_upgrade": "Hi ha hagut un problema durant l'actualització principal: El sistema encara està amb Jessie? Per investigar el problema, mireu el registres a {log}:s…",
+ "migration_0003_general_warning": "Tingueu en compte que la migració és una operació delicada. L'equip de YunoHost a fet els possibles per revisar-la i provar-la, però la migració pot provocar errors en parts del sistema o aplicacions.\n\nPer tant, es recomana:\n - Fer una còpia de seguretat de les dades o aplicacions importants. Més informació a https://yunohost.org/backup;\n - Sigueu pacient un cop llençada la migració: en funció de la connexió a internet i el maquinari, pot trigar fins a unes hores per actualitzar-ho tot.\n\nD'altra banda, el port per SMTP, utilitzat per clients de correu externs (com Thunderbird o K9-Mail) ha canviat de 465 (SSL/TLS) a 587 (STARTTLS). L'antic port (465) serà tancat automàticament, i el nou port (587) serà obert en el tallafocs. Tots els usuaris *hauran* d'adaptar la configuració dels clients de correu en acord amb aquests canvis.",
+ "migration_0003_problematic_apps_warning": "Tingueu en compte que s'han detectat les aplicacions, possiblement, problemàtiques següents. Sembla que aquestes no s'han instal·lat des d'un catàleg d'aplicacions, o que no estan marcades com a «working». Per conseqüent, no podem garantir que segueixin funcionant després de l'actualització: {problematic_apps}",
"migration_0003_modified_files": "Tingueu en compte que s'han detectat els següents fitxers que han estat modificats manualment i podrien sobreescriure's al final de l'actualització: {manually_modified_files}",
- "migration_0005_postgresql_94_not_installed": "Postgresql no està instal·lat en el sistema. No hi ha res per fer!",
- "migration_0005_postgresql_96_not_installed": "S'ha trobat Postgresql 9.4 instal·lat, però no Postgresql 9.6!? Alguna cosa estranya a passat en el sistema :( …",
- "migration_0005_not_enough_space": "No hi ha prou espai disponible en {path} per fer la migració en aquest moment :(.",
- "migration_0006_disclaimer": "YunoHost esperar que les contrasenyes admin i root estiguin sincronitzades. Fent aquesta migració, la contrasenya root serà reemplaçada per la contrasenya admin.",
- "migration_0007_cancelled": "YunoHost no ha pogut millorar la gestió de la configuració SSH.",
+ "migration_0005_postgresql_94_not_installed": "PostgreSQL no està instal·lat en el sistema. No hi ha res per fer.",
+ "migration_0005_postgresql_96_not_installed": "PostgreSQL 9.4 està instal·lat, però no PostgreSQL 9.6? Alguna cosa estranya a passat en el sistema :( …",
+ "migration_0005_not_enough_space": "Creu espai disponible en {path} per executar la migració.",
+ "migration_0006_disclaimer": "YunoHost esperar que les contrasenyes de admin i root estiguin sincronitzades. Aquesta migració canvia la contrasenya root per la contrasenya admin.",
+ "migration_0007_cancelled": "No s'ha pogut millorar la gestió de la configuració SSH.",
"migration_0007_cannot_restart": "No es pot reiniciar SSH després d'haver intentat cancel·lar la migració numero 6.",
"migration_0008_general_disclaimer": "Per millorar la seguretat del servidor, es recomana que sigui YunoHost qui gestioni la configuració SSH. La configuració SSH actual és diferent a la configuració recomanada. Si deixeu que YunoHost ho reconfiguri, la manera de connectar-se al servidor mitjançant SSH canviarà de la següent manera:",
- "migration_0008_port": " - la connexió es farà utilitzant el port 22 en lloc del port SSH personalitzat actual. Es pot reconfigurar;",
- "migration_0008_root": " - no es podrà connectar com a root a través de SSH. S'haurà d'utilitzar l'usuari admin per fer-ho;",
- "migration_0008_dsa": " - es desactivarà la clau DSA. Per tant, es podria haver d'invalidar un missatge esgarrifós del client SSH, i tornar a verificar l'empremta digital del servidor;",
- "migration_0008_warning": "Si heu entès els avisos i accepteu que YunoHost sobreescrigui la configuració actual, comenceu la migració. Sinó, podeu saltar-vos la migració, tot i que no està recomanat.",
- "migration_0008_no_warning": "No s'han detectat riscs importants per sobreescriure la configuració SSH, però no es pot estar del tot segur ;)! Si accepteu que YunoHost sobreescrigui la configuració actual, comenceu la migració. Sinó, podeu saltar-vos la migració, tot i que no està recomanat.",
- "migration_0009_not_needed": "Sembla que ja s'ha fet aquesta migració? Ometent.",
- "migrations_backward": "Migració cap enrere.",
- "migrations_bad_value_for_target": "Nombre invàlid pel paràmetre target, els nombres de migració disponibles són 0 o {}",
- "migrations_cant_reach_migration_file": "No s'ha pogut accedir als fitxers de migració al camí %s",
- "migrations_current_target": "La migració objectiu és {}",
- "migrations_error_failed_to_load_migration": "ERROR: no s'ha pogut carregar la migració {number} {name}",
- "migrations_forward": "Migració endavant",
- "migrations_list_conflict_pending_done": "No es pot utilitzar --previous i --done al mateix temps.",
- "migrations_loading_migration": "Carregant la migració {number} {name}…",
- "migrations_migration_has_failed": "La migració {number} {name} ha fallat amb l'excepció {exception}, cancel·lant",
+ "migration_0008_port": "• La connexió es farà utilitzant el port 22 en lloc del port SSH personalitzat actual. Es pot reconfigurar;",
+ "migration_0008_root": "• No es podrà connectar com a root a través de SSH. S'haurà d'utilitzar l'usuari admin per fer-ho;",
+ "migration_0008_dsa": "• Es desactivarà la clau DSA. Per tant, es podria haver d'invalidar un missatge esgarrifós del client SSH, i tornar a verificar l'empremta digital del servidor;",
+ "migration_0008_warning": "Si heu entès els avisos i voleu que YunoHost sobreescrigui la configuració actual, comenceu la migració. Sinó, podeu saltar-vos la migració, tot i que no està recomanat.",
+ "migration_0008_no_warning": "Hauria de ser segur sobreescriure la configuració SSH, però no es pot estar del tot segur! Executetu la migració per sobreescriure-la. Sinó, podeu saltar-vos la migració, tot i que no està recomanat.",
+ "migration_0009_not_needed": "Sembla que ja s'ha fet aquesta migració… (?) Ometent.",
+ "migrations_cant_reach_migration_file": "No s'ha pogut accedir als fitxers de migració al camí «%s»",
+ "migrations_list_conflict_pending_done": "No es pot utilitzar «--previous» i «--done» al mateix temps.",
+ "migrations_loading_migration": "Carregant la migració {id}…",
+ "migrations_migration_has_failed": "La migració {id} ha fallat, cancel·lant. Error: {exception}",
"migrations_no_migrations_to_run": "No hi ha cap migració a fer",
- "migrations_show_currently_running_migration": "Fent la migració {number} {name}…",
- "migrations_show_last_migration": "L'última migració feta és {}",
- "migrations_skip_migration": "Saltant migració {number} {name}…",
- "migrations_success": "S'ha completat la migració {number} {name} amb èxit!",
- "migrations_to_be_ran_manually": "La migració {number} {name} s'ha de fer manualment. Aneu a Eines > Migracions a la interfície admin, o executeu «yunohost tools migrations migrate».",
- "migrations_need_to_accept_disclaimer": "Per fer la migració {number} {name}, heu d'acceptar aquesta clàusula de no responsabilitat:\n---\n{disclaimer}\n---\nSi accepteu fer la migració, torneu a executar l'ordre amb l'opció --accept-disclaimer.",
- "monitor_disabled": "El monitoratge del servidor ha estat desactivat",
- "monitor_enabled": "El monitoratge del servidor ha estat activat",
- "monitor_glances_con_failed": "No s'ha pogut connectar al servidor Glances",
- "monitor_not_enabled": "El monitoratge del servidor no està activat",
- "monitor_period_invalid": "Període de temps invàlid",
- "monitor_stats_file_not_found": "No s'ha pogut trobar el fitxer d'estadístiques",
- "monitor_stats_no_update": "No hi ha dades de monitoratge per actualitzar",
- "monitor_stats_period_unavailable": "No s'han trobat estadístiques per aquest període",
- "mountpoint_unknown": "Punt de muntatge desconegut",
- "mysql_db_creation_failed": "No s'ha pogut crear la base de dades MySQL",
- "mysql_db_init_failed": "No s'ha pogut inicialitzar la base de dades MySQL",
- "mysql_db_initialized": "S'ha inicialitzat la base de dades MySQL",
- "network_check_mx_ko": "El registre DNS MX no està configurat",
- "network_check_smtp_ko": "El tràfic de correu sortint (SMTP port 25) sembla que està bloquejat per la xarxa",
- "network_check_smtp_ok": "El tràfic de correu sortint (SMTP port 25) no està bloquejat",
- "new_domain_required": "S'ha d'especificar un nou domini principal",
- "no_appslist_found": "No s'ha trobat cap llista d'aplicacions",
+ "migrations_skip_migration": "Saltant migració {id}…",
+ "migrations_to_be_ran_manually": "La migració {id} s'ha de fer manualment. Aneu a Eines → Migracions a la interfície admin, o executeu «yunohost tools migrations migrate».",
+ "migrations_need_to_accept_disclaimer": "Per fer la migració {id}, heu d'acceptar aquesta clàusula de no responsabilitat:\n---\n{disclaimer}\n---\nSi accepteu fer la migració, torneu a executar l'ordre amb l'opció «--accept-disclaimer».",
"no_internet_connection": "El servidor no està connectat a Internet",
- "no_ipv6_connectivity": "La connectivitat IPv6 no està disponible",
- "no_restore_script": "No hi ha cap script de restauració per l'aplicació «{app:s}»",
"not_enough_disk_space": "No hi ha prou espai en «{path:s}»",
- "package_not_installed": "El paquet «{pkgname}» no està instal·lat",
- "package_unexpected_error": "Hi ha hagut un error inesperat processant el paquet «{pkgname}»",
"package_unknown": "Paquet desconegut «{pkgname}»",
- "packages_upgrade_critical_later": "Els paquets crítics ({packages:s}) seran actualitzats més tard",
"packages_upgrade_failed": "No s'han pogut actualitzar tots els paquets",
- "path_removal_failed": "No s'ha pogut eliminar el camí {:s}",
"pattern_backup_archive_name": "Ha de ser un nom d'arxiu vàlid amb un màxim de 30 caràcters, compost per caràcters alfanumèrics i -_. exclusivament",
"pattern_domain": "Ha de ser un nom de domini vàlid (ex.: el-meu-domini.cat)",
"pattern_email": "Ha de ser una adreça de correu vàlida (ex.: algu@domini.cat)",
"pattern_firstname": "Ha de ser un nom vàlid",
"pattern_lastname": "Ha de ser un cognom vàlid",
- "pattern_listname": "Ha d'estar compost per caràcters alfanumèrics i guió baix exclusivament",
- "pattern_mailbox_quota": "Ha de ser una mida amb el sufix b/k/M/G/T o 0 per desactivar la quota",
+ "pattern_mailbox_quota": "Ha de ser una mida amb el sufix b/k/M/G/T o 0 per no tenir quota",
"pattern_password": "Ha de tenir un mínim de 3 caràcters",
- "pattern_port": "Ha de ser un número de port vàlid (i.e. 0-65535)",
"pattern_port_or_range": "Ha de ser un número de port vàlid (i.e. 0-65535) o un interval de ports (ex. 100:200)",
"pattern_positive_number": "Ha de ser un nombre positiu",
"pattern_username": "Ha d'estar compost per caràcters alfanumèrics en minúscula i guió baix exclusivament",
- "pattern_password_app": "Les contrasenyes no haurien de tenir els següents caràcters: {forbidden_chars}",
+ "pattern_password_app": "Les contrasenyes no poden de tenir els següents caràcters: {forbidden_chars}",
"port_already_closed": "El port {port:d} ja està tancat per les connexions {ip_version:s}",
"port_already_opened": "El port {port:d} ja està obert per les connexions {ip_version:s}",
- "port_available": "El port {port:d} està disponible",
- "port_unavailable": "El port {port:d} no està disponible",
- "recommend_to_add_first_user": "La post instal·lació s'ha acabat, però YunoHost necessita com a mínim un usuari per funcionar correctament, hauríeu d'afegir un usuari executant «yunohost user create $username» o amb la interfície d'administració.",
"regenconf_file_backed_up": "S'ha guardat una còpia de seguretat del fitxer de configuració «{conf}» a «{backup}»",
"regenconf_file_copy_failed": "No s'ha pogut copiar el nou fitxer de configuració «{new}» a «{conf}»",
"regenconf_file_kept_back": "S'espera que el fitxer de configuració «{conf}» sigui suprimit per regen-conf (categoria {category}) però s'ha mantingut.",
@@ -406,26 +321,24 @@
"regenconf_file_updated": "El fitxer de configuració «{conf}» ha estat actualitzat",
"regenconf_now_managed_by_yunohost": "El fitxer de configuració «{conf}» serà gestionat per YunoHost a partir d'ara (categoria {category}).",
"regenconf_up_to_date": "La configuració ja està al dia per la categoria «{category}»",
- "regenconf_updated": "La configuració ha estat actualitzada per la categoria «{category}»",
+ "regenconf_updated": "S'ha actualitzat la configuració per la categoria «{category}»",
"regenconf_would_be_updated": "La configuració hagués estat actualitzada per la categoria «{category}»",
"regenconf_dry_pending_applying": "Verificació de la configuració pendent que s'hauria d'haver aplicat per la categoria «{category}»…",
"regenconf_failed": "No s'ha pogut regenerar la configuració per la/les categoria/es : {categories}",
"regenconf_pending_applying": "Aplicació de la configuració pendent per la categoria «{category}»…",
- "restore_action_required": "S'ha d'especificar quelcom a restaurar",
- "restore_already_installed_app": "Ja hi ha una aplicació instal·lada amb l'id «{app:s}»",
+ "restore_already_installed_app": "Una aplicació amb la ID «{app:s}» ja està instal·lada",
"restore_app_failed": "No s'ha pogut restaurar l'aplicació «{app:s}»",
"restore_cleaning_failed": "No s'ha pogut netejar el directori temporal de restauració",
"restore_complete": "Restauració completada",
"restore_confirm_yunohost_installed": "Esteu segur de voler restaurar un sistema ja instal·lat? [{answers:s}]",
"restore_extracting": "Extracció dels fitxers necessaris de l'arxiu…",
"restore_failed": "No s'ha pogut restaurar el sistema",
- "restore_hook_unavailable": "L'script de restauració «{part:s}» no està disponible en el sistema i tampoc és en l'arxiu",
- "restore_may_be_not_enough_disk_space": "Sembla que no hi ha prou espai disponible en el disc (espai lliure: {free_space:d} B, espai necessari: {needed_space:d} B, marge de seguretat: {margin:d} B)",
- "restore_mounting_archive": "Muntatge de l'arxiu a «{path:s}»",
- "restore_not_enough_disk_space": "No hi ha prou espai disponible en el disc (espai lliure: {free_space:d} B, espai necessari: {needed_space:d} B, marge de seguretat: {margin:d} B)",
+ "restore_hook_unavailable": "El script de restauració «{part:s}» no està disponible en el sistema i tampoc és en l'arxiu",
+ "restore_may_be_not_enough_disk_space": "Sembla que no hi ha prou espai disponible en el sistema (lliure: {free_space:d} B, espai necessari: {needed_space:d} B, marge de seguretat: {margin:d} B)",
+ "restore_not_enough_disk_space": "No hi ha prou espai disponible (espai: {free_space:d} B, espai necessari: {needed_space:d} B, marge de seguretat: {margin:d} B)",
"restore_nothings_done": "No s'ha restaurat res",
"restore_removing_tmp_dir_failed": "No s'ha pogut eliminar un directori temporal antic",
- "restore_running_app_script": "Execució de l'script de restauració de l'aplicació «{app:s}»…",
+ "restore_running_app_script": "Restaurant l'aplicació «{app:s}»…",
"restore_running_hooks": "Execució dels hooks de restauració…",
"restore_system_part_failed": "No s'ha pogut restaurar la part «{part:s}» del sistema",
"root_password_desynchronized": "S'ha canviat la contrasenya d'administració, però YunoHost no ha pogut propagar-ho cap a la contrasenya root!",
@@ -436,32 +349,29 @@
"server_reboot_confirm": "Es reiniciarà el servidor immediatament, n'esteu segur? [{answers:s}]",
"service_add_failed": "No s'ha pogut afegir el servei «{service:s}»",
"service_added": "S'ha afegit el servei «{service:s}»",
- "service_already_started": "Ja s'ha iniciat el servei «{service:s}»",
+ "service_already_started": "El servei «{service:s}» ja està funcionant",
"service_already_stopped": "Ja s'ha aturat el servei «{service:s}»",
"service_cmd_exec_failed": "No s'ha pogut executar l'ordre «{command:s}»",
- "service_description_avahi-daemon": "permet accedir al servidor via yunohost.local en la xarxa local",
- "service_description_dnsmasq": "gestiona la resolució del nom de domini (DNS)",
- "service_description_dovecot": "permet als clients de correu accedir/recuperar correus (via IMAP i POP3)",
- "service_description_fail2ban": "protegeix contra els atacs de força bruta i a altres atacs provinents d'Internet",
- "service_description_glances": "monitora la informació del sistema en el servidor",
- "service_description_metronome": "gestiona els comptes de missatgeria instantània XMPP",
- "service_description_mysql": "guarda les dades de les aplicacions (base de dades SQL)",
- "service_description_nginx": "serveix o permet l'accés a totes les pàgines web allotjades en el servidor",
- "service_description_nslcd": "gestiona les connexions shell dels usuaris YunoHost",
- "service_description_php7.0-fpm": "executa les aplicacions escrites en PHP amb nginx",
- "service_description_postfix": "utilitzat per enviar i rebre correus",
- "service_description_redis-server": "una base de dades especialitzada per l'accés ràpid a dades, files d'espera i comunicació entre programes",
- "service_description_rmilter": "verifica diferents paràmetres en els correus",
- "service_description_rspamd": "filtra el correu brossa, i altres funcionalitats relacionades al correu",
- "service_description_slapd": "guarda el usuaris, dominis i informació relacionada",
- "service_description_ssh": "permet la connexió remota al servidor via terminal (protocol SSH)",
- "service_description_yunohost-api": "gestiona les interaccions entre la interfície web de YunoHost i el sistema",
- "service_description_yunohost-firewall": "gestiona els ports de connexió oberts i tancats als serveis",
- "service_disable_failed": "No s'han pogut deshabilitar el servei «{service:s}»\n\nRegistres recents: {logs:s}",
- "service_disabled": "S'ha deshabilitat el servei {service:s}",
- "service_enable_failed": "No s'ha pogut activar el servei «{service:s}»\n\nRegistres recents: {log:s}",
- "service_enabled": "S'ha activat el servei {service:s}",
- "service_no_log": "No hi ha cap registre pel servei «{service:s}»",
+ "service_description_avahi-daemon": "Permet accedir al servidor via «yunohost.local» en la xarxa local",
+ "service_description_dnsmasq": "Gestiona la resolució del nom de domini (DNS)",
+ "service_description_dovecot": "Permet als clients de correu accedir/recuperar correus (via IMAP i POP3)",
+ "service_description_fail2ban": "Protegeix contra els atacs de força bruta i a altres atacs provinents d'Internet",
+ "service_description_metronome": "Gestiona els comptes de missatgeria instantània XMPP",
+ "service_description_mysql": "Guarda les dades de les aplicacions (base de dades SQL)",
+ "service_description_nginx": "Serveix o permet l'accés a totes les pàgines web allotjades en el servidor",
+ "service_description_nslcd": "Gestiona les connexions shell dels usuaris YunoHost",
+ "service_description_php7.0-fpm": "Executa les aplicacions escrites en PHP amb NGINX",
+ "service_description_postfix": "Utilitzat per enviar i rebre correus",
+ "service_description_redis-server": "Una base de dades especialitzada per l'accés ràpid a dades, files d'espera i comunicació entre programes",
+ "service_description_rspamd": "Filtra el correu brossa, i altres funcionalitats relacionades amb el correu",
+ "service_description_slapd": "Guarda el usuaris, dominis i informació relacionada",
+ "service_description_ssh": "Permet la connexió remota al servidor via terminal (protocol SSH)",
+ "service_description_yunohost-api": "Gestiona les interaccions entre la interfície web de YunoHost i el sistema",
+ "service_description_yunohost-firewall": "Gestiona els ports de connexió oberts i tancats als serveis",
+ "service_disable_failed": "No s'han pogut fer que el servei «{service:s}» no comenci a l'arrancada.\n\nRegistres recents: {logs:s}",
+ "service_disabled": "El servei «{service:s}» ja no començarà al arrancar el sistema.",
+ "service_enable_failed": "No s'ha pogut fer que el servei «{service:s}» comenci automàticament a l'arrancada.\n\nRegistres recents: {logs:s}",
+ "service_enabled": "El servei «{service:s}» començarà automàticament durant l'arrancada del sistema.",
"service_regen_conf_is_deprecated": "«yunohost service regen-conf» està desfasat! Utilitzeu «yunohost tools regen-conf» en el seu lloc.",
"service_remove_failed": "No s'ha pogut eliminar el servei «{service:s}»",
"service_removed": "S'ha eliminat el servei «{service:s}»",
@@ -473,114 +383,218 @@
"service_reloaded_or_restarted": "S'ha tornat a carregar o s'ha reiniciat el servei «{service:s}»",
"service_start_failed": "No s'ha pogut iniciar el servei «{service:s}»\n\nRegistres recents: {logs:s}",
"service_started": "S'ha iniciat el servei «{service:s}»",
- "service_status_failed": "No s'ha pogut determinar l'estat del servei «{service:s}»",
"service_stop_failed": "No s'ha pogut aturar el servei «{service:s}»\n\nRegistres recents: {logs:s}",
"service_stopped": "S'ha aturat el servei «{service:s}»",
"service_unknown": "Servei «{service:s}» desconegut",
"ssowat_conf_generated": "S'ha generat la configuració SSOwat",
"ssowat_conf_updated": "S'ha actualitzat la configuració SSOwat",
- "ssowat_persistent_conf_read_error": "Error en llegir la configuració persistent de SSOwat: {error:s}. Modifiqueu el fitxer /etc/ssowat/conf.json.persistent per arreglar la sintaxi JSON",
- "ssowat_persistent_conf_write_error": "Error guardant la configuració persistent de SSOwat: {error:s}. Modifiqueu el fitxer /etc/ssowat/conf.json.persistent per arreglar la sintaxi JSON",
"system_upgraded": "S'ha actualitzat el sistema",
- "system_username_exists": "El nom d'usuari ja existeix en els usuaris de sistema",
- "this_action_broke_dpkg": "Aquesta acció a trencat dpkg/apt (els gestors de paquets del sistema)… Podeu intentar resoldre el problema connectant-vos amb SSH i executant «sudo dpkg --configure -a».",
- "tools_upgrade_at_least_one": "Especifiqueu --apps O --system",
+ "system_username_exists": "El nom d'usuari ja existeix en la llista d'usuaris de sistema",
+ "this_action_broke_dpkg": "Aquesta acció a trencat dpkg/APT (els gestors de paquets del sistema)… Podeu intentar resoldre el problema connectant-vos amb SSH i executant «sudo dpkg --configure -a».",
+ "tools_upgrade_at_least_one": "Especifiqueu «--apps», o «--system»",
"tools_upgrade_cant_both": "No es poden actualitzar tant el sistema com les aplicacions al mateix temps",
"tools_upgrade_cant_hold_critical_packages": "No es poden mantenir els paquets crítics…",
"tools_upgrade_cant_unhold_critical_packages": "No es poden deixar de mantenir els paquets crítics…",
"tools_upgrade_regular_packages": "Actualitzant els paquets «normals» (no relacionats amb YunoHost)…",
"tools_upgrade_regular_packages_failed": "No s'han pogut actualitzar els paquets següents: {packages_list}",
"tools_upgrade_special_packages": "Actualitzant els paquets «especials» (relacionats amb YunoHost)…",
- "tools_upgrade_special_packages_explanation": "Aquesta acció s'acabarà, però l'actualització especial continuarà en segon pla. No comenceu cap altra acció al servidor en els pròxims ~10 minuts (depèn de la velocitat del maquinari). Un cop acabat, pot ser que us hagueu de tornar a connectar a la interfície d'administració. Els registres de l'actualització estaran disponibles a Eines > Registres (a la interfície d'administració) o amb «yunohost log list» (a la línia d'ordres).",
- "tools_upgrade_special_packages_completed": "Actualització dels paquets YunoHost acabada!\nPremeu [Enter] per tornar a la línia d'ordres",
+ "tools_upgrade_special_packages_explanation": "Aquesta actualització especial continuarà en segon pla. No comenceu cap altra acció al servidor en els pròxims ~10 minuts (depèn de la velocitat del maquinari). Després d'això, pot ser que us hagueu de tornar a connectar a la interfície d'administració. Els registres de l'actualització estaran disponibles a Eines → Registres (a la interfície d'administració) o utilitzant «yunohost log list» (des de la línia d'ordres).",
+ "tools_upgrade_special_packages_completed": "Actualització dels paquets YunoHost acabada.\nPremeu [Enter] per tornar a la línia d'ordres",
"unbackup_app": "L'aplicació «{app:s}» no serà guardada",
"unexpected_error": "Hi ha hagut un error inesperat: {error}",
- "unit_unknown": "Unitat desconeguda «{unit:s}»",
"unlimit": "Sense quota",
"unrestore_app": "L'aplicació «{app:s} no serà restaurada",
- "update_apt_cache_failed": "No s'ha pogut actualitzar la memòria cau d'APT (el gestor de paquets de Debian). Aquí teniu les línies de sources.list que poden ajudar-vos a identificar les línies problemàtiques:\n{sourceslist}",
+ "update_apt_cache_failed": "No s'ha pogut actualitzar la memòria cau d'APT (el gestor de paquets de Debian). Aquí teniu les línies de sources.list, que poden ajudar-vos a identificar les línies problemàtiques:\n{sourceslist}",
"update_apt_cache_warning": "Hi ha hagut errors al actualitzar la memòria cau d'APT (el gestor de paquets de Debian). Aquí teniu les línies de sources.list que poden ajudar-vos a identificar les línies problemàtiques:\n{sourceslist}",
"updating_apt_cache": "Obtenció de les actualitzacions disponibles per als paquets del sistema…",
- "updating_app_lists": "Obtenció de les actualitzacions disponibles per a les aplicacions…",
"upgrade_complete": "Actualització acabada",
"upgrading_packages": "Actualitzant els paquets…",
"upnp_dev_not_found": "No s'ha trobat cap dispositiu UPnP",
"upnp_disabled": "S'ha desactivat UPnP",
"upnp_enabled": "S'ha activat UPnP",
- "upnp_port_open_failed": "No s'han pogut obrir els ports UPnP",
+ "upnp_port_open_failed": "No s'ha pogut obrir el port UPnP",
"user_created": "S'ha creat l'usuari",
- "user_creation_failed": "No s'ha pogut crear l'usuari",
+ "user_creation_failed": "No s'ha pogut crear l'usuari {user}: {error}",
"user_deleted": "S'ha suprimit l'usuari",
- "user_deletion_failed": "No s'ha pogut suprimir l'usuari",
- "user_home_creation_failed": "No s'ha pogut crear la carpeta personal («home») de l'usuari",
- "user_info_failed": "No s'ha pogut obtenir la informació de l'usuari",
+ "user_deletion_failed": "No s'ha pogut suprimir l'usuari {user}: {error}",
+ "user_home_creation_failed": "No s'ha pogut crear la carpeta personal «home» per l'usuari",
"user_unknown": "Usuari desconegut: {user:s}",
- "user_update_failed": "No s'ha pogut actualitzar l'usuari",
- "user_updated": "S'ha actualitzat l'usuari",
+ "user_update_failed": "No s'ha pogut actualitzar l'usuari {user}: {error}",
+ "user_updated": "S'ha canviat la informació de l'usuari",
"users_available": "Usuaris disponibles:",
"yunohost_already_installed": "YunoHost ja està instal·lat",
"yunohost_ca_creation_failed": "No s'ha pogut crear l'autoritat de certificació",
"yunohost_ca_creation_success": "S'ha creat l'autoritat de certificació local.",
- "yunohost_configured": "S'ha configurat YunoHost",
+ "yunohost_configured": "YunoHost està configurat",
"yunohost_installing": "Instal·lació de YunoHost…",
- "yunohost_not_installed": "YunoHost no està instal·lat o no està instal·lat correctament. Executeu «yunohost tools postinstall»",
- "apps_permission_not_found": "No s'ha trobat cap permís per les aplicacions instal·lades",
- "apps_permission_restoration_failed": "Ha fallat el permís «{permission:s}» per la restauració de l'aplicació {app:s}",
+ "yunohost_not_installed": "YunoHost no està instal·lat correctament. Executeu «yunohost tools postinstall»",
"backup_permission": "Permís de còpia de seguretat per l'aplicació {app:s}",
- "edit_group_not_allowed": "No teniu autorització per modificar el grup {group:s}",
- "edit_permission_with_group_all_users_not_allowed": "No podeu modificar els permisos del grup «all_users», s'ha d'utlilitzar «yunohost user permission clear APP» o «yunohost user permission add APP -u USER».",
- "error_when_removing_sftpuser_group": "Error intentant eliminar el gruo sftpusers",
- "group_already_allowed": "El grup «{group:s}» ja té el permís «{permission:s}» activat per l'aplicació «{app:s}»",
- "group_already_disallowed": "El grup «{group:s}» ja té els permisos «{permission:s}» desactivats per l'aplicació «{app:s}»",
- "group_name_already_exist": "El grup {name:s} ja existeix",
"group_created": "S'ha creat el grup «{group}»",
- "group_creation_failed": "No s'ha pogut crear el grup «{group}»",
+ "group_creation_failed": "No s'ha pogut crear el grup «{group}»: {error}",
"group_deleted": "S'ha eliminat el grup «{group}»",
- "group_deletion_failed": "No s'ha pogut eliminar el grup «{group}»",
- "group_deletion_not_allowed": "El grup {group:s} no es pot eliminar manualment.",
- "group_info_failed": "Ha fallat la informació del grup",
+ "group_deletion_failed": "No s'ha pogut eliminar el grup «{group}»: {error}",
"group_unknown": "Grup {group:s} desconegut",
"group_updated": "S'ha actualitzat el grup «{group}»",
- "group_update_failed": "No s'ha pogut actualitzat el grup «{group}»",
- "log_permission_add": "Afegir el permís «{}» per l'aplicació «{}»",
- "log_permission_remove": "Suprimir el permís «{}»",
- "log_permission_update": "Actualitzar el permís «{}» per l'aplicació «{}»",
- "log_user_group_add": "Afegir grup «{}»",
+ "group_update_failed": "No s'ha pogut actualitzat el grup «{group}»: {error}",
"log_user_group_delete": "Eliminar grup «{}»",
"log_user_group_update": "Actualitzar grup «{}»",
- "log_user_permission_add": "Actualitzar el permís «{}»",
- "log_user_permission_remove": "Actualitzar el permís «{}»",
- "mailbox_disabled": "La bústia de correu està desactivada per als usuaris {user:s}",
- "migration_description_0011_setup_group_permission": "Configurar el grup d'usuaris i els permisos per les aplicacions i els serveis",
+ "mailbox_disabled": "La bústia de correu està desactivada per al usuari {user:s}",
+ "migration_description_0011_setup_group_permission": "Configurar els grups d'usuaris i els permisos per les aplicacions i els serveis",
"migration_0011_backup_before_migration": "Creant una còpia de seguretat de la base de dades LDAP i la configuració de les aplicacions abans d'efectuar la migració.",
- "migration_0011_can_not_backup_before_migration": "No s'ha pogut fer la còpia de seguretat abans de la migració. No s'ha pogut fer la migració. Error: {error:s}",
+ "migration_0011_can_not_backup_before_migration": "No s'ha pogut completar la còpia de seguretat abans de que la migració fallés. Error: {error:s}",
"migration_0011_create_group": "Creant un grup per a cada usuari…",
- "migration_0011_done": "Migració completa. Ja podeu gestionar grups d'usuaris.",
- "migration_0011_LDAP_config_dirty": "Sembla que heu modificat manualment la configuració LDAP. Per fer aquesta migració s'ha d'actualitzar la configuració LDAP.\nGuardeu la configuració actual, reinicieu la configuració original amb l'ordre «yunohost tools regen-conf -f» i torneu a intentar la migració",
+ "migration_0011_done": "Migració completada. Ja podeu gestionar grups d'usuaris.",
"migration_0011_LDAP_update_failed": "Ha fallat l'actualització de LDAP. Error: {error:s}",
"migration_0011_migrate_permission": "Fent la migració dels permisos de la configuració de les aplicacions a LDAP…",
- "migration_0011_migration_failed_trying_to_rollback": "La migració ha fallat … s'intenta tornar el sistema a l'estat anterior.",
+ "migration_0011_migration_failed_trying_to_rollback": "No s'ha pogut fer la migració… s'intenta tornar el sistema a l'estat anterior.",
"migration_0011_rollback_success": "S'ha tornat el sistema a l'estat anterior.",
"migration_0011_update_LDAP_database": "Actualitzant la base de dades LDAP…",
"migration_0011_update_LDAP_schema": "Actualitzant l'esquema LDAP…",
- "need_define_permission_before": "Heu de tornar a redefinir els permisos utilitzant «yunohost user permission add -u USER» abans d'eliminar un grup permès",
- "permission_already_clear": "Ja s'ha donat el permís «{permission:s}» per l'aplicació {app:s}",
- "permission_already_exist": "El permís «{permission:s}» ja existeix per l'aplicació {app:s}",
- "permission_created": "S'ha creat el permís «{permission:s}» per l'aplicació {app:s}",
- "permission_creation_failed": "La creació del permís ha fallat",
- "permission_deleted": "S'ha eliminat el permís «{permission:s}» per l'aplicació {app:s}",
- "permission_deletion_failed": "L'eliminació del permís «{permission:s}» per l'aplicació {app:s} ha fallat",
- "permission_not_found": "No s'ha trobat el permís «{permission:s}» per l'aplicació {app:s}",
- "permission_name_not_valid": "El nom del permís «{permission:s}» no és vàlid",
- "permission_update_failed": "L'actualització del permís ha fallat",
- "permission_generated": "S'ha actualitzat la base de dades del permís",
- "permission_updated": "S'ha actualitzat el permís «{permission:s}» per l'aplicació {app:s}",
+ "permission_already_exist": "El permís «{permission:s}» ja existeix",
+ "permission_created": "S'ha creat el permís «{permission:s}»",
+ "permission_creation_failed": "No s'ha pogut crear el permís «{permission}»: {error}",
+ "permission_deleted": "S'ha eliminat el permís «{permission:s}»",
+ "permission_deletion_failed": "No s'ha pogut eliminar el permís «{permission:s}»: {error}",
+ "permission_not_found": "No s'ha trobat el permís «{permission:s}»",
+ "permission_update_failed": "No s'ha pogut actualitzar el permís «{permission}»: {error}",
+ "permission_updated": "S'ha actualitzat el permís «{permission:s}»",
"permission_update_nothing_to_do": "No hi ha cap permís per actualitzar",
- "remove_main_permission_not_allowed": "No es pot eliminar el permís principal",
- "remove_user_of_group_not_allowed": "No es pot eliminar l'usuari {user:s} del grup {group:s}",
- "system_groupname_exists": "El nom de grup ja existeix en el sistema de grups",
- "tools_update_failed_to_app_fetchlist": "No s'ha pogut actualitzar la llista d'aplicacions de YunoHost a causa de: {error}",
- "user_already_in_group": "L'usuari {user:s} ja és en el grup {group:s}",
- "user_not_in_group": "L'usuari {user:s} no és en el grup {group:s}",
- "migration_description_0012_postgresql_password_to_md5_authentication": "Força l'autenticació postgresql a fer servir md5 per a les connexions locals"
+ "migration_description_0012_postgresql_password_to_md5_authentication": "Força l'autenticació PostgreSQL a fer servir MD5 per a les connexions locals",
+ "app_full_domain_unavailable": "Aquesta aplicació ha de ser instal·lada en el seu propi domini, però ja hi ha altres aplicacions instal·lades en el domini «{domain}». Podeu utilitzar un subdomini dedicat a aquesta aplicació.",
+ "migrations_not_pending_cant_skip": "Aquestes migracions no estan pendents, així que no poden ser omeses: {ids}",
+ "app_action_broke_system": "Aquesta acció sembla haver trencat els següents serveis importants: {services}",
+ "log_user_group_create": "Crear grup «{}»",
+ "log_user_permission_update": "Actualitzar els accessos per al permís «{}»",
+ "log_user_permission_reset": "Restablir el permís «{}»",
+ "permission_already_disallowed": "El grup «{group}» ja té el permís «{permission}» desactivat",
+ "migrations_already_ran": "Aquestes migracions ja s'han fet: {ids}",
+ "migrations_dependencies_not_satisfied": "Executeu aquestes migracions: «{dependencies_id}», abans la migració {id}.",
+ "migrations_failed_to_load_migration": "No s'ha pogut carregar la migració {id}: {error}",
+ "migrations_exclusive_options": "«--auto», «--skip», i «--force-rerun» són opcions mútuament excloents.",
+ "migrations_must_provide_explicit_targets": "Heu de proporcionar objectius explícits al utilitzar «--skip» o «--force-rerun»",
+ "migrations_no_such_migration": "No hi ha cap migració anomenada «{id}»",
+ "migrations_pending_cant_rerun": "Aquestes migracions encara estan pendents, així que no es poden tornar a executar: {ids}",
+ "migrations_running_forward": "Executant la migració {id}…",
+ "migrations_success_forward": "Migració {id} completada",
+ "apps_already_up_to_date": "Ja estan actualitzades totes les aplicacions",
+ "dyndns_provider_unreachable": "No s'ha pogut connectar amb el proveïdor DynDNS {provider}: o el vostre YunoHost no està ben connectat a Internet o el servidor dynette està caigut.",
+ "operation_interrupted": "S'ha interromput manualment l'operació?",
+ "group_already_exist": "El grup {group} ja existeix",
+ "group_already_exist_on_system": "El grup {group} ja existeix en els grups del sistema",
+ "group_cannot_be_deleted": "El grup {group} no es pot eliminar manualment.",
+ "group_user_already_in_group": "L'usuari {user} ja està en el grup {group}",
+ "group_user_not_in_group": "L'usuari {user} no està en el grup {group}",
+ "log_permission_create": "Crear el permís «{}»",
+ "log_permission_delete": "Eliminar el permís «{}»",
+ "migration_0011_failed_to_remove_stale_object": "No s'ha pogut eliminar l'objecte obsolet {dn}: {error}",
+ "permission_already_allowed": "El grup «{group}» ja té el permís «{permission}» activat",
+ "permission_cannot_remove_main": "No es permet eliminar un permís principal",
+ "user_already_exists": "L'usuari «{user}» ja existeix",
+ "app_install_failed": "No s'ha pogut instal·lar {app}: {error}",
+ "app_install_script_failed": "Hi ha hagut un error en el script d'instal·lació de l'aplicació",
+ "group_cannot_edit_all_users": "El grup «all_users» no es pot editar manualment. És un grup especial destinat a contenir els usuaris registrats a YunoHost",
+ "group_cannot_edit_visitors": "El grup «visitors» no es pot editar manualment. És un grup especial que representa els visitants anònims",
+ "group_cannot_edit_primary_group": "El grup «{group}» no es pot editar manualment. És el grup principal destinat a contenir un usuari específic.",
+ "log_permission_url": "Actualització de la URL associada al permís «{}»",
+ "migration_0011_slapd_config_will_be_overwritten": "Sembla que heu modificat manualment la configuració de sldap. Per aquesta migració crítica, YunoHost ha de forçar l'actualització de la configuració sldap. Es farà una còpia de seguretat a {conf_backup_folder}.",
+ "permission_already_up_to_date": "No s'ha actualitzat el permís perquè la petició d'afegir/eliminar ja corresponent a l'estat actual.",
+ "permission_currently_allowed_for_all_users": "El permís ha el té el grup de tots els usuaris (all_users) a més d'altres grups. Segurament s'hauria de revocar el permís a «all_users» o eliminar els altres grups als que s'ha atribuït.",
+ "permission_require_account": "El permís {permission} només té sentit per als usuaris que tenen un compte, i per tant no es pot activar per als visitants.",
+ "app_remove_after_failed_install": "Eliminant l'aplicació després que hagi fallat la instal·lació…",
+ "diagnosis_basesystem_ynh_main_version": "El servidor funciona amb YunoHost {main_version} ({repo})",
+ "diagnosis_ram_low": "El sistema només té {available} ({available_percent}%) de memòria RAM disponibles d'un total de {total}. Aneu amb compte.",
+ "diagnosis_swap_none": "El sistema no té swap. Hauríeu de considerar afegir un mínim de {recommended} de swap per evitar situacions en les que el sistema es queda sense memòria.",
+ "diagnosis_regenconf_manually_modified": "El fitxer de configuració {file} ha estat modificat manualment.",
+ "diagnosis_security_vulnerable_to_meltdown_details": "Per arreglar-ho, hauríeu d'actualitzar i reiniciar el sistema per tal de carregar el nou nucli de linux (o contactar amb el proveïdor del servidor si no funciona). Vegeu https://meltdownattack.com/ per a més informació.",
+ "diagnosis_http_could_not_diagnose": "No s'ha pogut diagnosticar si el domini és accessible des de l'exterior.",
+ "diagnosis_http_could_not_diagnose_details": "Error: {error}",
+ "domain_cannot_remove_main_add_new_one": "No es pot eliminar «{domain:s}» ja que és el domini principal i únic domini, primer s'ha d'afegir un altre domini utilitzant «yunohost domain add », i després fer-lo el domini principal amb «yunohost domain main-domain -n » i després es pot eliminar el domini «{domain:s}» utilitzant «yunohost domain remove {domain:s}».",
+ "diagnosis_basesystem_host": "El servidor funciona amb Debian {debian_version}",
+ "diagnosis_basesystem_kernel": "El servidor funciona amb el nucli de Linux {kernel_version}",
+ "diagnosis_basesystem_ynh_single_version": "{package} versió: {version}({repo})",
+ "diagnosis_basesystem_ynh_inconsistent_versions": "Esteu utilitzant versions inconsistents dels paquets de YunoHost… probablement a causa d'una actualització fallida o parcial.",
+ "diagnosis_display_tip_web": "Podeu anar a la secció de Diagnòstics (en la pantalla principal) per veure els errors que s'han trobat.",
+ "diagnosis_failed_for_category": "Ha fallat el diagnòstic per la categoria «{category}»: {error}",
+ "diagnosis_display_tip_cli": "Podeu executar «yunohost diagnosis show --issues» per mostrar els errors que s'han trobat.",
+ "diagnosis_cache_still_valid": "(La memòria cau encara és vàlida pel diagnòstic de {category}. No es tornar a diagnosticar de moment!)",
+ "diagnosis_cant_run_because_of_dep": "No es pot fer el diagnòstic per {category} mentre hi ha problemes importants relacionats amb {dep}.",
+ "diagnosis_ignored_issues": "(+ {nb_ignored} problema(es) ignorat(s))",
+ "diagnosis_found_errors": "S'ha trobat problema(es) important(s) {errors} relacionats amb {category}!",
+ "diagnosis_found_errors_and_warnings": "S'ha trobat problema(es) important(s) {errors} (i avis(os) {warnings}) relacionats amb {category}!",
+ "diagnosis_found_warnings": "S'han trobat ítems {warnings} que es podrien millorar per {category}.",
+ "diagnosis_everything_ok": "Tot sembla correcte per {category}!",
+ "diagnosis_failed": "No s'han pogut obtenir els resultats del diagnòstic per la categoria «{category}»: {error}",
+ "diagnosis_ip_connected_ipv4": "El servidor està connectat a Internet amb IPv4!",
+ "diagnosis_ip_no_ipv4": "El servidor no té una IPv4 que funcioni.",
+ "diagnosis_ip_connected_ipv6": "El servidor està connectat a Internet amb IPv6!",
+ "diagnosis_ip_no_ipv6": "El servidor no té una IPv6 que funcioni.",
+ "diagnosis_ip_not_connected_at_all": "Sembla que el servidor no està connectat a internet!?",
+ "diagnosis_ip_dnsresolution_working": "La resolució de nom de domini està funcionant!",
+ "diagnosis_ip_broken_dnsresolution": "La resolució de nom de domini falla per algun motiu… Està el tallafocs bloquejant les peticions DNS?",
+ "diagnosis_ip_broken_resolvconf": "La resolució de nom de domini sembla caiguda en el servidor, podria estar relacionat amb el fet que /etc/resolv.conf no apunta cap a 127.0.0.1.",
+ "diagnosis_ip_weird_resolvconf": "La resolució DNS sembla estar funcionant, però aneu amb compte ja que esteu utilitzant un versió personalitzada de /etc/resolv.conf.",
+ "diagnosis_ip_weird_resolvconf_details": "En canvi, aquest fitxer hauria de ser un enllaç simbòlic cap a /etc/resolvconf/run/resolv.conf i que aquest apunti cap a 127.0.0.1 (dnsmasq). La configuració del «resolver» real s'hauria de fer a /etc/resolv.dnsmaq.conf.",
+ "diagnosis_dns_good_conf": "Bona configuració DNS pel domini {domain} (categoria {category})",
+ "diagnosis_dns_bad_conf": "Configuració DNS incorrecta o inexistent pel domini {domain} (categoria {category})",
+ "diagnosis_dns_missing_record": "Segons la configuració DNS recomanada, hauríeu d'afegir un registre DNS\ntipus: {type}\nnom: {name}\nvalor: {value}.",
+ "diagnosis_dns_discrepancy": "El registre DNS de tipus {type} i nom {name} no concorda amb la configuració recomanada.\nValor actual: {current}\nValor esperat: {value}",
+ "diagnosis_services_bad_status": "El servei {service} està {status} :(",
+ "diagnosis_diskusage_verylow": "El lloc d'emmagatzematge {mountpoint} (en l'aparell {device}) només té disponibles {free} ({free_percent}%). Hauríeu de considerar alliberar una mica d'espai.",
+ "diagnosis_diskusage_low": "El lloc d'emmagatzematge {mountpoint} (en l'aparell {device}) només té disponibles {free} ({free_percent}%). Aneu amb compte.",
+ "diagnosis_diskusage_ok": "El lloc d'emmagatzematge {mountpoint} (en l'aparell {device}) encara té {free} ({free_percent}%) lliures!",
+ "diagnosis_ram_verylow": "El sistema només té {available} ({available_percent}%) de memòria RAM disponibles! (d'un total de {total})",
+ "diagnosis_ram_ok": "El sistema encara té {available} ({available_percent}%) de memòria RAM disponibles d'un total de {total}.",
+ "diagnosis_swap_notsomuch": "El sistema només té {total} de swap. Hauríeu de considerar tenir un mínim de {recommended} per evitar situacions en les que el sistema es queda sense memòria.",
+ "diagnosis_swap_ok": "El sistema té {total} de swap!",
+ "diagnosis_regenconf_allgood": "Tots els fitxers de configuració estan en acord amb la configuració recomanada!",
+ "diagnosis_regenconf_manually_modified_details": "No hauria de ser cap problema sempre i quan sapigueu el que esteu fent ;) !",
+ "diagnosis_regenconf_manually_modified_debian": "El fitxer de configuració {file} ha estat modificat manualment respecte al fitxer per defecte de Debian.",
+ "diagnosis_regenconf_manually_modified_debian_details": "No hauria de ser cap problema, però ho haureu de vigilar...",
+ "diagnosis_security_all_good": "No s'ha trobat cap vulnerabilitat de seguretat crítica.",
+ "diagnosis_security_vulnerable_to_meltdown": "Sembla que el sistema és vulnerable a la vulnerabilitat de seguretat crítica Meltdown",
+ "diagnosis_description_basesystem": "Sistema de base",
+ "diagnosis_description_ip": "Connectivitat a Internet",
+ "diagnosis_description_dnsrecords": "Registres DNS",
+ "diagnosis_description_services": "Verificació de l'estat dels serveis",
+ "diagnosis_description_systemresources": "Recursos del sistema",
+ "diagnosis_description_ports": "Exposició dels ports",
+ "diagnosis_description_regenconf": "Configuració del sistema",
+ "diagnosis_description_security": "Verificacions de seguretat",
+ "diagnosis_ports_could_not_diagnose": "No s'ha pogut diagnosticar si els ports són accessibles des de l'exterior.",
+ "diagnosis_ports_could_not_diagnose_details": "Error: {error}",
+ "diagnosis_ports_unreachable": "El port {port} no és accessible des de l'exterior.",
+ "diagnosis_ports_ok": "El port {port} és accessible des de l'exterior.",
+ "diagnosis_http_ok": "El domini {domain} és accessible per mitjà de HTTP des de fora de la xarxa local.",
+ "diagnosis_http_unreachable": "Sembla que el domini {domain} no és accessible a través de HTTP des de fora de la xarxa local.",
+ "diagnosis_unknown_categories": "Les següents categories són desconegudes: {categories}",
+ "apps_catalog_init_success": "S'ha iniciat el sistema de catàleg d'aplicacions!",
+ "apps_catalog_updating": "S'està actualitzant el catàleg d'aplicacions…",
+ "apps_catalog_failed_to_download": "No s'ha pogut descarregar el catàleg d'aplicacions {apps_catalog}: {error}",
+ "apps_catalog_obsolete_cache": "La memòria cau del catàleg d'aplicacions és buida o obsoleta.",
+ "apps_catalog_update_success": "S'ha actualitzat el catàleg d'aplicacions!",
+ "diagnosis_mail_ougoing_port_25_ok": "El port de sortida 25 no està bloquejat i els correus es poden enviar a altres servidors.",
+ "diagnosis_mail_outgoing_port_25_blocked": "Sembla que el port de sortida 25 està bloquejat. Hauríeu d'intentar desbloquejar-lo al panell de configuració del proveïdor d'accés a internet (o allotjador). Mentrestant, el servidor no podrà enviar correus a altres servidors.",
+ "diagnosis_description_mail": "Correu electrònic",
+ "migration_description_0013_futureproof_apps_catalog_system": "Migrar al nou sistema de catàleg d'aplicacions resistent al pas del temps",
+ "app_upgrade_script_failed": "Hi ha hagut un error en el script d'actualització de l'aplicació",
+ "diagnosis_services_bad_status_tip": "Podeu intentar reiniciar el servei, i si no funciona, podeu mirar els registres del servei utilitzant «yunohost service log {service}» o a través de «Serveis» a la secció de la pàgina web d'administració.",
+ "diagnosis_ports_forwarding_tip": "Per arreglar aquest problema, segurament s'ha de configurar el reenviament de ports en el router tal i com s'explica a https://yunohost.org/isp_box_config",
+ "diagnosis_http_bad_status_code": "El sistema de diagnòstic no ha pogut connectar amb el servidor. Podria ser que una altra màquina hagi contestat en lloc del servidor. S'hauria de comprovar que el reenviament del port 80 sigui correcte, que la configuració NGINX està actualitzada i que el reverse-proxy no està interferint.",
+ "diagnosis_no_cache": "Encara no hi ha memòria cau pel diagnòstic de la categoria «{category}»",
+ "diagnosis_http_timeout": "S'ha exhaurit el temps d'esperar intentant connectar amb el servidor des de l'exterior. Sembla que no s'hi pot accedir. S'hauria de comprovar que el reenviament del port 80 és correcte, que NGINX funciona, i que el tallafocs no està interferint.",
+ "diagnosis_http_connection_error": "Error de connexió: no s'ha pogut connectar amb el domini demanat, segurament és inaccessible.",
+ "yunohost_postinstall_end_tip": "S'ha completat la post-instal·lació. Per acabar la configuració, considereu:\n - afegir un primer usuari a través de la secció «Usuaris» a la pàgina web d'administració (o emprant «yunohost user create » a la línia d'ordres);\n - diagnosticar possibles problemes a través de la secció «Diagnòstics» a la pàgina web d'administració (o emprant «yunohost diagnosis run» a la línia d'ordres);\n - llegir les seccions «Finalizing your setup» i «Getting to know Yunohost» a la documentació per administradors: https://yunohost.org/admindoc.",
+ "migration_description_0014_remove_app_status_json": "Eliminar els fitxers d'aplicació status.json heretats",
+ "diagnosis_services_running": "El servei {service} s'està executant!",
+ "diagnosis_services_conf_broken": "La configuració pel servei {service} està trencada!",
+ "diagnosis_ports_needed_by": "És necessari exposar aquest port per a les funcions {category} (servei {service})",
+ "global_settings_setting_pop3_enabled": "Activa el protocol POP3 per al servidor de correu",
+ "log_app_action_run": "Executa l'acció de l'aplicació «{}»",
+ "log_app_config_show_panel": "Mostra el taulell de configuració de l'aplicació «{}»",
+ "log_app_config_apply": "Afegeix la configuració a l'aplicació «{}»",
+ "diagnosis_never_ran_yet": "Sembla que el servidor s'ha configurat recentment i encara no hi cap informe de diagnòstic per mostrar. S'ha d'executar un diagnòstic complet primer, ja sigui des de la pàgina web d'administració o utilitzant la comanda «yunohost diagnosis run» al terminal.",
+ "diagnosis_description_web": "Web",
+ "diagnosis_basesystem_hardware_board": "El model de la targeta del servidor és {model}",
+ "diagnosis_basesystem_hardware": "L'arquitectura del maquinari del servidor és {virt} {arch}",
+ "group_already_exist_on_system_but_removing_it": "El grup {group} ja existeix en els grups del sistema, però YunoHost l'eliminarà…"
}
diff --git a/locales/de.json b/locales/de.json
index 10087e12f..2369e3bdc 100644
--- a/locales/de.json
+++ b/locales/de.json
@@ -4,72 +4,52 @@
"admin_password_change_failed": "Passwort kann nicht geändert werden",
"admin_password_changed": "Das Administrator-Kennwort wurde geändert",
"app_already_installed": "{app:s} ist schon installiert",
- "app_argument_choice_invalid": "Verwende einen der folgenden Werte {choices:s}",
- "app_argument_invalid": "Wähle einen gültigen Wert für das Argument '{name: s}': {error: s}",
+ "app_argument_choice_invalid": "Wähle einen der folgenden Werte '{choices:s}' für das Argument '{name:s}'",
+ "app_argument_invalid": "Wähle einen gültigen Wert für das Argument '{name:s}': {error:s}",
"app_argument_required": "Argument '{name:s}' wird benötigt",
"app_extraction_failed": "Installationsdateien konnten nicht entpackt werden",
"app_id_invalid": "Falsche App-ID",
"app_install_files_invalid": "Diese Dateien können nicht installiert werden",
- "app_location_already_used": "Die App ({app}) ist bereits hier ({path}) installiert",
- "app_location_install_failed": "Die App kann dort nicht installiert werden, da ein Konflikt mit der App '{other_app}' besteht, die bereits in '{other_path}' installiert ist",
"app_manifest_invalid": "Mit dem App-Manifest stimmt etwas nicht: {error}",
- "app_no_upgrade": "Alle Apps sind bereits aktuell",
"app_not_installed": "Die App {app:s} konnte nicht in der Liste installierter Apps gefunden werden: {all_apps}",
- "app_recent_version_required": "Für {:s} benötigt eine aktuellere Version von moulinette",
"app_removed": "{app:s} wurde entfernt",
"app_sources_fetch_failed": "Quelldateien konnten nicht abgerufen werden, ist die URL korrekt?",
"app_unknown": "Unbekannte App",
"app_upgrade_failed": "{app:s} konnte nicht aktualisiert werden",
- "app_upgraded": "{app:s} wurde erfolgreich aktualisiert",
- "appslist_fetched": "Appliste {appslist:s} wurde erfolgreich heruntergelanden",
- "appslist_removed": "Appliste {appslist:s} wurde erfolgreich entfernt",
- "appslist_retrieve_error": "Entfernte Appliste {appslist:s} kann nicht empfangen werden: {error:s}",
- "appslist_unknown": "Appliste {appslist:s} ist unbekannt.",
- "ask_current_admin_password": "Derzeitiges Administrator-Kennwort",
+ "app_upgraded": "{app:s} aktualisiert",
"ask_email": "E-Mail-Adresse",
"ask_firstname": "Vorname",
"ask_lastname": "Nachname",
- "ask_list_to_remove": "zu entfernende Liste",
"ask_main_domain": "Hauptdomain",
"ask_new_admin_password": "Neues Verwaltungskennwort",
"ask_password": "Passwort",
- "backup_action_required": "Du musst etwas zum Speichern auswählen",
- "backup_app_failed": "Konnte keine Sicherung für '{app:s}' erstellen",
+ "backup_app_failed": "Konnte keine Sicherung für die App '{app:s}' erstellen",
"backup_archive_app_not_found": "App '{app:s}' konnte in keiner Datensicherung gefunden werden",
- "backup_archive_hook_not_exec": "Hook '{hook:s}' konnte für diese Datensicherung nicht ausgeführt werden",
- "backup_archive_name_exists": "Datensicherung mit dem selben Namen existiert bereits",
+ "backup_archive_name_exists": "Datensicherung mit dem selben Namen existiert bereits.",
"backup_archive_name_unknown": "Unbekanntes lokale Datensicherung mit Namen '{name:s}' gefunden",
"backup_archive_open_failed": "Kann Sicherungsarchiv nicht öfnen",
"backup_cleaning_failed": "Temporäres Sicherungsverzeichnis konnte nicht geleert werden",
"backup_created": "Datensicherung komplett",
- "backup_creating_archive": "Datensicherung wird erstellt…",
"backup_delete_error": "Pfad '{path:s}' konnte nicht gelöscht werden",
- "backup_deleted": "Datensicherung wurde entfernt",
- "backup_extracting_archive": "Entpacke Sicherungsarchiv...",
- "backup_hook_unknown": "Datensicherungshook '{hook:s}' unbekannt",
- "backup_invalid_archive": "Ungültige Datensicherung",
- "backup_nothings_done": "Es gibt keine Änderungen zur Speicherung",
- "backup_output_directory_forbidden": "Verbotenes Ausgabeverzeichnis. Datensicherung können nicht in /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var oder in Unterordnern von /home/yunohost.backup/archives erstellt werden",
- "backup_output_directory_not_empty": "Ausgabeordner ist nicht leer",
+ "backup_deleted": "Backup wurde entfernt",
+ "backup_hook_unknown": "Der Datensicherungshook '{hook:s}' unbekannt",
+ "backup_invalid_archive": "Dies ist kein Backup-Archiv",
+ "backup_nothings_done": "Keine Änderungen zur Speicherung",
+ "backup_output_directory_forbidden": "Wähle ein anderes Ausgabeverzeichnis. Datensicherung können nicht in /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var oder in Unterordnern von /home/yunohost.backup/archives erstellt werden",
+ "backup_output_directory_not_empty": "Der gewählte Ausgabeordner sollte leer sein",
"backup_output_directory_required": "Für die Datensicherung muss ein Zielverzeichnis angegeben werden",
- "backup_running_app_script": "Datensicherung für App '{app:s}' wurd durchgeführt...",
"backup_running_hooks": "Datensicherunghook wird ausgeführt…",
"custom_app_url_required": "Es muss eine URL angegeben werden, um deine benutzerdefinierte App {app:s} zu aktualisieren",
- "custom_appslist_name_required": "Du musst einen Namen für deine benutzerdefinierte Appliste angeben",
- "dnsmasq_isnt_installed": "dnsmasq scheint nicht installiert zu sein. Bitte führe 'apt-get remove bind9 && apt-get install dnsmasq' aus",
"domain_cert_gen_failed": "Zertifikat konnte nicht erzeugt werden",
"domain_created": "Die Domain wurde angelegt",
"domain_creation_failed": "Konnte Domain nicht erzeugen",
"domain_deleted": "Die Domain wurde gelöscht",
"domain_deletion_failed": "Konnte Domain nicht löschen",
"domain_dyndns_already_subscribed": "Du hast dich schon für eine DynDNS-Domain angemeldet",
- "domain_dyndns_invalid": "Domain nicht mittels DynDNS nutzbar",
"domain_dyndns_root_unknown": "Unbekannte DynDNS Hauptdomain",
"domain_exists": "Die Domain existiert bereits",
"domain_uninstall_app_first": "Mindestens eine App ist noch für diese Domain installiert. Bitte deinstalliere zuerst die App, bevor du die Domain löschst",
"domain_unknown": "Unbekannte Domain",
- "domain_zone_exists": "DNS Zonen Datei existiert bereits",
- "domain_zone_not_found": "DNS Zonen Datei kann nicht für Domäne {:s} gefunden werden",
"done": "Erledigt",
"downloading": "Wird heruntergeladen…",
"dyndns_cron_installed": "DynDNS Cronjob erfolgreich angelegt",
@@ -88,9 +68,6 @@
"firewall_reload_failed": "Die Firewall konnte nicht neu geladen werden",
"firewall_reloaded": "Die Firewall wurde neu geladen",
"firewall_rules_cmd_failed": "Einzelne Firewallregeln konnten nicht übernommen werden. Mehr Informationen sind im Log zu finden.",
- "format_datetime_short": "%d/%m/%Y %I:%M %p",
- "hook_argument_missing": "Fehlend Argument '{:s}'",
- "hook_choice_invalid": "ungültige Wahl '{:s}'",
"hook_exec_failed": "Skriptausführung fehlgeschlagen: {path:s}",
"hook_exec_not_terminated": "Skriptausführung noch nicht beendet: {path:s}",
"hook_list_by_invalid": "Ungültiger Wert zur Anzeige von Hooks",
@@ -100,53 +77,24 @@
"ip6tables_unavailable": "ip6tables kann nicht verwendet werden. Du befindest dich entweder in einem Container oder es wird nicht vom Kernel unterstützt",
"iptables_unavailable": "iptables kann nicht verwendet werden. Du befindest dich entweder in einem Container oder es wird nicht vom Kernel unterstützt",
"ldap_initialized": "LDAP wurde initialisiert",
- "license_undefined": "Undeiniert",
"mail_alias_remove_failed": "E-Mail Alias '{mail:s}' konnte nicht entfernt werden",
"mail_domain_unknown": "Unbekannte Mail Domain '{domain:s}'",
"mail_forward_remove_failed": "Mailweiterleitung '{mail:s}' konnte nicht entfernt werden",
- "maindomain_change_failed": "Die Hauptdomain konnte nicht geändert werden",
- "maindomain_changed": "Die Hauptdomain wurde geändert",
- "monitor_disabled": "Das Servermonitoring wurde erfolgreich deaktiviert",
- "monitor_enabled": "Das Servermonitoring wurde aktiviert",
- "monitor_glances_con_failed": "Verbindung mit Glances nicht möglich",
- "monitor_not_enabled": "Servermonitoring ist nicht aktiviert",
- "monitor_period_invalid": "Falscher Zeitraum",
- "monitor_stats_file_not_found": "Statistikdatei nicht gefunden",
- "monitor_stats_no_update": "Keine Monitoringstatistik zur Aktualisierung",
- "monitor_stats_period_unavailable": "Keine Statistiken für den gewählten Zeitraum verfügbar",
- "mountpoint_unknown": "Unbekannten Einhängepunkt",
- "mysql_db_creation_failed": "MySQL Datenbankerzeugung fehlgeschlagen",
- "mysql_db_init_failed": "MySQL Datenbankinitialisierung fehlgeschlagen",
- "mysql_db_initialized": "Die MySQL Datenbank wurde initialisiert",
- "network_check_mx_ko": "Es ist kein DNS MX Eintrag vorhanden",
- "network_check_smtp_ko": "Ausgehender Mailverkehr (SMTP Port 25) scheint in deinem Netzwerk blockiert zu sein",
- "network_check_smtp_ok": "Ausgehender Mailverkehr (SMTP Port 25) ist blockiert",
- "new_domain_required": "Du musst eine neue Hauptdomain angeben",
- "no_appslist_found": "Keine Appliste gefunden",
+ "main_domain_change_failed": "Die Hauptdomain konnte nicht geändert werden",
+ "main_domain_changed": "Die Hauptdomain wurde geändert",
"no_internet_connection": "Der Server ist nicht mit dem Internet verbunden",
- "no_ipv6_connectivity": "Eine IPv6 Verbindung steht nicht zur Verfügung",
- "no_restore_script": "Es konnte kein Wiederherstellungsskript für '{app:s}' gefunden werden",
- "no_such_conf_file": "Datei {file:s}: konnte nicht kopiert werden, da diese nicht existiert",
- "packages_no_upgrade": "Es müssen keine Pakete aktualisiert werden",
- "packages_upgrade_critical_later": "Ein wichtiges Paket ({packages:s}) wird später aktualisiert",
"packages_upgrade_failed": "Es konnten nicht alle Pakete aktualisiert werden",
- "path_removal_failed": "Pfad {:s} konnte nicht entfernt werden",
"pattern_backup_archive_name": "Ein gültiger Dateiname kann nur aus maximal 30 alphanumerischen sowie -_. Zeichen bestehen",
"pattern_domain": "Muss ein gültiger Domainname sein (z.B. meine-domain.org)",
"pattern_email": "Muss eine gültige E-Mail Adresse sein (z.B. someone@domain.org)",
"pattern_firstname": "Muss ein gültiger Vorname sein",
"pattern_lastname": "Muss ein gültiger Nachname sein",
- "pattern_listname": "Kann nur Alphanumerische Zeichen oder Unterstriche enthalten",
"pattern_mailbox_quota": "Muss eine Größe inkl. b/k/M/G/T Suffix, oder 0 zum deaktivieren sein",
"pattern_password": "Muss mindestens drei Zeichen lang sein",
- "pattern_port": "Es muss ein valider Port (zwischen 0 und 65535) angegeben werden",
"pattern_port_or_range": "Muss ein valider Port (z.B. 0-65535) oder ein Bereich (z.B. 100:200) sein",
"pattern_username": "Darf nur aus klein geschriebenen alphanumerischen Zeichen und Unterstrichen bestehen",
"port_already_closed": "Der Port {port:d} wurde bereits für {ip_version:s} Verbindungen geschlossen",
"port_already_opened": "Der Port {port:d} wird bereits von {ip_version:s} benutzt",
- "port_available": "Der Port {port:d} ist verfügbar",
- "port_unavailable": "Der Port {port:d} ist nicht verfügbar",
- "restore_action_required": "Du musst etwas zum Wiederherstellen auswählen",
"restore_already_installed_app": "Es ist bereits eine App mit der ID '{app:s}' installiet",
"restore_app_failed": "App '{app:s}' konnte nicht wiederhergestellt werden",
"restore_cleaning_failed": "Das temporäre Wiederherstellungsverzeichnis konnte nicht geleert werden",
@@ -157,38 +105,30 @@
"restore_nothings_done": "Es wurde nicht wiederhergestellt",
"restore_running_app_script": "Wiederherstellung wird ausfeührt für App '{app:s}'...",
"restore_running_hooks": "Wiederherstellung wird gestartet…",
- "service_add_configuration": "Füge Konfigurationsdatei {file:s} hinzu",
"service_add_failed": "Der Dienst '{service:s}' kann nicht hinzugefügt werden",
"service_added": "Der Service '{service:s}' wurde erfolgreich hinzugefügt",
"service_already_started": "Der Dienst '{service:s}' läuft bereits",
"service_already_stopped": "Dienst '{service:s}' wurde bereits gestoppt",
"service_cmd_exec_failed": "Der Befehl '{command:s}' konnte nicht ausgeführt werden",
- "service_configuration_conflict": "Die Datei {file:s} wurde zwischenzeitlich verändert. Bitte übernehme die Änderungen manuell oder nutze die Option --force (diese wird alle Änderungen überschreiben).",
"service_disable_failed": "Der Dienst '{service:s}' konnte nicht deaktiviert werden",
"service_disabled": "Der Dienst '{service:s}' wurde erfolgreich deaktiviert",
"service_enable_failed": "Der Dienst '{service:s}' konnte nicht aktiviert werden",
"service_enabled": "Der Dienst '{service:s}' wurde erfolgreich aktiviert",
- "service_no_log": "Für den Dienst '{service:s}' kann kein Log angezeigt werden",
"service_remove_failed": "Der Dienst '{service:s}' konnte nicht entfernt werden",
"service_removed": "Der Dienst '{service:s}' wurde erfolgreich entfernt",
"service_start_failed": "Der Dienst '{service:s}' konnte nicht gestartet werden",
"service_started": "Der Dienst '{service:s}' wurde erfolgreich gestartet",
- "service_status_failed": "Der Status von '{service:s}' kann nicht festgestellt werden",
"service_stop_failed": "Der Dienst '{service:s}' kann nicht gestoppt werden",
"service_stopped": "Der Dienst '{service:s}' wurde erfolgreich beendet",
"service_unknown": "Unbekannter Dienst '{service:s}'",
- "services_configured": "Konfiguration erfolgreich erstellt",
- "show_diff": "Es gibt folgende Änderungen:\n{diff:s}",
"ssowat_conf_generated": "Die Konfiguration von SSOwat war erfolgreich",
"ssowat_conf_updated": "Die persistente SSOwat Einstellung wurde aktualisiert",
"system_upgraded": "Das System wurde aktualisiert",
"system_username_exists": "Der Benutzername existiert bereits",
"unbackup_app": "App '{app:s}' konnte nicht gespeichert werden",
"unexpected_error": "Ein unerwarteter Fehler ist aufgetreten",
- "unit_unknown": "Unbekannte Einheit '{unit:s}'",
"unlimit": "Kein Kontingent",
"unrestore_app": "App '{app:s}' kann nicht Wiederhergestellt werden",
- "update_cache_failed": "Konnte APT cache nicht aktualisieren",
"updating_apt_cache": "Die Liste der verfügbaren Pakete wird aktualisiert…",
"upgrade_complete": "Upgrade vollständig",
"upgrading_packages": "Pakete werden aktualisiert…",
@@ -201,7 +141,6 @@
"user_deleted": "Der Benutzer wurde entfernt",
"user_deletion_failed": "Nutzer konnte nicht gelöscht werden",
"user_home_creation_failed": "Benutzer Home konnte nicht erstellt werden",
- "user_info_failed": "Nutzerinformationen können nicht angezeigt werden",
"user_unknown": "Unbekannter Benutzer: {user:s}",
"user_update_failed": "Benutzer kann nicht aktualisiert werden",
"user_updated": "Der Benutzer wurde aktualisiert",
@@ -211,59 +150,33 @@
"yunohost_installing": "YunoHost wird installiert…",
"yunohost_not_installed": "YunoHost ist nicht oder unvollständig installiert worden. 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 freier Speicherplatz unter '{path:s}' verfügbar",
- "backup_creation_failed": "Erstellen des Backups fehlgeschlagen",
- "service_conf_up_to_date": "Die Konfiguration für den Dienst '{service}' ist bereits aktuell",
- "package_not_installed": "Das Paket '{pkgname}' ist nicht installiert",
+ "backup_creation_failed": "Konnte Backup-Archiv nicht erstellen",
"pattern_positive_number": "Muss eine positive Zahl sein",
- "diagnosis_kernel_version_error": "Kann Kernelversion nicht abrufen: {error}",
- "package_unexpected_error": "Ein unerwarteter Fehler trat bei der Verarbeitung des Pakets '{pkgname}' auf",
- "app_incompatible": "Die Anwendung {app} ist nicht mit deiner YunoHost-Version kompatibel",
"app_not_correctly_installed": "{app:s} scheint nicht korrekt installiert zu sein",
"app_requirements_checking": "Überprüfe notwendige Pakete für {app}…",
- "app_requirements_failed": "Anforderungen für {app} werden nicht erfüllt: {error}",
"app_requirements_unmeet": "Anforderungen für {app} werden nicht erfüllt, das Paket {pkgname} ({version}) muss {spec} sein",
"app_unsupported_remote_type": "Für die App wurde ein nicht unterstützer Steuerungstyp verwendet",
"backup_archive_broken_link": "Auf das Backup-Archiv konnte nicht zugegriffen werden (ungültiger Link zu {path:s})",
- "diagnosis_debian_version_error": "Debian Version konnte nicht abgerufen werden: {error}",
- "diagnosis_monitor_disk_error": "Festplatten können nicht aufgelistet werden: {error}",
- "diagnosis_monitor_network_error": "Netzwerk kann nicht angezeigt werden: {error}",
- "diagnosis_monitor_system_error": "System kann nicht angezeigt werden: {error}",
- "diagnosis_no_apps": "Keine Anwendung ist installiert",
"domains_available": "Verfügbare Domains:",
"dyndns_key_not_found": "DNS-Schlüssel für die Domain wurde nicht gefunden",
"dyndns_no_domain_registered": "Es wurde keine Domain mit DynDNS registriert",
"ldap_init_failed_to_create_admin": "Die LDAP Initialisierung konnte keinen admin Benutzer erstellen",
"mailbox_used_space_dovecot_down": "Der Dovecot Mailbox Dienst muss gestartet sein, wenn du den von der Mailbox belegten Speicher angezeigen lassen willst",
"package_unknown": "Unbekanntes Paket '{pkgname}'",
- "service_conf_file_backed_up": "Von der Konfigurationsdatei {conf} wurde ein Backup in {backup} erstellt",
- "service_conf_file_copy_failed": "Die neue Konfigurationsdatei konnte von {new} nach {conf} nicht kopiert werden",
- "service_conf_file_manually_modified": "Die Konfigurationsdatei {conf} wurde manuell verändert und wird nicht aktualisiert",
- "service_conf_file_manually_removed": "Die Konfigurationsdatei {conf} wurde manuell entfern und wird nicht erstellt",
- "service_conf_file_not_managed": "Die Konfigurationsdatei {conf} wurde noch nicht verwaltet und wird nicht aktualisiert",
- "service_conf_file_remove_failed": "Die Konfigurationsdatei {conf} konnte nicht entfernt werden",
- "service_conf_file_removed": "Die Konfigurationsdatei {conf} wurde entfernt",
- "service_conf_file_updated": "Die Konfigurationsdatei {conf} wurde aktualisiert",
- "service_conf_updated": "Die Konfigurationsdatei wurde für den Service {service} aktualisiert",
- "service_conf_would_be_updated": "Die Konfigurationsdatei sollte für den Service {service} aktualisiert werden",
- "ssowat_persistent_conf_read_error": "Ein Fehler ist aufgetreten, als die persistente SSOwat Konfiguration eingelesen wurde {error:s} Bearbeite die persistente Datei /etc/ssowat/conf.json , um die JSON syntax zu korregieren",
- "ssowat_persistent_conf_write_error": "Ein Fehler ist aufgetreten, als die persistente SSOwat Konfiguration gespeichert wurde {error:s} Bearbeite die persistente Datei /etc/ssowat/conf.json , um die JSON syntax zu korregieren",
"certmanager_attempt_to_replace_valid_cert": "Du versuchst gerade eine richtiges und gültiges Zertifikat der Domain {domain:s} zu überschreiben! (Benutze --force , um diese Nachricht zu umgehen)",
"certmanager_domain_unknown": "Unbekannte Domain {domain:s}",
"certmanager_domain_cert_not_selfsigned": "Das Zertifikat der Domain {domain:s} is kein selbstsigniertes Zertifikat. Bist du dir sicher, dass du es ersetzen willst? (Benutze --force)",
"certmanager_certificate_fetching_or_enabling_failed": "Es scheint so als wäre die Aktivierung des Zertifikats für die Domain {domain:s} fehlgeschlagen...",
"certmanager_attempt_to_renew_nonLE_cert": "Das Zertifikat der Domain {domain:s} wurde nicht von Let's Encrypt ausgestellt. Es kann nicht automatisch erneuert werden!",
- "certmanager_attempt_to_renew_valid_cert": "Das Zertifikat der Domain {domain:s} läuft in Kürze ab! Benutze --force um diese Nachricht zu umgehen",
+ "certmanager_attempt_to_renew_valid_cert": "Das Zertifikat der Domain {domain:s} läuft nicht in Kürze ab! (Benutze --force um diese Nachricht zu umgehen)",
"certmanager_domain_http_not_working": "Es scheint so, dass die Domain {domain:s} nicht über HTTP erreicht werden kann. Bitte überprüfe, ob deine DNS und nginx Konfiguration in Ordnung ist",
"certmanager_error_no_A_record": "Kein DNS 'A' Eintrag für die Domain {domain:s} gefunden. Dein Domainname muss auf diese Maschine weitergeleitet werden, um ein Let's Encrypt Zertifikat installieren zu können! (Wenn du weißt was du tust, kannst du --no-checks benutzen, um diese Überprüfung zu überspringen. )",
"certmanager_domain_dns_ip_differs_from_public_ip": "Der DNS 'A' Eintrag der Domain {domain:s} unterscheidet sich von dieser Server-IP. Wenn du gerade deinen A Eintrag verändert hast, warte bitte etwas, damit die Änderungen wirksam werden (du kannst die DNS Propagation mittels Website überprüfen) (Wenn du weißt was du tust, kannst du --no-checks benutzen, um diese Überprüfung zu überspringen. )",
- "certmanager_domain_not_resolved_locally": "Die Domain {domain:s} konnte von innerhalb des Yunohost-Servers nicht aufgelöst werden. Das kann passieren, wenn du den DNS Eintrag vor Kurzem verändert hast. Falls dies der Fall ist, warte bitte ein paar Stunden, damit die Änderungen wirksam werden. Wenn der Fehler bestehen bleibt, ziehe in Betracht die Domain {domain:s} in /etc/hosts einzutragen. (Wenn du weißt was du tust, benutze --no-checks , um diese Nachricht zu umgehen. )",
"certmanager_cannot_read_cert": "Es ist ein Fehler aufgetreten, als es versucht wurde das aktuelle Zertifikat für die Domain {domain:s} zu öffnen (Datei: {file:s}), Grund: {reason:s}",
"certmanager_cert_install_success_selfsigned": "Ein selbstsigniertes Zertifikat für die Domain {domain:s} wurde erfolgreich installiert!",
"certmanager_cert_install_success": "Für die Domain {domain:s} wurde erfolgreich ein Let's Encrypt installiert!",
"certmanager_cert_renew_success": "Das Let's Encrypt Zertifikat für die Domain {domain:s} wurde erfolgreich erneuert!",
- "certmanager_old_letsencrypt_app_detected": "\nYunohost hat erkannt, dass eine Version von 'letsencrypt' installiert ist, die mit den neuen, integrierten Zertifikatsmanagement-Features in Yunohost kollidieren. Wenn du die neuen Features nutzen willst, führe die folgenden Befehle aus:\n\n yunohost app remove letsencrypt\n yunohost domain cert-install\n\nAnm.: Diese Befehle werden die selbstsignierten und Let's Encrypt Zertifikate aller Domains neu installieren",
"certmanager_hit_rate_limit": "Es wurden innerhalb kurzer Zeit schon zu viele Zertifikate für die exakt gleiche Domain {domain:s} ausgestellt. Bitte versuche es später nochmal. Besuche https://letsencrypt.org/docs/rate-limits/ für mehr Informationen",
"certmanager_cert_signing_failed": "Signieren des neuen Zertifikats ist fehlgeschlagen",
"certmanager_no_cert_file": "Die Zertifikatsdatei für die Domain {domain:s} (Datei: {file:s}) konnte nicht gelesen werden",
@@ -272,87 +185,134 @@
"certmanager_self_ca_conf_file_not_found": "Die Konfigurationsdatei der Zertifizierungsstelle für selbstsignierte Zertifikate wurde nicht gefunden (Datei {file:s})",
"certmanager_acme_not_configured_for_domain": "Das Zertifikat für die Domain {domain:s} scheint nicht richtig installiert zu sein. Bitte führe den Befehl cert-install für diese Domain nochmals aus.",
"certmanager_unable_to_parse_self_CA_name": "Der Name der Zertifizierungsstelle für selbstsignierte Zertifikate konnte nicht analysiert werden (Datei: {file:s})",
- "app_package_need_update": "Es ist notwendig das Paket {app} zu aktualisieren, um Aktualisierungen für YunoHost zu erhalten",
- "service_regenconf_dry_pending_applying": "Überprüfe ausstehende Konfigurationen, die für den Server {service} notwendig sind...",
- "service_regenconf_pending_applying": "Überprüfe ausstehende Konfigurationen, die für den Server '{service}' notwendig sind...",
"certmanager_http_check_timeout": "Eine Zeitüberschreitung ist aufgetreten als der Server versuchte sich selbst über HTTP mit der öffentlichen IP (Domain {domain:s} mit der IP {ip:s}) zu erreichen. Möglicherweise ist dafür hairpinning oder eine falsch konfigurierte Firewall/Router deines Servers dafür verantwortlich.",
"certmanager_couldnt_fetch_intermediate_cert": "Eine Zeitüberschreitung ist aufgetreten als der Server versuchte die Teilzertifikate von Let's Encrypt zusammenzusetzen. Die Installation/Erneuerung des Zertifikats wurde abgebrochen - bitte versuche es später erneut.",
- "appslist_retrieve_bad_format": "Die empfangene Datei der Appliste {appslist:s} ist ungültig",
"domain_hostname_failed": "Erstellen des neuen Hostnamens fehlgeschlagen",
- "appslist_name_already_tracked": "Es gibt bereits eine registrierte App-Liste mit Namen {name:s}.",
- "appslist_url_already_tracked": "Es gibt bereits eine registrierte Anwendungsliste mit dem URL {url:s}.",
- "appslist_migrating": "Migriere Anwendungsliste {appslist:s} …",
- "appslist_could_not_migrate": "Konnte Anwendungsliste {appslist:s} nicht migrieren. Konnte die URL nicht verarbeiten... Der alte Cron-Job wurde unter {bkp_file:s} beibehalten.",
- "appslist_corrupted_json": "Konnte die Anwendungslisten. Es scheint, dass {filename:s} beschädigt ist.",
"yunohost_ca_creation_success": "Die lokale Zertifizierungs-Authorität wurde angelegt.",
"app_already_installed_cant_change_url": "Diese Application ist bereits installiert. Die URL kann durch diese Funktion nicht modifiziert werden. Überprüfe ob `app changeurl` verfügbar ist.",
- "app_change_no_change_url_script": "Die Application {app_name:s} unterstützt das anpassen der URL noch nicht. Sie muss gegebenenfalls erweitert werden.",
"app_change_url_failed_nginx_reload": "NGINX konnte nicht neu gestartet werden. Hier ist der Output von 'nginx -t':\n{nginx_errors:s}",
"app_change_url_identical_domains": "Die alte und neue domain/url_path sind identisch: ('{domain:s} {path:s}'). Es gibt nichts zu tun.",
- "app_already_up_to_date": "{app:s} ist schon aktuell",
+ "app_already_up_to_date": "{app:s} ist bereits aktuell",
"backup_abstract_method": "Diese Backup-Methode wird noch nicht unterstützt",
- "backup_applying_method_tar": "Erstellen des Backup-tar Archives...",
+ "backup_applying_method_tar": "Erstellen des Backup-tar Archives…",
"backup_applying_method_copy": "Kopiere alle Dateien ins Backup…",
- "app_change_url_no_script": "Die Anwendung '{app_name:s}' unterstützt bisher keine URL-Modufikation. Vielleicht gibt es eine Aktualisierung.",
- "app_location_unavailable": "Diese URL ist entweder nicht verfügbar oder steht in Konflikt mit den bereits installierten Apps:\n{apps: s}",
+ "app_change_url_no_script": "Die Anwendung '{app_name:s}' unterstützt bisher keine URL-Modifikation. Vielleicht sollte sie aktualisiert werden.",
+ "app_location_unavailable": "Diese URL ist nicht verfügbar oder wird von einer installierten Anwendung genutzt:\n{apps:s}",
"backup_applying_method_custom": "Rufe die benutzerdefinierte Backup-Methode '{method:s}' auf…",
"backup_archive_system_part_not_available": "Der System-Teil '{part:s}' ist in diesem Backup nicht enthalten",
- "backup_archive_mount_failed": "Das Einbinden des Backup-Archives ist fehlgeschlagen",
- "backup_archive_writing_error": "Die Dateien konnten nicht in der komprimierte Archiv-Backup hinzugefügt werden",
+ "backup_archive_writing_error": "Die Dateien '{source:s} (im Ordner '{dest:s}') konnten nicht in das komprimierte Archiv-Backup '{archive:s}' hinzugefügt werden",
"app_change_url_success": "{app:s} URL ist nun {domain:s}{path:s}",
"backup_applying_method_borg": "Sende alle Dateien zur Sicherung ins borg-backup repository…",
- "invalid_url_format": "ungültiges URL Format",
- "global_settings_bad_type_for_setting": "Falscher Typ für Einstellung {setting: s}. Empfangen: {receive_type: s}, aber erwartet: {expected_type: s}",
- "global_settings_bad_choice_for_enum": "Falsche Wahl für die Einstellung {setting: s}. Habe '{choice: s}' erhalten, aber es stehen nur folgende Auswahlmöglichkeiten zur Verfügung: {available_choices: s}",
- "file_does_not_exist": "Die Datei {path: s} existiert nicht.",
+ "global_settings_bad_type_for_setting": "Falscher Typ für Einstellung {setting:s}. Empfangen: {received_type:s}, aber erwartet: {expected_type:s}",
+ "global_settings_bad_choice_for_enum": "Falsche Wahl für die Einstellung {setting:s}. Habe '{choice:s}' erhalten, aber es stehen nur folgende Auswahlmöglichkeiten zur Verfügung: {available_choices:s}",
+ "file_does_not_exist": "Die Datei {path:s} existiert nicht.",
"experimental_feature": "Warnung: Diese Funktion ist experimentell und gilt nicht als stabil. Sie sollten sie nur verwenden, wenn Sie wissen, was Sie tun.",
- "error_when_removing_sftpuser_group": "Fehler beim Versuch, die Gruppe sftpusers zu entfernen",
- "edit_permission_with_group_all_users_not_allowed": "Sie dürfen die Berechtigung für die Gruppe \"all_users\" nicht bearbeiten. Verwenden Sie stattdessen \"yunohost user permission clear APP\" oder \"yunohost user permission add APP -u USER\".",
- "edit_group_not_allowed": "Du bist nicht berechtigt zum Bearbeiten der Gruppe {group: s}",
- "dyndns_domain_not_provided": "Der Dyndns-Anbieter {provider: s} kann die Domain(s) {domain: s} nicht bereitstellen.",
- "dyndns_could_not_check_available": "Konnte nicht überprüfen, ob {domain: s} auf {provider: s} verfügbar ist.",
- "dyndns_could_not_check_provide": "Konnte nicht überprüft, ob {provider: s} die Domain(s) {domain: s} bereitstellen kann.",
- "domain_dyndns_dynette_is_unreachable": "YunoHost dynette kann nicht erreicht werden, entweder ist Ihr YunoHost nicht korrekt mit dem Internet verbunden oder der dynette-Server ist inaktiv. Fehler: {error}",
+ "dyndns_domain_not_provided": "Der DynDNS-Anbieter {provider:s} kann die Domain(s) {domain:s} nicht bereitstellen.",
+ "dyndns_could_not_check_available": "Konnte nicht überprüfen, ob {domain:s} auf {provider:s} verfügbar ist.",
+ "dyndns_could_not_check_provide": "Konnte nicht überprüft, ob {provider:s} die Domain(s) {domain:s} bereitstellen kann.",
"domain_dns_conf_is_just_a_recommendation": "Dieser Befehl zeigt Ihnen, was die * empfohlene * Konfiguration ist. Die DNS-Konfiguration wird NICHT für Sie eingerichtet. Es liegt in Ihrer Verantwortung, Ihre DNS-Zone in Ihrem Registrar gemäß dieser Empfehlung zu konfigurieren.",
"dpkg_lock_not_available": "Dieser Befehl kann momentan nicht ausgeführt werden, da anscheinend ein anderes Programm die Sperre von dpkg (dem Systempaket-Manager) verwendet",
- "confirm_app_install_thirdparty": "WARNUNG! Das Installieren von Anwendungen von Drittanbietern kann die Integrität und Sicherheit Deines Systems beeinträchtigen. Du solltest es wahrscheinlich NICHT installieren, es sei denn, Du weisst, was Du tust. Bist du bereit, dieses Risiko einzugehen? [{answers: s}] ",
- "confirm_app_install_danger": "WARNUNG! Diese Anwendung ist noch experimentell (wenn nicht ausdrücklich \"not working\"/\"funktioniert nicht\") und es ist wahrscheinlich, dass Dein System Schaden nimmt! Du solltest es wahrscheinlich NICHT installieren, es sei denn, Du weisst, was Du tust. Bist du bereit, dieses Risiko einzugehen? [{answers: s}] ",
- "confirm_app_install_warning": "Warnung: Diese Anwendung funktioniert möglicherweise, ist jedoch nicht gut in YunoHost integriert. Einige Funktionen wie Single Sign-On und Backup / Restore sind möglicherweise nicht verfügbar. Trotzdem installieren? [{answers: s}] ",
- "backup_with_no_restore_script_for_app": "App {app: s} hat kein Wiederherstellungsskript. Das Backup dieser App kann nicht automatisch wiederhergestellt werden.",
- "backup_with_no_backup_script_for_app": "App {app: s} hat kein Sicherungsskript. Ignoriere es.",
- "backup_unable_to_organize_files": "Dateien im Archiv können mit der schnellen Methode nicht organisiert werden",
- "backup_system_part_failed": "Der Systemteil '{part: s}' kann nicht gesichert werden",
- "backup_permission": "Sicherungsberechtigung für App {app: s}",
- "backup_output_symlink_dir_broken": "Sie haben einen fehlerhaften Symlink anstelle Ihres Archivverzeichnisses '{path: s}'. Möglicherweise haben Sie ein spezielles Setup, um Ihre Daten auf einem anderen Dateisystem zu sichern. In diesem Fall haben Sie wahrscheinlich vergessen, Ihre Festplatte oder Ihren USB-Schlüssel erneut einzuhängen oder anzuschließen.",
+ "confirm_app_install_thirdparty": "WARNUNG! Das Installieren von Anwendungen von Drittanbietern kann die Integrität und Sicherheit Deines Systems beeinträchtigen. Du solltest es wahrscheinlich NICHT installieren, es sei denn, Du weisst, was Du tust. Bist du bereit, dieses Risiko einzugehen? [{answers:s}]",
+ "confirm_app_install_danger": "WARNUNG! Diese Anwendung ist noch experimentell (wenn nicht ausdrücklich \"not working\"/\"funktioniert nicht\") und es ist wahrscheinlich, dass Dein System Schaden nimmt! Du solltest es wahrscheinlich NICHT installieren, es sei denn, Du weisst, was Du tust. Bist du bereit, dieses Risiko einzugehen? [{answers:s}]",
+ "confirm_app_install_warning": "Warnung: Diese Anwendung funktioniert möglicherweise, ist jedoch nicht gut in YunoHost integriert. Einige Funktionen wie Single Sign-On und Backup / Restore sind möglicherweise nicht verfügbar. Trotzdem installieren? [{answers:s}] ",
+ "backup_with_no_restore_script_for_app": "Die App {app:s} hat kein Wiederherstellungsskript. Das Backup dieser App kann nicht automatisch wiederhergestellt werden.",
+ "backup_with_no_backup_script_for_app": "Die App {app:s} hat kein Sicherungsskript. Ignoriere es.",
+ "backup_unable_to_organize_files": "Dateien im Archiv konnten nicht mit der schnellen Methode organisiert werden",
+ "backup_system_part_failed": "Der Systemteil '{part:s}' konnte nicht gesichert werden",
+ "backup_permission": "Sicherungsberechtigung für App {app:s}",
+ "backup_output_symlink_dir_broken": "Ihr Archivverzeichnis '{path:s}' ist ein fehlerhafter Symlink. Vielleicht haben Sie vergessen, das Speichermedium, auf das er verweist, neu zu mounten oder einzustecken.",
"backup_mount_archive_for_restore": "Archiv für Wiederherstellung vorbereiten…",
- "backup_method_tar_finished": "Sicherungs-Tar-Archiv erstellt",
- "backup_method_custom_finished": "Benutzerdefinierte Sicherungsmethode '{method: s}' beendet",
+ "backup_method_tar_finished": "Tar-Backup-Archiv erstellt",
+ "backup_method_custom_finished": "Benutzerdefinierte Sicherungsmethode '{method:s}' beendet",
"backup_method_copy_finished": "Sicherungskopie beendet",
"backup_method_borg_finished": "Backup in Borg beendet",
- "backup_custom_need_mount_error": "Bei der benutzerdefinierten Sicherungsmethode ist beim Arbeitsschritt \"Braucht ein Einhängen/Verbinden\" (need_mount) ein Fehler aufgetreten",
"backup_custom_mount_error": "Bei der benutzerdefinierten Sicherungsmethode ist beim Arbeitsschritt \"Einhängen/Verbinden\" ein Fehler aufgetreten",
"backup_custom_backup_error": "Bei der benutzerdefinierten Sicherungsmethode ist beim Arbeitsschritt \"Sicherung\" ein Fehler aufgetreten",
- "backup_csv_creation_failed": "Die CSV-Datei, die für zukünftige Wiederherstellungsvorgänge erforderlich ist, kann nicht erstellt werden",
- "backup_couldnt_bind": "{Src: s} konnte nicht an {dest: s} angebunden werden.",
+ "backup_csv_creation_failed": "Die zur Wiederherstellung erforderliche CSV-Datei kann nicht erstellt werden",
+ "backup_couldnt_bind": "{src:s} konnte nicht an {dest:s} angebunden werden.",
"backup_borg_not_implemented": "Die Borg-Sicherungsmethode ist noch nicht implementiert",
- "backup_ask_for_copying_if_needed": "Einige Dateien konnten mit der Methode, die es vermeidet vorübergehend Speicherplatz auf dem System zu verschwenden, nicht gesichert werden. Zur Durchführung der Sicherung sollten vorübergehend {size: s} MB verwendet werden. Sind Sie einverstanden?",
- "backup_actually_backuping": "Erstelle nun ein Backup-Archiv aus den gesammelten Dateien …",
- "ask_path": "Pfad",
+ "backup_ask_for_copying_if_needed": "Möchten Sie die Sicherung mit {size:s} MB temporär durchführen? (Dieser Weg wird verwendet, da einige Dateien nicht mit einer effizienteren Methode vorbereitet werden konnten).",
+ "backup_actually_backuping": "Erstellt ein Backup-Archiv aus den gesammelten Dateien …",
"ask_new_path": "Neuer Pfad",
"ask_new_domain": "Neue Domain",
- "apps_permission_restoration_failed": "Die Berechtigung '{permission: s}' für die Wiederherstellung der App {app: s} ist fehlgeschlagen",
- "apps_permission_not_found": "Keine Berechtigung für die installierten Apps gefunden",
"app_upgrade_some_app_failed": "Einige Anwendungen können nicht aktualisiert werden",
- "app_upgrade_app_name": "{App} wird jetzt aktualisiert…",
+ "app_upgrade_app_name": "{app} wird jetzt aktualisiert…",
"app_upgrade_several_apps": "Die folgenden Apps werden aktualisiert: {apps}",
"app_start_restore": "Anwendung {app} wird wiederhergestellt…",
"app_start_backup": "Sammeln von Dateien, die für {app} gesichert werden sollen…",
"app_start_remove": "Anwendung {app} wird entfernt…",
"app_start_install": "Anwendung {app} wird installiert…",
"app_not_upgraded": "Die App '{failed_app}' konnte nicht aktualisiert werden. Infolgedessen wurden die folgenden App-Upgrades abgebrochen: {apps}",
- "app_make_default_location_already_used": "Die App \"{app}\" kann nicht als Standard für die Domain \"{domain}\" festgelegt werden. Sie wird bereits von der anderen App \"{other_app}\" verwendet",
+ "app_make_default_location_already_used": "Die App \"{app}\" kann nicht als Standard für die Domain \"{domain}\" festgelegt werden. Sie wird bereits von der App \"{other_app}\" verwendet",
"aborting": "Breche ab.",
- "app_action_cannot_be_ran_because_required_services_down": "Diese App erfordert einige Dienste, die derzeit nicht verfügbar sind. Bevor Sie fortfahren, sollten Sie versuchen, die folgenden Dienste neu zu starten (und möglicherweise untersuchen, warum sie nicht verfügbar sind): {services}",
+ "app_action_cannot_be_ran_because_required_services_down": "Diese erforderlichen Dienste sollten zur Durchführung dieser Aktion laufen: {services}. Versuchen Sie, sie neu zu starten, um fortzufahren (und möglicherweise zu untersuchen, warum sie nicht verfügbar sind).",
"already_up_to_date": "Nichts zu tun. Alles ist bereits auf dem neusten Stand.",
- "admin_password_too_long": "Bitte ein Passwort kürzer als 127 Zeichen wählen"
+ "admin_password_too_long": "Bitte ein Passwort kürzer als 127 Zeichen wählen",
+ "app_action_broke_system": "Diese Aktion scheint diese wichtigen Dienste unterbrochen zu haben: {services}",
+ "apps_already_up_to_date": "Alle Apps sind bereits aktuell",
+ "backup_copying_to_organize_the_archive": "Kopieren von {size:s} MB, um das Archiv zu organisieren",
+ "global_settings_setting_security_ssh_compatibility": "Kompatibilität vs. Sicherheitskompromiss für den SSH-Server. Beeinflusst die Chiffren (und andere sicherheitsrelevante Aspekte)",
+ "group_deleted": "Gruppe '{group}' gelöscht",
+ "group_deletion_failed": "Kann Gruppe '{group}' nicht löschen",
+ "dyndns_provider_unreachable": "Dyndns-Anbieter {provider} kann nicht erreicht werden: Entweder ist dein YunoHost nicht korrekt mit dem Internet verbunden oder der Dynette-Server ist ausgefallen.",
+ "group_created": "Gruppe '{group}' angelegt",
+ "group_creation_failed": "Kann Gruppe '{group}' nicht anlegen",
+ "group_unknown": "Die Gruppe '{group:s}' ist unbekannt",
+ "group_updated": "Gruppe '{group:s}' erneuert",
+ "group_update_failed": "Kann Gruppe '{group:s}' nicht anpassen",
+ "log_does_exists": "Es gibt kein Operationsprotokoll mit dem Namen'{log}', verwende'yunohost log list', um alle verfügbaren Operationsprotokolle anzuzeigen",
+ "log_operation_unit_unclosed_properly": "Die Operationseinheit wurde nicht richtig geschlossen",
+ "global_settings_setting_security_postfix_compatibility": "Kompatibilität vs. Sicherheitskompromiss für den Postfix-Server. Beeinflusst die Chiffren (und andere sicherheitsrelevante Aspekte)",
+ "log_category_404": "Die Log-Kategorie '{category}' existiert nicht",
+ "global_settings_unknown_type": "Unerwartete Situation, die Einstellung {setting:s} scheint den Typ {unknown_type:s} zu haben, ist aber kein vom System unterstützter Typ.",
+ "dpkg_is_broken": "Du kannst das gerade nicht tun, weil dpkg/APT (der Systempaketmanager) in einem defekten Zustand zu sein scheint.... Du kannst versuchen, dieses Problem zu lösen, indem du dich über SSH verbindest und `sudo dpkg --configure -a` ausführst.",
+ "global_settings_unknown_setting_from_settings_file": "Unbekannter Schlüssel in den Einstellungen: '{setting_key:s}', verwerfen und speichern in /etc/yunohost/settings-unknown.json",
+ "log_link_to_log": "Vollständiges Log dieser Operation: '{desc}'",
+ "global_settings_setting_example_bool": "Beispiel einer booleschen Option",
+ "log_help_to_get_log": "Um das Protokoll der Operation '{desc}' anzuzeigen, verwende den Befehl 'yunohost log display {name}'",
+ "global_settings_setting_security_nginx_compatibility": "Kompatibilität vs. Sicherheitskompromiss für den Webserver NGINX. Beeinflusst die Chiffren (und andere sicherheitsrelevante Aspekte)",
+ "backup_php5_to_php7_migration_may_fail": "Dein Archiv konnte nicht für PHP 7 konvertiert werden, Du kannst deine PHP-Anwendungen möglicherweise nicht wiederherstellen (Grund: {error:s})",
+ "global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "Erlaubt die Verwendung eines (veralteten) DSA-Hostkeys für die SSH-Daemon-Konfiguration",
+ "global_settings_setting_example_string": "Beispiel einer string Option",
+ "log_app_remove": "Entferne die Anwendung '{}'",
+ "global_settings_setting_example_int": "Beispiel einer int Option",
+ "global_settings_cant_open_settings": "Einstellungsdatei konnte nicht geöffnet werden, Grund: {reason:s}",
+ "global_settings_cant_write_settings": "Einstellungsdatei konnte nicht gespeichert werden, Grund: {reason:s}",
+ "log_app_install": "Installiere die Anwendung '{}'",
+ "global_settings_reset_success": "Frühere Einstellungen werden nun auf {path:s} gesichert",
+ "log_app_upgrade": "Upgrade der Anwendung '{}'",
+ "good_practices_about_admin_password": "Sie sind nun dabei, ein neues Administrationspasswort zu definieren. Das Passwort sollte mindestens 8 Zeichen lang sein - obwohl es sinnvoll ist, ein längeres Passwort (z.B. eine Passphrase) und/oder eine Variation von Zeichen (Groß- und Kleinschreibung, Ziffern und Sonderzeichen) zu verwenden.",
+ "log_corrupted_md_file": "Die mit Protokollen verknüpfte YAML-Metadatendatei ist beschädigt: '{md_file}\nFehler: {error}''",
+ "global_settings_cant_serialize_settings": "Einstellungsdaten konnten nicht serialisiert werden, Grund: {reason:s}",
+ "log_help_to_get_failed_log": "Der Vorgang'{desc}' konnte nicht abgeschlossen werden. Bitte teile das vollständige Protokoll dieser Operation mit dem Befehl 'yunohost log display {name} --share', um Hilfe zu erhalten",
+ "backup_no_uncompress_archive_dir": "Dieses unkomprimierte Archivverzeichnis gibt es nicht",
+ "log_app_change_url": "Ändere die URL der Anwendung '{}'",
+ "global_settings_setting_security_password_user_strength": "Stärke des Benutzerpassworts",
+ "good_practices_about_user_password": "Du bist nun dabei, ein neues Benutzerpasswort zu definieren. Das Passwort sollte mindestens 8 Zeichen lang sein - obwohl es ratsam ist, ein längeres Passwort (z.B. eine Passphrase) und/oder eine Variation von Zeichen (Groß- und Kleinschreibung, Ziffern und Sonderzeichen) zu verwenden.",
+ "global_settings_setting_example_enum": "Beispiel einer enum Option",
+ "log_link_to_failed_log": "Der Vorgang konnte nicht abgeschlossen werden '{desc}'. Bitte gib das vollständige Protokoll dieser Operation mit Klicken Sie hier an, um Hilfe zu erhalten",
+ "backup_cant_mount_uncompress_archive": "Das unkomprimierte Archiv konnte nicht als schreibgeschützt gemountet werden",
+ "backup_csv_addition_failed": "Es konnten keine Dateien zur Sicherung in die CSV-Datei hinzugefügt werden",
+ "global_settings_setting_security_password_admin_strength": "Stärke des Admin-Passworts",
+ "global_settings_key_doesnt_exists": "Der Schlüssel'{settings_key:s}' existiert nicht in den globalen Einstellungen, du kannst alle verfügbaren Schlüssel sehen, indem du 'yunohost settings list' ausführst",
+ "log_app_makedefault": "Mache '{}' zur Standard-Anwendung",
+ "hook_json_return_error": "Konnte die Rückkehr vom Einsprungpunkt {path:s} nicht lesen. Fehler: {msg:s}. Unformatierter Inhalt: {raw_content}",
+ "app_full_domain_unavailable": "Es tut uns leid, aber diese Anwendung erfordert die Installation auf einer eigenen Domain, aber einige andere Anwendungen sind bereits auf der Domäne'{domain}' installiert. Eine mögliche Lösung ist das Hinzufügen und Verwenden einer Subdomain, die dieser Anwendung zugeordnet ist.",
+ "app_install_failed": "Installation von {app} fehlgeschlagen: {error}",
+ "app_install_script_failed": "Im Installationsscript ist ein Fehler aufgetreten",
+ "app_remove_after_failed_install": "Entfernen der App nach fehlgeschlagener Installation…",
+ "app_upgrade_script_failed": "Es ist ein Fehler im App-Upgrade-Skript aufgetreten",
+ "diagnosis_basesystem_host": "Server läuft unter Debian {debian_version}.",
+ "diagnosis_basesystem_kernel": "Server läuft unter Linux-Kernel {kernel_version}",
+ "diagnosis_basesystem_ynh_single_version": "{package} Version: {version} ({repo})",
+ "diagnosis_basesystem_ynh_main_version": "Server läuft YunoHost {main_version} ({repo})",
+ "diagnosis_basesystem_ynh_inconsistent_versions": "Sie verwenden inkonsistente Versionen der YunoHost-Pakete... wahrscheinlich wegen eines fehlgeschlagenen oder teilweisen Upgrades.",
+ "diagnosis_display_tip_web": "Sie können den Abschnitt Diagnose (im Startbildschirm) aufrufen, um die gefundenen Probleme anzuzeigen.",
+ "apps_catalog_init_success": "Apps-Katalogsystem initialisiert!",
+ "apps_catalog_updating": "Aktualisierung des Applikationskatalogs...",
+ "apps_catalog_failed_to_download": "Der {apps_catalog} Apps-Katalog kann nicht heruntergeladen werden: {error}",
+ "apps_catalog_obsolete_cache": "Der Cache des Apps-Katalogs ist leer oder veraltet.",
+ "apps_catalog_update_success": "Der Apps-Katalog wurde aktualisiert!",
+ "password_too_simple_1": "Das Passwort muss mindestens 8 Zeichen lang sein",
+ "diagnosis_display_tip_cli": "Sie können 'yunohost diagnosis show --issues' ausführen, um die gefundenen Probleme anzuzeigen."
}
diff --git a/locales/el.json b/locales/el.json
index 0967ef424..efa5bf769 100644
--- a/locales/el.json
+++ b/locales/el.json
@@ -1 +1,3 @@
-{}
+{
+ "password_too_simple_1": "Ο κωδικός πρόσβασης πρέπει να έχει μήκος τουλάχιστον 8 χαρακτήρων"
+}
\ No newline at end of file
diff --git a/locales/en.json b/locales/en.json
index 61fdcfa9b..3607052e3 100644
--- a/locales/en.json
+++ b/locales/en.json
@@ -3,76 +3,65 @@
"action_invalid": "Invalid action '{action:s}'",
"admin_password": "Administration password",
"admin_password_change_failed": "Cannot change password",
- "admin_password_changed": "The administration password got changed",
+ "admin_password_changed": "The administration password was changed",
"admin_password_too_long": "Please choose a password shorter than 127 characters",
"already_up_to_date": "Nothing to do. Everything is already up-to-date.",
- "app_action_cannot_be_ran_because_required_services_down": "This app requires some services which are currently down. Before continuing, you should try to restart the following services (and possibly investigate why they are down): {services}",
- "app_action_broke_system": "This action seem to have broke these important services: {services}",
+ "app_action_cannot_be_ran_because_required_services_down": "These required services should be running to run this action: {services}. Try restarting them to continue (and possibly investigate why they are down).",
+ "app_action_broke_system": "This action seems to have broken these important services: {services}",
"app_already_installed": "{app:s} is already installed",
- "app_already_installed_cant_change_url": "This app is already installed. The URL cannot be changed just by this function. Look into `app changeurl` if it's available.",
+ "app_already_installed_cant_change_url": "This app is already installed. The URL cannot be changed just by this function. Check in `app changeurl` if it's available.",
"app_already_up_to_date": "{app:s} is already up-to-date",
"app_argument_choice_invalid": "Use one of these choices '{choices:s}' for the argument '{name:s}'",
"app_argument_invalid": "Pick a valid value for the argument '{name:s}': {error:s}",
"app_argument_required": "Argument '{name:s}' is required",
"app_change_url_failed_nginx_reload": "Could not reload NGINX. Here is the output of 'nginx -t':\n{nginx_errors:s}",
"app_change_url_identical_domains": "The old and new domain/url_path are identical ('{domain:s}{path:s}'), nothing to do.",
- "app_change_url_no_script": "This application '{app_name:s}' doesn't support URL modification yet. Maybe you should upgrade it.",
+ "app_change_url_no_script": "The app '{app_name:s}' doesn't support URL modification yet. Maybe you should upgrade it.",
"app_change_url_success": "{app:s} URL is now {domain:s}{path:s}",
"app_extraction_failed": "Could not extract the installation files",
+ "app_full_domain_unavailable": "Sorry, this app must be installed on a domain of its own, but other apps are already installed on the domain '{domain}'. You could use a subdomain dedicated to this app instead.",
"app_id_invalid": "Invalid app ID",
- "app_incompatible": "The app {app} is incompatible with your YunoHost version",
"app_install_files_invalid": "These files cannot be installed",
- "app_location_already_used": "The app '{app}' is already installed in ({path})",
- "app_make_default_location_already_used": "Can't make the app '{app}' the default on the domain, {domain} is already in use by the other app '{other_app}'",
- "app_location_install_failed": "Cannot install the app there because it conflicts with the app '{other_app}' already installed in '{other_path}'",
+ "app_install_failed": "Could not install {app}: {error}",
+ "app_install_script_failed": "An error occurred inside the app installation script",
+ "app_make_default_location_already_used": "Can't make the app '{app}' the default on the domain, '{domain}' is already in use by the other app '{other_app}'",
"app_location_unavailable": "This URL is either unavailable, or conflicts with the already installed app(s):\n{apps:s}",
"app_manifest_invalid": "Something is wrong with the app manifest: {error}",
- "app_not_upgraded": "The app '{failed_app}' failed to upgrade, and as a consequence the following apps upgrades have been cancelled: {apps}",
- "app_upgrade_stopped": "The upgrade of all applications has been stopped to prevent possible damage because the previous application failed to upgrade",
+ "app_not_upgraded": "The app '{failed_app}' failed to upgrade, and as a consequence the following apps' upgrades have been cancelled: {apps}",
"app_not_correctly_installed": "{app:s} seems to be incorrectly installed",
- "app_not_installed": "Could not find the application '{app:s}' in the list of installed apps: {all_apps}",
+ "app_not_installed": "Could not find the app '{app:s}' in the list of installed apps: {all_apps}",
"app_not_properly_removed": "{app:s} has not been properly removed",
- "app_package_need_update": "The app {app} package needs to be updated to follow YunoHost changes",
"app_removed": "{app:s} removed",
"app_requirements_checking": "Checking required packages for {app}…",
- "app_requirements_failed": "Some requirements are not met for {app}: {error}",
"app_requirements_unmeet": "Requirements are not met for {app}, the package {pkgname} ({version}) must be {spec}",
+ "app_remove_after_failed_install": "Removing the app following the installation failure…",
"app_sources_fetch_failed": "Could not fetch sources files, is the URL correct?",
- "app_start_install": "Installing application {app}…",
- "app_start_remove": "Removing application {app}…",
- "app_start_backup": "Collecting files to be backed up for {app}…",
- "app_start_restore": "Restoring application {app}…",
+ "app_start_install": "Installing the app '{app}'…",
+ "app_start_remove": "Removing the app '{app}'…",
+ "app_start_backup": "Collecting files to be backed up for the app '{app}'…",
+ "app_start_restore": "Restoring the app '{app}'…",
"app_unknown": "Unknown app",
"app_unsupported_remote_type": "Unsupported remote type used for the app",
"app_upgrade_several_apps": "The following apps will be upgraded: {apps}",
"app_upgrade_app_name": "Now upgrading {app}…",
- "app_upgrade_failed": "Could not upgrade {app:s}",
- "app_upgrade_some_app_failed": "Some applications could not be upgraded",
+ "app_upgrade_failed": "Could not upgrade {app:s}: {error}",
+ "app_upgrade_script_failed": "An error occurred inside the app upgrade script",
+ "app_upgrade_some_app_failed": "Some apps could not be upgraded",
"app_upgraded": "{app:s} upgraded",
- "apps_already_up_to_date": "All applications are already up-to-date",
- "apps_permission_not_found": "No permission found for the installed apps",
- "apps_permission_restoration_failed": "Grant the permission permission '{permission:s}' to restore {app:s}",
- "appslist_corrupted_json": "Could not load the application lists. It looks like {filename:s} is damaged.",
- "appslist_could_not_migrate": "Could not migrate the app list {appslist:s}! Could not parse the URL… The old cron job was kept kept in {bkp_file:s}.",
- "appslist_fetched": "Updated application list {appslist:s} fetched",
- "appslist_migrating": "Migrating application list {appslist:s}…",
- "appslist_name_already_tracked": "A registered application list with name {name:s} already exists.",
- "appslist_removed": "{appslist:s} application list removed",
- "appslist_retrieve_bad_format": "Could not read the fetched application list {appslist:s}",
- "appslist_retrieve_error": "Cannot retrieve the remote application list {appslist:s}: {error:s}",
- "appslist_unknown": "Application list {appslist:s} unknown.",
- "appslist_url_already_tracked": "There is already a registered application list with the URL {url:s}.",
- "ask_current_admin_password": "Current administration password",
+ "apps_already_up_to_date": "All apps are already up-to-date",
+ "apps_catalog_init_success": "App catalog system initialized!",
+ "apps_catalog_updating": "Updating application catalog…",
+ "apps_catalog_failed_to_download": "Unable to download the {apps_catalog} app catalog: {error}",
+ "apps_catalog_obsolete_cache": "The app catalog cache is empty or obsolete.",
+ "apps_catalog_update_success": "The application catalog has been updated!",
"ask_email": "E-mail address",
"ask_firstname": "First name",
"ask_lastname": "Last name",
- "ask_list_to_remove": "List to remove",
"ask_main_domain": "Main domain",
"ask_new_admin_password": "New administration password",
"ask_new_domain": "New domain",
"ask_new_path": "New path",
"ask_password": "Password",
- "ask_path": "Path",
"backup_abstract_method": "This backup method has yet to be implemented",
"backup_actually_backuping": "Creating a backup archive from the collected files…",
"backup_app_failed": "Could not back up the app '{app:s}'",
@@ -85,12 +74,14 @@
"backup_archive_name_exists": "A backup archive with this name already exists.",
"backup_archive_name_unknown": "Unknown local backup archive named '{name:s}'",
"backup_archive_open_failed": "Could not open the backup archive",
+ "backup_archive_cant_retrieve_info_json": "Could not load infos for archive '{archive}' ... The info.json cannot be retrieved (or is not a valid json).",
+ "backup_archive_corrupted": "It looks like the backup archive '{archive}' is corrupted : {error}",
"backup_archive_system_part_not_available": "System part '{part:s}' unavailable in this backup",
"backup_archive_writing_error": "Could not add the files '{source:s}' (named in the archive '{dest:s}') to be backed up into the compressed archive '{archive:s}'",
- "backup_ask_for_copying_if_needed": "Some files could not be prepared for backup using the method that avoids temporarily wasting space on the system. To perform the backup, {size:s}MB will be temporarily. Do you agree?",
+ "backup_ask_for_copying_if_needed": "Do you want to perform the backup using {size:s} MB temporarily? (This way is used since some files could not be prepared using a more efficient method.)",
"backup_borg_not_implemented": "The Borg backup method is not yet implemented",
"backup_cant_mount_uncompress_archive": "Could not mount the uncompressed archive as write protected",
- "backup_cleaning_failed": "Could not clean-up the temporary backup folder",
+ "backup_cleaning_failed": "Could not clean up the temporary backup folder",
"backup_copying_to_organize_the_archive": "Copying {size:s}MB to organize the archive",
"backup_couldnt_bind": "Could not bind {src:s} to {dest:s}.",
"backup_created": "Backup created",
@@ -102,7 +93,6 @@
"backup_delete_error": "Could not delete '{path:s}'",
"backup_deleted": "Backup deleted",
"backup_hook_unknown": "The backup hook '{hook:s}' is unknown",
- "backup_invalid_archive": "This is not a backup archive",
"backup_method_borg_finished": "Backup into Borg finished",
"backup_method_copy_finished": "Backup copy finalized",
"backup_method_custom_finished": "Custom backup method '{method:s}' finished",
@@ -110,10 +100,10 @@
"backup_mount_archive_for_restore": "Preparing archive for restoration…",
"backup_no_uncompress_archive_dir": "There is no such uncompressed archive directory",
"backup_nothings_done": "Nothing to save",
- "backup_output_directory_forbidden": "Pick a different output directory. Backups can not 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": "Pick a different output directory. Backups cannot 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": "You should pick an empty output directory",
"backup_output_directory_required": "You must provide an output directory for the backup",
- "backup_output_symlink_dir_broken": "You have a broken symlink in place of your archive directory '{path:s}'. You may have a specific setup to backup your data on another filesystem, in this case you probably forgot to remount or plug in your hard-drive or USB key.",
+ "backup_output_symlink_dir_broken": "Your archive directory '{path:s}' is a broken symlink. Maybe you forgot to re/mount or plug in the storage medium it points to.",
"backup_permission": "Backup permission for app {app:s}",
"backup_php5_to_php7_migration_may_fail": "Could not convert your archive to support PHP 7, you may be unable to restore your PHP apps (reason: {error:s})",
"backup_running_hooks": "Running backup hooks…",
@@ -121,7 +111,7 @@
"backup_unable_to_organize_files": "Could not use the quick method to organize files in the archive",
"backup_with_no_backup_script_for_app": "The app '{app:s}' has no backup script. Ignoring.",
"backup_with_no_restore_script_for_app": "The '{app:s}' has no restoration script, you will not be able to automatically restore the backup of this app.",
- "certmanager_acme_not_configured_for_domain": "Certificate for the domain '{domain:s}' does not appear to be correctly installed. Please run 'cert-install' for this domain first.",
+ "certmanager_acme_not_configured_for_domain": "The ACME challenge cannot be ran for {domain} right now because its nginx conf lacks the corresponding code snippet... Please make sure that your nginx configuration is up to date using `yunohost tools regen-conf nginx --dry-run --with-diff`.",
"certmanager_attempt_to_renew_nonLE_cert": "The certificate for the domain '{domain:s}' is not issued by Let's Encrypt. Cannot renew it automatically!",
"certmanager_attempt_to_renew_valid_cert": "The certificate for the domain '{domain:s}' is not about to expire! (You may use --force if you know what you're doing)",
"certmanager_attempt_to_replace_valid_cert": "You are attempting to overwrite a good and valid certificate for domain {domain:s}! (Use --force to bypass)",
@@ -131,36 +121,143 @@
"certmanager_cert_renew_success": "Let's Encrypt certificate renewed for the domain '{domain:s}'",
"certmanager_cert_signing_failed": "Could not sign the new certificate",
"certmanager_certificate_fetching_or_enabling_failed": "Trying to use the new certificate for {domain:s} did not work…",
- "certmanager_conflicting_nginx_file": "Could not prepare domain for ACME challenge: the NGINX configuration file {filepath:s} is conflicting and should be removed first",
"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_domain_cert_not_selfsigned": "The certificate for domain {domain:s} is not self-signed. Are you sure you want to replace it? (Use '--force' to do so.)",
- "certmanager_domain_dns_ip_differs_from_public_ip": "The DNS 'A' record for the 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 turn off those checks.)",
+ "certmanager_domain_dns_ip_differs_from_public_ip": "The DNS 'A' record for the domain '{domain:s}' is different from this server's 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 turn off those checks.)",
"certmanager_domain_http_not_working": "It seems the domain {domain:s} cannot be accessed through HTTP. Check that your DNS and NGINX configuration is correct",
"certmanager_domain_unknown": "Unknown domain '{domain:s}'",
"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 turn off those checks.)",
+ "certmanager_warning_subdomain_dns_record": "Subdomain '{subdomain:s}' does not resolve to the same IP address as '{domain:s}'. Some features will not be available until you fix this and regenerate the certificate.",
"certmanager_hit_rate_limit": "Too many certificates already issued for this exact set of domains {domain:s} recently. Please try again later. See https://letsencrypt.org/docs/rate-limits/ for more details",
"certmanager_http_check_timeout": "Timed out when server tried to contact itself through HTTP using a public IP address (domain '{domain:s}' with IP '{ip:s}'). You may be experiencing a hairpinning issue, or the firewall/router ahead of your server is misconfigured.",
"certmanager_no_cert_file": "Could not read the certificate file for the domain {domain:s} (file: {file:s})",
"certmanager_self_ca_conf_file_not_found": "Could not find configuration file for self-signing authority (file: {file:s})",
"certmanager_unable_to_parse_self_CA_name": "Could not parse name of self-signing authority (file: {file:s})",
- "confirm_app_install_warning": "Warning: This application may work, but is not well-integrated in YunoHost. Some features such as single sign-on and backup/restore might not be available. Install anyway? [{answers:s}] ",
- "confirm_app_install_danger": "WARNING! This application is still experimental (if not explicitly not working) and it is likely to break your system! You should probably NOT install it unless you know what you are doing. Are you willing to take that risk? [{answers:s}] ",
- "confirm_app_install_thirdparty": "WARNING! Installing third-party applications may compromise the integrity and security of your system. You should probably NOT install it unless you know what you are doing. Are you willing to take that risk? [{answers:s}] ",
+ "confirm_app_install_warning": "Warning: This app may work, but is not well-integrated in YunoHost. Some features such as single sign-on and backup/restore might not be available. Install anyway? [{answers:s}] ",
+ "confirm_app_install_danger": "DANGER! This app is known to be still experimental (if not explicitly not working)! You should probably NOT install it unless you know what you are doing. NO SUPPORT will be provided if this app doesn't work or breaks your system… If you are willing to take that risk anyway, type '{answers:s}'",
+ "confirm_app_install_thirdparty": "DANGER! This app is not part of Yunohost's app catalog. Installing third-party apps may compromise the integrity and security of your system. You should probably NOT install it unless you know what you are doing. NO SUPPORT will be provided if this app doesn't work or breaks your system… If you are willing to take that risk anyway, type '{answers:s}'",
"custom_app_url_required": "You must provide a URL to upgrade your custom app {app:s}",
- "custom_appslist_name_required": "You must provide a name for your custom app list",
- "diagnosis_debian_version_error": "Could not retrieve the Debian version: {error}",
- "diagnosis_kernel_version_error": "Could not retrieve kernel version: {error}",
- "diagnosis_monitor_disk_error": "Could not monitor disks: {error}",
- "diagnosis_monitor_system_error": "Could not monitor system: {error}",
- "diagnosis_no_apps": "No installed application",
- "dpkg_is_broken": "You cannot do this right now because dpkg/APT (the system package managers) seems to be in a broken state… You can try to solve this issue by connecting through SSH and running `sudo dpkg --configure -a`.",
- "dpkg_lock_not_available": "This command can't be ran right now because another program seems to be using the lock of dpkg (the system package manager)",
- "domain_cannot_remove_main": "Cannot remove main domain. Set one first",
+ "diagnosis_basesystem_hardware": "Server hardware architecture is {virt} {arch}",
+ "diagnosis_basesystem_hardware_board": "Server board model is {model}",
+ "diagnosis_basesystem_host": "Server is running Debian {debian_version}",
+ "diagnosis_basesystem_kernel": "Server is running Linux kernel {kernel_version}",
+ "diagnosis_basesystem_ynh_single_version": "{package} version: {version} ({repo})",
+ "diagnosis_basesystem_ynh_main_version": "Server is running YunoHost {main_version} ({repo})",
+ "diagnosis_basesystem_ynh_inconsistent_versions": "You are running inconsistent versions of the YunoHost packages... most probably because of a failed or partial upgrade.",
+ "diagnosis_display_tip": "To see the issues found, you can go to the Diagnosis section of the webadmin, or run 'yunohost diagnosis show --issues' from the command-line.",
+ "diagnosis_failed_for_category": "Diagnosis failed for category '{category}': {error}",
+ "diagnosis_cache_still_valid": "(Cache still valid for {category} diagnosis. Won't re-diagnose it yet!)",
+ "diagnosis_cant_run_because_of_dep": "Can't run diagnosis for {category} while there are important issues related to {dep}.",
+ "diagnosis_ignored_issues": "(+ {nb_ignored} ignored issue(s))",
+ "diagnosis_found_errors": "Found {errors} significant issue(s) related to {category}!",
+ "diagnosis_found_errors_and_warnings": "Found {errors} significant issue(s) (and {warnings} warning(s)) related to {category}!",
+ "diagnosis_found_warnings": "Found {warnings} item(s) that could be improved for {category}.",
+ "diagnosis_everything_ok": "Everything looks good for {category}!",
+ "diagnosis_failed": "Failed to fetch diagnosis result for category '{category}': {error}",
+ "diagnosis_no_cache": "No diagnosis cache yet for category '{category}'",
+ "diagnosis_ip_connected_ipv4": "The server is connected to the Internet through IPv4 !",
+ "diagnosis_ip_no_ipv4": "The server does not have working IPv4.",
+ "diagnosis_ip_connected_ipv6": "The server is connected to the Internet through IPv6 !",
+ "diagnosis_ip_no_ipv6": "The server does not have working IPv6.",
+ "diagnosis_ip_global": "Global IP: {global}
",
+ "diagnosis_ip_local": "Local IP: {local}
",
+ "diagnosis_ip_not_connected_at_all": "The server does not seem to be connected to the Internet at all!?",
+ "diagnosis_ip_dnsresolution_working": "Domain name resolution is working!",
+ "diagnosis_ip_broken_dnsresolution": "Domain name resolution seems to be broken for some reason... Is a firewall blocking DNS requests ?",
+ "diagnosis_ip_broken_resolvconf": "Domain name resolution seems to be broken on your server, which seems related to /etc/resolv.conf
not pointing to 127.0.0.1
.",
+ "diagnosis_ip_weird_resolvconf": "DNS resolution seems to be working, but it looks like you're using a custom /etc/resolv.conf
.",
+ "diagnosis_ip_weird_resolvconf_details": "The file /etc/resolv.conf
should be a symlink to /etc/resolvconf/run/resolv.conf
itself pointing to 127.0.0.1
(dnsmasq). If you want to manually configure DNS resolvers, please edit /etc/resolv.dnsmasq.conf
.",
+ "diagnosis_dns_good_conf": "DNS records are correctly configured for domain {domain} (category {category})",
+ "diagnosis_dns_bad_conf": "Some DNS records are missing or incorrect for domain {domain} (category {category})",
+ "diagnosis_dns_missing_record": "According to the recommended DNS configuration, you should add a DNS record with the following info.
Type: {type}
Name: {name}
Value: {value}
",
+ "diagnosis_dns_discrepancy": "The following DNS record does not seem to follow the recommended configuration:
Type: {type}
Name: {name}
Current value: {current}
Excepted value: {value}
",
+ "diagnosis_dns_point_to_doc": "Please check the documentation at https://yunohost.org/dns_config if you need help about configuring DNS records.",
+ "diagnosis_services_running": "Service {service} is running!",
+ "diagnosis_services_conf_broken": "Configuration is broken for service {service}!",
+ "diagnosis_services_bad_status": "Service {service} is {status} :(",
+ "diagnosis_services_bad_status_tip": "You can try to restart the service, and if it doesn't work, have a look at the service logs in the webadmin (from the command line, you can do this with yunohost service restart {service} and yunohost service log {service}).",
+ "diagnosis_diskusage_verylow": "Storage {mountpoint}
(on device {device}
) has only {free} ({free_percent}%) space remaining (out of {total}). You should really consider cleaning up some space!",
+ "diagnosis_diskusage_low": "Storage {mountpoint}
(on device {device}
) has only {free} ({free_percent}%) space remaining (out of {total}). Be careful.",
+ "diagnosis_diskusage_ok": "Storage {mountpoint}
(on device {device}
) still has {free} ({free_percent}%) space left (out of {total})!",
+ "diagnosis_ram_verylow": "The system has only {available} ({available_percent}%) RAM available! (out of {total})",
+ "diagnosis_ram_low": "The system has {available} ({available_percent}%) RAM available (out of {total}). Be careful.",
+ "diagnosis_ram_ok": "The system still has {available} ({available_percent}%) RAM available out of {total}.",
+ "diagnosis_swap_none": "The system has no swap at all. You should consider adding at least {recommended} of swap to avoid situations where the system runs out of memory.",
+ "diagnosis_swap_notsomuch": "The system has only {total} swap. You should consider having at least {recommended} to avoid situations where the system runs out of memory.",
+ "diagnosis_swap_ok": "The system has {total} of swap!",
+ "diagnosis_mail_outgoing_port_25_ok": "The SMTP mail server is able to send emails (outgoing port 25 is not blocked).",
+ "diagnosis_mail_outgoing_port_25_blocked": "The SMTP mail server cannot send emails to other servers because outgoing port 25 is blocked in IPv{ipversion}.",
+ "diagnosis_mail_outgoing_port_25_blocked_details": "You should first try to unblock outgoing port 25 in your internet router interface or your hosting provider interface. (Some hosting provider may require you to send them a support ticket for this).",
+ "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Some providers won't let you unblock outgoing port 25 because they don't care about Net Neutrality.
- Some of them provide the alternative of using a mail server relay though it implies that the relay will be able to spy on your email traffic.
- A privacy-friendly alternative is to use a VPN *with a dedicated public IP* to bypass this kind of limits. See https://yunohost.org/#/vpn_advantage
- You can also consider switching to a more net neutrality-friendly provider",
+ "diagnosis_mail_ehlo_ok": "The SMTP mail server is reachable from the outside and therefore is able to receive emails!",
+ "diagnosis_mail_ehlo_unreachable": "The SMTP mail server is unreachable from the outside on IPv{ipversion}. It won't be able to receive emails.",
+ "diagnosis_mail_ehlo_unreachable_details": "Could not open a connection on port 25 to your server in IPv{ipversion}. It appears to be unreachable.
1. The most common cause for this issue is that port 25 is not correctly forwarded to your server.
2. You should also make sure that service postfix is running.
3. On more complex setups: make sure that no firewall or reverse-proxy is interfering.",
+ "diagnosis_mail_ehlo_bad_answer": "A non-SMTP service answered on port 25 on IPv{ipversion}",
+ "diagnosis_mail_ehlo_bad_answer_details": "It could be due to an other machine answering instead of your server.",
+ "diagnosis_mail_ehlo_wrong": "A different SMTP mail server answers on IPv{ipversion}. It will probably not be able to receive emails.",
+ "diagnosis_mail_ehlo_wrong_details": "The EHLO received by the remote diagnoser in IPv{ipversion} is different from your server's domain.
Received EHLO: {wrong_ehlo}
Expected: {right_ehlo}
The most common cause for this issue is that port 25 is not correctly forwarded to your server. Alternatively, make sure that no firewall or reverse-proxy is interfering.",
+ "diagnosis_mail_ehlo_could_not_diagnose": "Could not diagnose if postfix mail server is reachable from outside in IPv{ipversion}.",
+ "diagnosis_mail_ehlo_could_not_diagnose_details": "Error: {error}",
+ "diagnosis_mail_fcrdns_ok": "Your reverse DNS is correctly configured!",
+ "diagnosis_mail_fcrdns_dns_missing": "No reverse DNS is defined in IPv{ipversion}. Some emails may fail to get delivered or may get flagged as spam.",
+ "diagnosis_mail_fcrdns_nok_details": "You should first try to configure the reverse DNS with {ehlo_domain}
in your internet router interface or your hosting provider interface. (Some hosting provider may require you to send them a support ticket for this).",
+ "diagnosis_mail_fcrdns_nok_alternatives_4": "Some providers won't let you configure your reverse DNS (or their feature might be broken...). If you are experiencing issues because of this, consider the following solutions:
- Some ISP provide the alternative of using a mail server relay though it implies that the relay will be able to spy on your email traffic.
- A privacy-friendly alternative is to use a VPN *with a dedicated public IP* to bypass this kind of limits. See https://yunohost.org/#/vpn_advantage
- Finally, it's also possible to change of provider",
+ "diagnosis_mail_fcrdns_nok_alternatives_6": "Some providers won't let you configure your reverse DNS (or their feature might be broken...). If your reverse DNS is correctly configured for IPv4, you can try disabling the use of IPv6 when sending emails by running yunohost settings set smtp.allow_ipv6 -v off. Note: this last solution means that you won't be able to send or receive emails from the few IPv6-only servers out there.",
+ "diagnosis_mail_fcrdns_different_from_ehlo_domain": "The reverse DNS is not correctly configured in IPv{ipversion}. Some emails may fail to get delivered or may get flagged as spam.",
+ "diagnosis_mail_fcrdns_different_from_ehlo_domain_details": "Current reverse DNS: {rdns_domain}
Expected value: {ehlo_domain}
",
+ "diagnosis_mail_blacklist_ok": "The IPs and domains used by this server do not appear to be blacklisted",
+ "diagnosis_mail_blacklist_listed_by": "Your IP or domain {item}
is blacklisted on {blacklist_name}",
+ "diagnosis_mail_blacklist_reason": "The blacklist reason is: {reason}",
+ "diagnosis_mail_blacklist_website": "After identifying why you are listed and fixed it, feel free to ask for delisting on {blacklist_website}",
+ "diagnosis_mail_queue_ok": "{nb_pending} pending emails in the mail queues",
+ "diagnosis_mail_queue_unavailable": "Can not consult number of pending emails in queue",
+ "diagnosis_mail_queue_unavailable_details": "Error: {error}",
+ "diagnosis_mail_queue_too_big": "Too many pending emails in mail queue ({nb_pending} emails)",
+ "diagnosis_regenconf_allgood": "All configurations files are in line with the recommended configuration!",
+ "diagnosis_regenconf_manually_modified": "Configuration file {file}
appears to have been manually modified.",
+ "diagnosis_regenconf_manually_modified_details": "This is probably OK if you know what you're doing! YunoHost will stop updating this file automatically... But beware that YunoHost upgrades could contain important recommended changes. If you want to, you can inspect the differences with yunohost tools regen-conf {category} --dry-run --with-diff and force the reset to the recommended configuration with yunohost tools regen-conf {category} --force",
+ "diagnosis_security_all_good": "No critical security vulnerability was found.",
+ "diagnosis_security_vulnerable_to_meltdown": "You appear vulnerable to the Meltdown criticial security vulnerability",
+ "diagnosis_security_vulnerable_to_meltdown_details": "To fix this, you should upgrade your system and reboot to load the new linux kernel (or contact your server provider if this doesn't work). See https://meltdownattack.com/ for more infos.",
+ "diagnosis_description_basesystem": "Base system",
+ "diagnosis_description_ip": "Internet connectivity",
+ "diagnosis_description_dnsrecords": "DNS records",
+ "diagnosis_description_services": "Services status check",
+ "diagnosis_description_systemresources": "System resources",
+ "diagnosis_description_ports": "Ports exposure",
+ "diagnosis_description_web": "Web",
+ "diagnosis_description_mail": "Email",
+ "diagnosis_description_regenconf": "System configurations",
+ "diagnosis_description_security": "Security checks",
+ "diagnosis_ports_could_not_diagnose": "Could not diagnose if ports are reachable from outside in IPv{ipversion}.",
+ "diagnosis_ports_could_not_diagnose_details": "Error: {error}",
+ "diagnosis_ports_unreachable": "Port {port} is not reachable from outside.",
+ "diagnosis_ports_partially_unreachable": "Port {port} is not reachable from outside in IPv{failed}.",
+ "diagnosis_ports_ok": "Port {port} is reachable from outside.",
+ "diagnosis_ports_needed_by": "Exposing this port is needed for {category} features (service {service})",
+ "diagnosis_ports_forwarding_tip": "To fix this issue, you most probably need to configure port forwarding on your internet router as described in https://yunohost.org/isp_box_config",
+ "diagnosis_http_hairpinning_issue": "Your local network does not seem to have hairpinning enabled.",
+ "diagnosis_http_hairpinning_issue_details": "This is probably because of your ISP box / router. As a result, people from outside your local network will be able to access your server as expected, but not people from inside the local network (like you, probably?). You may be able to improve the situation by having a look at https://yunohost.org/dns_local_network",
+ "diagnosis_http_could_not_diagnose": "Could not diagnose if domains are reachable from outside in IPv{ipversion}.",
+ "diagnosis_http_could_not_diagnose_details": "Error: {error}",
+ "diagnosis_http_ok": "Domain {domain} is reachable through HTTP from outside the local network.",
+ "diagnosis_http_timeout": "Timed-out while trying to contact your server from outside. It appears to be unreachable.
1. The most common cause for this issue is that port 80 (and 443) are not correctly forwarded to your server.
2. You should also make sure that the service nginx is running
3. On more complex setups: make sure that no firewall or reverse-proxy is interfering.",
+ "diagnosis_http_connection_error": "Connection error: could not connect to the requested domain, it's very likely unreachable.",
+ "diagnosis_http_bad_status_code": "It looks like another machine (maybe your internet router) answered instead of your server.
1. The most common cause for this issue is that port 80 (and 443) are not correctly forwarded to your server.
2. On more complex setups: make sure that no firewall or reverse-proxy is interfering.",
+ "diagnosis_http_unreachable": "Domain {domain} appears unreachable through HTTP from outside the local network.",
+ "diagnosis_http_partially_unreachable": "Domain {domain} appears unreachable through HTTP from outside the local network in IPv{failed}, though it works in IPv{passed}.",
+ "diagnosis_http_nginx_conf_not_up_to_date": "This domain's nginx configuration appears to have been modified manually, and prevents YunoHost from diagnosing if it's reachable on HTTP.",
+ "diagnosis_http_nginx_conf_not_up_to_date_details": "To fix the situation, inspect the difference with the command line using yunohost tools regen-conf nginx --dry-run --with-diff and if you're ok, apply the changes with yunohost tools regen-conf nginx --force.",
+ "diagnosis_unknown_categories": "The following categories are unknown: {categories}",
+ "diagnosis_never_ran_yet": "It looks like this server was setup recently and there's no diagnosis report to show yet. You should start by running a full diagnosis, either from the webadmin or using 'yunohost diagnosis run' from the command line.",
+ "domain_cannot_remove_main": "You cannot remove '{domain:s}' since it's the main domain, you first need to set another domain as the main domain using 'yunohost domain main-domain -n '; here is the list of candidate domains: {other_domains:s}",
+ "domain_cannot_add_xmpp_upload": "You cannot add domains starting with 'xmpp-upload.'. This kind of name is reserved for the XMPP upload feature integrated in YunoHost.",
+ "domain_cannot_remove_main_add_new_one": "You cannot remove '{domain:s}' since it's the main domain and your only domain, you need to first add another domain using 'yunohost domain add ', then set is as the main domain using 'yunohost domain main-domain -n ' and then you can remove the domain '{domain:s}' using 'yunohost domain remove {domain:s}'.'",
"domain_cert_gen_failed": "Could not generate certificate",
"domain_created": "Domain created",
- "domain_creation_failed": "Could not create domain",
+ "domain_creation_failed": "Unable to create domain {domain}: {error}",
"domain_deleted": "Domain deleted",
- "domain_deletion_failed": "Could not delete domain",
+ "domain_deletion_failed": "Unable to delete domain {domain}: {error}",
"domain_dns_conf_is_just_a_recommendation": "This command shows you the *recommended* configuration. It does not actually set up the DNS configuration for you. It is your responsability to configure your DNS zone in your registrar according to this recommendation.",
"domain_dyndns_already_subscribed": "You have already subscribed to a DynDNS domain",
"domain_dyndns_root_unknown": "Unknown DynDNS root domain",
@@ -171,6 +268,8 @@
"domains_available": "Available domains:",
"done": "Done",
"downloading": "Downloading…",
+ "dpkg_is_broken": "You cannot do this right now because dpkg/APT (the system package managers) seems to be in a broken state… You can try to solve this issue by connecting through SSH and running `sudo dpkg --configure -a`.",
+ "dpkg_lock_not_available": "This command can't be run right now because another program seems to be using the lock of dpkg (the system package manager)",
"dyndns_could_not_check_provide": "Could not check if {provider:s} can provide {domain:s}.",
"dyndns_could_not_check_available": "Could not check if {domain:s} is available on {provider:s}.",
"dyndns_cron_installed": "DynDNS cron job created",
@@ -181,14 +280,11 @@
"dyndns_key_generating": "Generating DNS key… It may take a while.",
"dyndns_key_not_found": "DNS key not found for the domain",
"dyndns_no_domain_registered": "No domain registered with DynDNS",
- "dyndns_provider_unreachable": "Unable to reach Dyndns provider {provider}: either your YunoHost is not correctly connected to the internet or the dynette server is down.",
+ "dyndns_provider_unreachable": "Unable to reach DynDNS provider {provider}: either your YunoHost is not correctly connected to the internet or the dynette server is down.",
"dyndns_registered": "DynDNS domain registered",
"dyndns_registration_failed": "Could not register DynDNS domain: {error:s}",
"dyndns_domain_not_provided": "DynDNS provider {provider:s} cannot provide domain {domain:s}.",
"dyndns_unavailable": "The domain '{domain:s}' is unavailable.",
- "edit_group_not_allowed": "You are not allowed to edit the group {group:s}",
- "edit_permission_with_group_all_users_not_allowed": "You are not allowed to edit permission for the group 'all_users', use 'yunohost user permission clear APP' or 'yunohost user permission add APP -u USER' instead.",
- "error_when_removing_sftpuser_group": "Could not remove the sftpusers group",
"executing_command": "Executing command '{command:s}'…",
"executing_script": "Executing script '{script:s}'…",
"extracting": "Extracting…",
@@ -197,7 +293,7 @@
"file_does_not_exist": "The file {path:s} does not exist.",
"firewall_reload_failed": "Could not reload the firewall",
"firewall_reloaded": "Firewall reloaded",
- "firewall_rules_cmd_failed": "Some firewall rules commands have failed. More info in log.",
+ "firewall_rules_cmd_failed": "Some firewall rule commands have failed. More info in log.",
"global_settings_bad_choice_for_enum": "Bad choice for setting {setting:s}, received '{choice:s}', but available choices are: {available_choices:s}",
"global_settings_bad_type_for_setting": "Bad type for setting {setting:s}, received {received_type:s}, expected {expected_type:s}",
"global_settings_cant_open_settings": "Could not open settings file, reason: {reason:s}",
@@ -205,6 +301,7 @@
"global_settings_cant_write_settings": "Could not save settings file, reason: {reason:s}",
"global_settings_key_doesnt_exists": "The key '{settings_key:s}' does not exist in the global settings, you can see all the available keys by running 'yunohost settings list'",
"global_settings_reset_success": "Previous settings now backed up to {path:s}",
+ "global_settings_setting_pop3_enabled": "Enable the POP3 protocol for the mail server",
"global_settings_setting_example_bool": "Example boolean option",
"global_settings_setting_example_enum": "Example enum option",
"global_settings_setting_example_int": "Example int option",
@@ -216,26 +313,32 @@
"global_settings_setting_security_postfix_compatibility": "Compatibility vs. security tradeoff for the Postfix server. Affects the ciphers (and other security-related aspects)",
"global_settings_unknown_setting_from_settings_file": "Unknown key in settings: '{setting_key:s}', discard it and save it in /etc/yunohost/settings-unknown.json",
"global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "Allow the use of (deprecated) DSA hostkey for the SSH daemon configuration",
+ "global_settings_setting_smtp_allow_ipv6": "Allow the use of IPv6 to receive and send mail",
"global_settings_unknown_type": "Unexpected situation, the setting {setting:s} appears to have the type {unknown_type:s} but it is not a type supported by the system.",
- "good_practices_about_admin_password": "You are now about to define a new administration password. The password should be at-least 8 characters—though it is good practice to use a longer password (i.e. a passphrase) and/or to use a variation of characters (uppercase, lowercase, digits and special characters).",
- "good_practices_about_user_password": "You are now about to define a new user password. The password should be at least 8 characters—though it is good practice to use longer password (i.e. a passphrase) and/or to a variation of characters (uppercase, lowercase, digits and special characters).",
- "group_already_allowed": "Group '{group:s}' already has permission '{permission:s}' turned on for the app '{app:s}'",
- "group_already_disallowed": "Group '{group:s}' already has permissions '{permission:s}' turned off for the app '{app:s}'",
- "group_name_already_exist": "Group {name:s} already exists",
+ "good_practices_about_admin_password": "You are now about to define a new administration password. The password should be at least 8 characters long—though it is good practice to use a longer password (i.e. a passphrase) and/or to use a variation of characters (uppercase, lowercase, digits and special characters).",
+ "good_practices_about_user_password": "You are now about to define a new user password. The password should be at least 8 characters long—though it is good practice to use a longer password (i.e. a passphrase) and/or to a variation of characters (uppercase, lowercase, digits and special characters).",
+ "group_already_exist": "Group {group} already exists",
+ "group_already_exist_on_system": "Group {group} already exists in the system groups",
+ "group_already_exist_on_system_but_removing_it": "Group {group} already exists in the system groups, but YunoHost will remove it…",
"group_created": "Group '{group}' created",
- "group_creation_failed": "Could not create the group '{group}'",
+ "group_creation_failed": "Could not create the group '{group}': {error}",
+ "group_cannot_edit_all_users": "The group 'all_users' cannot be edited manually. It is a special group meant to contain all users registered in YunoHost",
+ "group_cannot_edit_visitors": "The group 'visitors' cannot be edited manually. It is a special group representing anonymous visitors",
+ "group_cannot_edit_primary_group": "The group '{group}' cannot be edited manually. It is the primary group meant to contain only one specific user.",
+ "group_cannot_be_deleted": "The group {group} cannot be deleted manually.",
"group_deleted": "Group '{group}' deleted",
- "group_deletion_failed": "Could not delete the group '{group}'",
- "group_deletion_not_allowed": "The group {group:s} cannot be deleted manually.",
+ "group_deletion_failed": "Could not delete the group '{group}': {error}",
"group_unknown": "The group '{group:s}' is unknown",
"group_updated": "Group '{group}' updated",
- "group_update_failed": "Could not update the group '{group}'",
+ "group_update_failed": "Could not update the group '{group}': {error}",
+ "group_user_already_in_group": "User {user} is already in group {group}",
+ "group_user_not_in_group": "User {user} is not in group {group}",
"hook_exec_failed": "Could not run script: {path:s}",
"hook_exec_not_terminated": "Script did not finish properly: {path:s}",
"hook_json_return_error": "Could not read return from hook {path:s}. Error: {msg:s}. Raw content: {raw_content}",
"hook_list_by_invalid": "This property can not be used to list hooks",
"hook_name_unknown": "Unknown hook name '{name:s}'",
- "installation_complete": "Installation complete",
+ "installation_complete": "Installation completed",
"installation_failed": "Something went wrong with the installation",
"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",
@@ -245,18 +348,16 @@
"log_help_to_get_log": "To view the log of the operation '{desc}', use the command 'yunohost log display {name}'",
"log_link_to_failed_log": "Could not complete the operation '{desc}'. Please provide the full log of this operation by clicking here to get help",
"log_help_to_get_failed_log": "The operation '{desc}' could not be completed. Please share the full log of this operation using the command 'yunohost log display {name} --share' to get help",
- "log_does_exists": "There is not operation log with the name '{log}', use 'yunohost log list' to see all available operation logs",
+ "log_does_exists": "There is no operation log with the name '{log}', use 'yunohost log list' to see all available operation logs",
"log_operation_unit_unclosed_properly": "Operation unit has not been closed properly",
- "log_app_addaccess": "Add access to '{}'",
- "log_app_removeaccess": "Remove access to '{}'",
- "log_app_clearaccess": "Remove all access to '{}'",
- "log_app_fetchlist": "Add an application list",
- "log_app_removelist": "Remove an application list",
- "log_app_change_url": "Change the URL of '{}' application",
- "log_app_install": "Install the '{}' application",
- "log_app_remove": "Remove the '{}' application",
- "log_app_upgrade": "Upgrade the '{}' application",
- "log_app_makedefault": "Make '{}' the default application",
+ "log_app_change_url": "Change the URL of the '{}' app",
+ "log_app_install": "Install the '{}' app",
+ "log_app_remove": "Remove the '{}' app",
+ "log_app_upgrade": "Upgrade the '{}' app",
+ "log_app_makedefault": "Make '{}' the default app",
+ "log_app_action_run": "Run action of the '{}' app",
+ "log_app_config_show_panel": "Show the config panel of the '{}' app",
+ "log_app_config_apply": "Apply config to the '{}' app",
"log_available_on_yunopaste": "This log is now available via {url}",
"log_backup_restore_system": "Restore system from a backup archive",
"log_backup_restore_app": "Restore '{}' from a backup archive",
@@ -266,38 +367,37 @@
"log_domain_remove": "Remove '{}' domain from system configuration",
"log_dyndns_subscribe": "Subscribe to a YunoHost subdomain '{}'",
"log_dyndns_update": "Update the IP associated with your YunoHost subdomain '{}'",
- "log_letsencrypt_cert_install": "Install a Let's encrypt certificate on '{}' domain",
- "log_permission_add": "Add the '{}' permission for the app '{}'",
- "log_permission_remove": "Remove permission '{}'",
- "log_permission_update": "Update permission '{}' for app '{}'",
- "log_selfsigned_cert_install": "Install self signed certificate on '{}' domain",
- "log_letsencrypt_cert_renew": "Renew '{}' Let's encrypt certificate",
+ "log_letsencrypt_cert_install": "Install a Let's Encrypt certificate on '{}' domain",
+ "log_permission_create": "Create permission '{}'",
+ "log_permission_delete": "Delete permission '{}'",
+ "log_permission_url": "Update url related to permission '{}'",
+ "log_selfsigned_cert_install": "Install self-signed certificate on '{}' domain",
+ "log_letsencrypt_cert_renew": "Renew '{}' Let's Encrypt certificate",
"log_regen_conf": "Regenerate system configurations '{}'",
"log_user_create": "Add '{}' user",
"log_user_delete": "Delete '{}' user",
- "log_user_group_add": "Add '{}' group",
+ "log_user_group_create": "Create '{}' group",
"log_user_group_delete": "Delete '{}' group",
"log_user_group_update": "Update '{}' group",
"log_user_update": "Update user info of '{}'",
- "log_user_permission_add": "Update '{}' permission",
- "log_user_permission_remove": "Update '{}' permission",
- "log_tools_maindomain": "Make '{}' the main domain",
- "log_tools_migrations_migrate_forward": "Migrate forward",
+ "log_user_permission_update": "Update accesses for permission '{}'",
+ "log_user_permission_reset": "Reset permission '{}'",
+ "log_domain_main_domain": "Make '{}' the main domain",
+ "log_tools_migrations_migrate_forward": "Run migrations",
"log_tools_postinstall": "Postinstall your YunoHost server",
"log_tools_upgrade": "Upgrade system packages",
"log_tools_shutdown": "Shutdown your server",
"log_tools_reboot": "Reboot your server",
"ldap_init_failed_to_create_admin": "LDAP initialization could not create admin user",
"ldap_initialized": "LDAP initialized",
- "license_undefined": "undefined",
"mail_alias_remove_failed": "Could not remove e-mail alias '{mail:s}'",
- "mail_domain_unknown": "Unknown e-mail address for domain '{domain:s}'",
+ "mail_domain_unknown": "Invalid e-mail address for domain '{domain:s}'. Please, use a domain administrated by this server.",
"mail_forward_remove_failed": "Could not remove e-mail forwarding '{mail:s}'",
"mailbox_disabled": "E-mail turned off for user {user:s}",
- "mailbox_used_space_dovecot_down": "The Dovecot mailbox service needs to be up, if you want to fetch used mailbox space",
+ "mailbox_used_space_dovecot_down": "The Dovecot mailbox service needs to be up if you want to fetch used mailbox space",
"mail_unavailable": "This e-mail address is reserved and shall be automatically allocated to the very first user",
- "maindomain_change_failed": "Could not change the main domain",
- "maindomain_changed": "The main domain now changed",
+ "main_domain_change_failed": "Unable to change the main domain",
+ "main_domain_changed": "The main domain has been changed",
"migrate_tsig_end": "Migration to HMAC-SHA-512 finished",
"migrate_tsig_failed": "Could not migrate the DynDNS domain '{domain}' to HMAC-SHA-512, rolling back. Error: {error_code}, {error}",
"migrate_tsig_start": "Insufficiently secure key algorithm detected for TSIG signature of the domain '{domain}', initiating migration to the more secure HMAC-SHA-512",
@@ -315,48 +415,51 @@
"migration_description_0007_ssh_conf_managed_by_yunohost_step1": "Let the SSH configuration be managed by YunoHost (step 1, automatic)",
"migration_description_0008_ssh_conf_managed_by_yunohost_step2": "Let the SSH configuration be managed by YunoHost (step 2, manual)",
"migration_description_0009_decouple_regenconf_from_services": "Decouple the regen-conf mechanism from services",
- "migration_description_0010_migrate_to_apps_json": "Remove deprecated applists and use the new unified 'apps.json' list instead",
- "migration_description_0011_setup_group_permission": "Set up user group and set up permission for apps and services",
+ "migration_description_0010_migrate_to_apps_json": "Remove deprecated apps catalogs and use the new unified 'apps.json' list instead (outdated, replaced by migration 13)",
+ "migration_description_0011_setup_group_permission": "Set up user groups and permissions for apps and services",
"migration_description_0012_postgresql_password_to_md5_authentication": "Force PostgreSQL authentication to use MD5 for local connections",
+ "migration_description_0013_futureproof_apps_catalog_system": "Migrate to the new future-proof apps catalog system",
+ "migration_description_0014_remove_app_status_json": "Remove legacy status.json app files",
"migration_0003_start": "Starting migration to Stretch. The logs will be available in {logfile}.",
"migration_0003_patching_sources_list": "Patching the sources.lists…",
"migration_0003_main_upgrade": "Starting main upgrade…",
"migration_0003_fail2ban_upgrade": "Starting the Fail2Ban upgrade…",
- "migration_0003_restoring_origin_nginx_conf": "Your file /etc/nginx/nginx.conf was edited somehow. The migration is going to reset to its original state first… The previous file will be available as {backup_dest}.",
- "migration_0003_yunohost_upgrade": "Starting the YunoHost package upgrade… The migration will end, but the actual upgrade will happen immediately afterwards. After the operation is complete, you might have to log in on the webadmin page again.",
+ "migration_0003_restoring_origin_nginx_conf": "Your file /etc/nginx/nginx.conf was edited somehow. The migration is going to reset it to its original state first… The previous file will be available as {backup_dest}.",
+ "migration_0003_yunohost_upgrade": "Starting the YunoHost package upgrade… The migration will end, but the actual upgrade will happen immediately afterwards. After the operation is complete, you might have to log in to the webadmin page again.",
"migration_0003_not_jessie": "The current Debian distribution is not Jessie!",
"migration_0003_system_not_fully_up_to_date": "Your system is not fully up-to-date. Please perform a regular upgrade before running the migration to Stretch.",
"migration_0003_still_on_jessie_after_main_upgrade": "Something went wrong during the main upgrade: Is the system still on Jessie‽ To investigate the issue, please look at {log}:s…",
"migration_0003_general_warning": "Please note that this migration is a delicate operation. The YunoHost team did its best to review and test it, but the migration might still break parts of the system or its apps.\n\nTherefore, it is recommended to:\n - Perform a backup of any critical data or app. More info on https://yunohost.org/backup;\n - Be patient after launching the migration: Depending on your Internet connection and hardware, it might take up to a few hours for everything to upgrade.\n\nAdditionally, the port for SMTP, used by external e-mail clients (like Thunderbird or K9-Mail) was changed from 465 (SSL/TLS) to 587 (STARTTLS). The old port (465) will automatically be closed, and the new port (587) will be opened in the firewall. You and your users *will* have to adapt the configuration of your e-mail clients accordingly.",
- "migration_0003_problematic_apps_warning": "Please note that the following possibly problematic installed apps were detected. It looks like those were not installed from an applist, or are not flagged as 'working'. Consequently, it cannot be guaranteed that they will still work after the upgrade: {problematic_apps}",
+ "migration_0003_problematic_apps_warning": "Please note that the following possibly problematic installed apps were detected. It looks like those were not installed from an app catalog, or are not flagged as 'working'. Consequently, it cannot be guaranteed that they will still work after the upgrade: {problematic_apps}",
"migration_0003_modified_files": "Please note that the following files were found to be manually modified and might be overwritten following the upgrade: {manually_modified_files}",
"migration_0005_postgresql_94_not_installed": "PostgreSQL was not installed on your system. Nothing to do.",
- "migration_0005_postgresql_96_not_installed": "PostgreSQL 9.4 is installed, but not postgresql 9.6‽ Something weird might have happened on your system:(…",
+ "migration_0005_postgresql_96_not_installed": "PostgreSQL 9.4 is installed, but not postgresql 9.6‽ Something weird might have happened on your system :(…",
"migration_0005_not_enough_space": "Make sufficient space available in {path} to run the migration.",
- "migration_0006_disclaimer": "YunoHost now expects admin and root passwords to be synchronized. By running this migration, your root password is going to be replaced by the admin password.",
- "migration_0007_cancelled": "YunoHost has failed to improve the way your SSH conf is managed.",
+ "migration_0006_disclaimer": "YunoHost now expects the admin and root passwords to be synchronized. This migration replaces your root password with the admin password.",
+ "migration_0007_cancelled": "Could not improve the way your SSH configuration is managed.",
"migration_0007_cannot_restart": "SSH can't be restarted after trying to cancel migration number 6.",
"migration_0008_general_disclaimer": "To improve the security of your server, it is recommended to let YunoHost manage the SSH configuration. Your current SSH setup differs from the recommendation. If you let YunoHost reconfigure it, the way you connect to your server through SSH will change thusly:",
"migration_0008_port": "• You will have to connect using port 22 instead of your current custom SSH port. Feel free to reconfigure it;",
"migration_0008_root": "• You will not be able to connect as root through SSH. Instead you should use the admin user;",
"migration_0008_dsa": "• The DSA key will be turned off. Hence, you might need to invalidate a spooky warning from your SSH client, and recheck the fingerprint of your server;",
- "migration_0008_warning": "If you understand those warnings and agree to let YunoHost override your current configuration, run the migration. Otherwise, you can also skip the migration - though it is not recommended.",
- "migration_0008_no_warning": "No major risk indentified concerning overriding your SSH configuration—one can however not be absolutely sure ;)! Run the migration to override it. Otherwise, you can also skip the migration - though it is not recommended.",
+ "migration_0008_warning": "If you understand those warnings and want YunoHost to override your current configuration, run the migration. Otherwise, you can also skip the migration, though it is not recommended.",
+ "migration_0008_no_warning": "Overriding your SSH configuration should be safe, though this cannot be promised! Run the migration to override it. Otherwise, you can also skip the migration, though it is not recommended.",
"migration_0009_not_needed": "This migration already happened somehow… (?) Skipping.",
"migration_0011_backup_before_migration": "Creating a backup of LDAP database and apps settings prior to the actual migration.",
- "migration_0011_can_not_backup_before_migration": "Could not back up the system prior to migration. Error: {error:s}",
+ "migration_0011_can_not_backup_before_migration": "The backup of the system could not be completed before the migration failed. Error: {error:s}",
"migration_0011_create_group": "Creating a group for each user…",
- "migration_0011_done": "Migration successful. You are now able to manage usergroups.",
- "migration_0011_LDAP_config_dirty": "It look like that you customized your LDAP configuration. For this migration the LDAP configuration needs to be updated.\nYou need to save your actual configuration, reintialize the original configuration by running 'yunohost tools regen-conf -f' and retry the migration",
+ "migration_0011_done": "Migration completed. You are now able to manage usergroups.",
+ "migration_0011_slapd_config_will_be_overwritten": "It looks like you manually edited the slapd configuration. For this critical migration, YunoHost needs to force the update of the slapd configuration. The original files will be backuped in {conf_backup_folder}.",
"migration_0011_LDAP_update_failed": "Could not update LDAP. Error: {error:s}",
"migration_0011_migrate_permission": "Migrating permissions from apps settings to LDAP…",
- "migration_0011_migration_failed_trying_to_rollback": "Migration failed… trying to roll back the system.",
+ "migration_0011_migration_failed_trying_to_rollback": "Could not migrate… trying to roll back the system.",
"migration_0011_rollback_success": "System rolled back.",
"migration_0011_update_LDAP_database": "Updating LDAP database…",
"migration_0011_update_LDAP_schema": "Updating LDAP schema…",
+ "migration_0011_failed_to_remove_stale_object": "Could not remove stale object {dn}: {error}",
"migrations_already_ran": "Those migrations are already done: {ids}",
- "migrations_cant_reach_migration_file": "Could not access migrations files at path %s",
- "migrations_dependencies_not_satisfied": "Cannot run migration {id} because first you need to run these migrations: {dependencies_id}",
+ "migrations_cant_reach_migration_file": "Could not access migrations files at the path '%s'",
+ "migrations_dependencies_not_satisfied": "Run these migrations: '{dependencies_id}', before migration {id}.",
"migrations_failed_to_load_migration": "Could not load migration {id}: {error}",
"migrations_exclusive_options": "'--auto', '--skip', and '--force-rerun' are mutually exclusive options.",
"migrations_list_conflict_pending_done": "You cannot use both '--previous' and '--done' at the same time.",
@@ -365,73 +468,51 @@
"migrations_must_provide_explicit_targets": "You must provide explicit targets when using '--skip' or '--force-rerun'",
"migrations_need_to_accept_disclaimer": "To run the migration {id}, your must accept the following disclaimer:\n---\n{disclaimer}\n---\nIf you accept to run the migration, please re-run the command with the option '--accept-disclaimer'.",
"migrations_no_migrations_to_run": "No migrations to run",
- "migrations_no_such_migration": "There is no migration called {id}",
+ "migrations_no_such_migration": "There is no migration called '{id}'",
"migrations_not_pending_cant_skip": "Those migrations are not pending, so cannot be skipped: {ids}",
"migrations_pending_cant_rerun": "Those migrations are still pending, so cannot be run again: {ids}",
"migrations_running_forward": "Running migration {id}…",
"migrations_skip_migration": "Skipping migration {id}…",
"migrations_success_forward": "Migration {id} completed",
"migrations_to_be_ran_manually": "Migration {id} has to be run manually. Please go to Tools → Migrations on the webadmin page, or run `yunohost tools migrations migrate`.",
- "monitor_disabled": "Server monitoring now turned off",
- "monitor_enabled": "Server monitoring now turned on",
- "monitor_glances_con_failed": "Could not connect to Glances server",
- "monitor_not_enabled": "Server monitoring is off",
- "monitor_period_invalid": "Invalid time period",
- "monitor_stats_file_not_found": "Statistics file not found",
- "monitor_stats_no_update": "No monitoring statistics to update",
- "monitor_stats_period_unavailable": "No available statistics for the period",
- "mountpoint_unknown": "Unknown mountpoint",
- "mysql_db_creation_failed": "MySQL database creation failed",
- "mysql_db_init_failed": "MySQL database init failed",
- "mysql_db_initialized": "The MySQL database now initialized",
- "need_define_permission_before": "Redefine the permission using 'yunohost user permission add -u USER' before removing an allowed group",
- "network_check_mx_ko": "DNS MX record is not set",
- "network_check_smtp_ko": "Outbound e-mail (SMTP port 25) seems to be blocked by your network",
- "network_check_smtp_ok": "Outbound e-mail (SMTP port 25) is not blocked",
- "no_internet_connection": "Server not connected to the Internet",
+ "no_internet_connection": "The server is not connected to the Internet",
"not_enough_disk_space": "Not enough free space on '{path:s}'",
"operation_interrupted": "The operation was manually interrupted?",
- "package_not_installed": "Package '{pkgname}' is not installed",
- "package_unexpected_error": "An unexpected error occurred processing the package '{pkgname}'",
"package_unknown": "Unknown package '{pkgname}'",
"packages_upgrade_failed": "Could not upgrade all the packages",
- "password_listed": "This password is among the most used password in the world. Please choose something more unique.",
+ "password_listed": "This password is among the most used passwords in the world. Please choose something more unique.",
"password_too_simple_1": "The password needs to be at least 8 characters long",
"password_too_simple_2": "The password needs to be at least 8 characters long and contain a digit, upper and lower characters",
"password_too_simple_3": "The password needs to be at least 8 characters long and contain a digit, upper, lower and special characters",
"password_too_simple_4": "The password needs to be at least 12 characters long and contain a digit, upper, lower and special characters",
"pattern_backup_archive_name": "Must be a valid filename with max 30 characters, alphanumeric and -_. characters only",
"pattern_domain": "Must be a valid domain name (e.g. my-domain.org)",
- "pattern_email": "Must be a valid email address (e.g. someone@domain.org)",
+ "pattern_email": "Must be a valid e-mail address (e.g. someone@example.com)",
"pattern_firstname": "Must be a valid first name",
"pattern_lastname": "Must be a valid last name",
- "pattern_listname": "Must be alphanumeric and underscore characters only",
"pattern_mailbox_quota": "Must be a size with b/k/M/G/T suffix or 0 to not have a quota",
"pattern_password": "Must be at least 3 characters long",
- "pattern_port": "Must be a valid port number (i.e. 0-65535)",
"pattern_port_or_range": "Must be a valid port number (i.e. 0-65535) or range of ports (e.g. 100:200)",
"pattern_positive_number": "Must be a positive number",
"pattern_username": "Must be lower-case alphanumeric and underscore characters only",
"pattern_password_app": "Sorry, passwords can not contain the following characters: {forbidden_chars}",
- "permission_already_clear": "Permission '{permission:s}' already clear for app {app:s}",
- "permission_already_exist": "Permission '{permission:s}' for app {app:s} already exist",
- "permission_created": "Permission '{permission:s}' for app {app:s} created",
- "permission_creation_failed": "Could not grant permission",
- "permission_deleted": "Permission '{permission:s}' for app {app:s} deleted",
- "permission_deletion_failed": "Missing permission '{permission:s}' to delete the app '{app:s}'",
- "permission_not_found": "Permission '{permission:s}' not found for the application '{app:s}'",
- "permission_name_not_valid": "Pick an allowed permission name for '{permission:s}'",
- "permission_update_failed": "Could not update permission",
- "permission_generated": "Permission database updated",
- "permission_updated": "Permission '{permission:s}' for the app '{app:s}' updated",
+ "permission_already_allowed": "Group '{group}' already has permission '{permission}' enabled",
+ "permission_already_disallowed": "Group '{group}' already has permission '{permission}' disabled",
+ "permission_already_exist": "Permission '{permission}' already exists",
+ "permission_already_up_to_date": "The permission was not updated because the addition/removal requests already match the current state.",
+ "permission_cannot_remove_main": "Removing a main permission is not allowed",
+ "permission_created": "Permission '{permission:s}' created",
+ "permission_creation_failed": "Could not create permission '{permission}': {error}",
+ "permission_currently_allowed_for_all_users": "This permission is currently granted to all users in addition to other groups. You probably want to either remove the 'all_users' permission or remove the other groups it is currently granted to.",
+ "permission_deleted": "Permission '{permission:s}' deleted",
+ "permission_deletion_failed": "Could not delete permission '{permission}': {error}",
+ "permission_not_found": "Permission '{permission:s}' not found",
+ "permission_update_failed": "Could not update permission '{permission}': {error}",
+ "permission_updated": "Permission '{permission:s}' updated",
"permission_update_nothing_to_do": "No permissions to update",
+ "permission_require_account": "Permission {permission} only makes sense for users having an account, and therefore cannot be enabled for visitors.",
"port_already_closed": "Port {port:d} is already closed for {ip_version:s} connections",
"port_already_opened": "Port {port:d} is already opened for {ip_version:s} connections",
- "port_available": "Port {port:d} is available",
- "port_unavailable": "Port {port:d} is not available",
- "recommend_to_add_first_user": "The post-install is finished, but YunoHost needs at least one user to work correctly, you should add one using 'yunohost user create ' or do it from the admin interface.",
- "remove_main_permission_not_allowed": "Removing the main permission is not allowed",
- "remove_user_of_group_not_allowed": "You are not allowed to remove the user '{user:s}' in the group '{group:s}'",
"regenconf_file_backed_up": "Configuration file '{conf}' backed up to '{backup}'",
"regenconf_file_copy_failed": "Could not copy the new configuration file '{new}' to '{conf}'",
"regenconf_file_kept_back": "The configuration file '{conf}' is expected to be deleted by regen-conf (category {category}) but was kept back.",
@@ -442,7 +523,7 @@
"regenconf_file_updated": "Configuration file '{conf}' updated",
"regenconf_now_managed_by_yunohost": "The configuration file '{conf}' is now managed by YunoHost (category {category}).",
"regenconf_up_to_date": "The configuration is already up-to-date for category '{category}'",
- "regenconf_updated": "Configuration for category '{category}' updated",
+ "regenconf_updated": "Configuration updated for '{category}'",
"regenconf_would_be_updated": "The configuration would have been updated for category '{category}'",
"regenconf_dry_pending_applying": "Checking pending configuration which would have been applied for category '{category}'…",
"regenconf_failed": "Could not regenerate the configuration for category(s): {categories}",
@@ -454,8 +535,8 @@
"restore_confirm_yunohost_installed": "Do you really want to restore an already installed system? [{answers:s}]",
"restore_extracting": "Extracting needed files from the archive…",
"restore_failed": "Could not restore system",
- "restore_hook_unavailable": "The restoration script for '{part:s}' not available on your system and not in the archive either",
- "restore_may_be_not_enough_disk_space": "Your system seems does not have enough space (free: {free_space:d} B, needed space: {needed_space:d} B, security margin: {margin:d} B)",
+ "restore_hook_unavailable": "Restoration script for '{part:s}' not available on your system and not in the archive either",
+ "restore_may_be_not_enough_disk_space": "Your system does not seem to have enough space (free: {free_space:d} B, needed space: {needed_space:d} B, security margin: {margin:d} B)",
"restore_not_enough_disk_space": "Not enough space (space: {free_space:d} B, needed space: {needed_space:d} B, security margin: {margin:d} B)",
"restore_nothings_done": "Nothing was restored",
"restore_removing_tmp_dir_failed": "Could not remove an old temporary directory",
@@ -469,96 +550,86 @@
"server_reboot": "The server will reboot",
"server_reboot_confirm": "The server will reboot immediatly, are you sure? [{answers:s}]",
"service_add_failed": "Could not add the service '{service:s}'",
- "service_added": "The service '{service:s}' added",
- "service_already_started": "The service '{service:s}' has already been started",
+ "service_added": "The service '{service:s}' was added",
+ "service_already_started": "The service '{service:s}' is running already",
"service_already_stopped": "The service '{service:s}' has already been stopped",
"service_cmd_exec_failed": "Could not execute the command '{command:s}'",
"service_description_avahi-daemon": "Allows you to reach your server using 'yunohost.local' in your local network",
"service_description_dnsmasq": "Handles domain name resolution (DNS)",
"service_description_dovecot": "Allows e-mail clients to access/fetch email (via IMAP and POP3)",
"service_description_fail2ban": "Protects against brute-force and other kinds of attacks from the Internet",
- "service_description_glances": "Monitors system info on your server",
"service_description_metronome": "Manage XMPP instant messaging accounts",
- "service_description_mysql": "Stores applications data (SQL database)",
+ "service_description_mysql": "Stores app data (SQL database)",
"service_description_nginx": "Serves or provides access to all the websites hosted on your server",
"service_description_nslcd": "Handles YunoHost user shell connection",
- "service_description_php7.0-fpm": "Runs applications written in PHP with NGINX",
+ "service_description_php7.0-fpm": "Runs apps written in PHP with NGINX",
"service_description_postfix": "Used to send and receive e-mails",
"service_description_redis-server": "A specialized database used for rapid data access, task queue, and communication between programs",
- "service_description_rmilter": "Checks various parameters in e-mails",
"service_description_rspamd": "Filters spam, and other e-mail related features",
"service_description_slapd": "Stores users, domains and related info",
"service_description_ssh": "Allows you to connect remotely to your server via a terminal (SSH protocol)",
"service_description_yunohost-api": "Manages interactions between the YunoHost web interface and the system",
- "service_description_yunohost-firewall": "Manages open and close connexion ports to services",
- "service_disable_failed": "Could not turn off the service '{service:s}'\n\nRecent service logs:{logs:s}",
- "service_disabled": "'{service:s}' service turned off",
- "service_enable_failed": "Could not turn on the service '{service:s}'\n\nRecent service logs:{logs:s}",
- "service_enabled": "'{service:s}' service turned off",
- "service_no_log": "No log to display for service '{service:s}'",
+ "service_description_yunohost-firewall": "Manages open and close connection ports to services",
+ "service_disable_failed": "Could not make the service '{service:s}' not start at boot.\n\nRecent service logs:{logs:s}",
+ "service_disabled": "The service '{service:s}' will not be started anymore when system boots.",
+ "service_enable_failed": "Could not make the service '{service:s}' automatically start at boot.\n\nRecent service logs:{logs:s}",
+ "service_enabled": "The service '{service:s}' will now be automatically started during system boots.",
"service_regen_conf_is_deprecated": "'yunohost service regen-conf' is deprecated! Please use 'yunohost tools regen-conf' instead.",
"service_remove_failed": "Could not remove the service '{service:s}'",
- "service_removed": "'{service:s}' service removed",
+ "service_removed": "Service '{service:s}' removed",
"service_reload_failed": "Could not reload the service '{service:s}'\n\nRecent service logs:{logs:s}",
- "service_reloaded": "'{service:s}' service reloaded",
+ "service_reloaded": "Service '{service:s}' reloaded",
"service_restart_failed": "Could not restart the service '{service:s}'\n\nRecent service logs:{logs:s}",
- "service_restarted": "'{service:s}' service restarted",
+ "service_restarted": "Service '{service:s}' restarted",
"service_reload_or_restart_failed": "Could not reload or restart the service '{service:s}'\n\nRecent service logs:{logs:s}",
- "service_reloaded_or_restarted": "'{service:s}' service reloaded or restarted",
+ "service_reloaded_or_restarted": "The service '{service:s}' was reloaded or restarted",
"service_start_failed": "Could not start the service '{service:s}'\n\nRecent service logs:{logs:s}",
- "service_started": "'{service:s}' service started",
+ "service_started": "Service '{service:s}' started",
"service_stop_failed": "Could not stop the service '{service:s}'\n\nRecent service logs:{logs:s}",
- "service_stopped": "'{service:s}' service stopped",
+ "service_stopped": "Service '{service:s}' stopped",
"service_unknown": "Unknown service '{service:s}'",
"ssowat_conf_generated": "SSOwat configuration generated",
"ssowat_conf_updated": "SSOwat configuration updated",
- "ssowat_persistent_conf_read_error": "Could not read persistent SSOwat configuration: {error:s}. Edit /etc/ssowat/conf.json.persistent file to fix the JSON syntax",
- "ssowat_persistent_conf_write_error": "Could not save persistent SSOwat configuration: {error:s}. Edit /etc/ssowat/conf.json.persistent file to fix the JSON syntax",
- "system_groupname_exists": "Groupname already exists in the system group",
"system_upgraded": "System upgraded",
"system_username_exists": "Username already exists in the list of system users",
"this_action_broke_dpkg": "This action broke dpkg/APT (the system package managers)… You can try to solve this issue by connecting through SSH and running `sudo dpkg --configure -a`.",
- "tools_update_failed_to_app_fetchlist": "Could not update YunoHost's applists because: {error}",
"tools_upgrade_at_least_one": "Please specify '--apps', or '--system'",
"tools_upgrade_cant_both": "Cannot upgrade both system and apps at the same time",
"tools_upgrade_cant_hold_critical_packages": "Could not hold critical packages…",
- "tools_upgrade_cant_unhold_critical_packages": "Could not to unhold critical packages…",
+ "tools_upgrade_cant_unhold_critical_packages": "Could not unhold critical packages…",
"tools_upgrade_regular_packages": "Now upgrading 'regular' (non-yunohost-related) packages…",
"tools_upgrade_regular_packages_failed": "Could not upgrade packages: {packages_list}",
"tools_upgrade_special_packages": "Now upgrading 'special' (yunohost-related) packages…",
- "tools_upgrade_special_packages_explanation": "This action will end, but the actual special upgrade will continue in background. Please don't start any other action on your server in the next ~10 minutes (depending on your hardware speed). Once it i done, you may have to log in on the webadmin page again. The upgrade log will be available in Tools → Log (on the webadmin page) or through 'yunohost log list' (from the command line).",
+ "tools_upgrade_special_packages_explanation": "The special upgrade will continue in the background. Please don't start any other actions on your server for the next ~10 minutes (depending on hardware speed). After this, you may have to re-log in to the webadmin. The upgrade log will be available in Tools → Log (in the webadmin) or using 'yunohost log list' (from the command-line).",
"tools_upgrade_special_packages_completed": "YunoHost package upgrade completed.\nPress [Enter] to get the command line back",
"unbackup_app": "App '{app:s}' will not be saved",
"unexpected_error": "Something unexpected went wrong: {error}",
- "unit_unknown": "Unknown unit '{unit:s}'",
"unlimit": "No quota",
"unrestore_app": "App '{app:s}' will not be restored",
"update_apt_cache_failed": "Could not to update the cache of APT (Debian's package manager). Here is a dump of the sources.list lines, which might help identify problematic lines: \n{sourceslist}",
"update_apt_cache_warning": "Something went wrong while updating the cache of APT (Debian's package manager). Here is a dump of the sources.list lines, which might help identify problematic lines: \n{sourceslist}",
"updating_apt_cache": "Fetching available upgrades for system packages…",
- "updating_app_lists": "Fetching available upgrades for applications…",
"upgrade_complete": "Upgrade complete",
"upgrading_packages": "Upgrading packages…",
"upnp_dev_not_found": "No UPnP device found",
"upnp_disabled": "UPnP turned off",
"upnp_enabled": "UPnP turned on",
"upnp_port_open_failed": "Could not open port via UPnP",
- "user_already_in_group": "The user '{user:}' is already in the '{group:s}' group",
+ "user_already_exists": "The user '{user}' already exists",
"user_created": "User created",
- "user_creation_failed": "Could not create user",
+ "user_creation_failed": "Could not create user {user}: {error}",
"user_deleted": "User deleted",
- "user_deletion_failed": "Could not delete user",
+ "user_deletion_failed": "Could not delete user {user}: {error}",
"user_home_creation_failed": "Could not create 'home' folder for user",
- "user_info_failed": "Could not retrieve user info",
- "user_not_in_group": "The user '{user:s}' is not in the group {group:s}",
"user_unknown": "Unknown user: {user:s}",
- "user_update_failed": "Could not change user info",
+ "user_update_failed": "Could not update user {user}: {error}",
"user_updated": "User info changed",
"users_available": "Available users:",
"yunohost_already_installed": "YunoHost is already installed",
"yunohost_ca_creation_failed": "Could not create certificate authority",
"yunohost_ca_creation_success": "Local certification authority created.",
- "yunohost_configured": "YunoHost now configured",
+ "yunohost_configured": "YunoHost is now configured",
"yunohost_installing": "Installing YunoHost…",
- "yunohost_not_installed": "YunoHost is incorrectly or not correctly installed. Please run 'yunohost tools postinstall'"
+ "yunohost_not_installed": "YunoHost is not correctly installed. Please run 'yunohost tools postinstall'",
+ "yunohost_postinstall_end_tip": "The post-install completed! To finalize your setup, please consider:\n - adding a first user through the 'Users' section of the webadmin (or 'yunohost user create ' in command-line);\n - diagnose potential issues through the 'Diagnosis' section of the webadmin (or 'yunohost diagnosis run' in command-line);\n - reading the 'Finalizing your setup' and 'Getting to know Yunohost' parts in the admin documentation: https://yunohost.org/admindoc."
}
diff --git a/locales/eo.json b/locales/eo.json
index 1a6a2ea9a..f81ea8da6 100644
--- a/locales/eo.json
+++ b/locales/eo.json
@@ -1,43 +1,595 @@
{
- "admin_password_change_failed": "Malebla ŝanĝi pasvorton",
- "admin_password_changed": "Pasvorto de la estro estas ŝanĝita",
+ "admin_password_change_failed": "Ne eblas ŝanĝi pasvorton",
+ "admin_password_changed": "La pasvorto de administrado ŝanĝiĝis",
"app_already_installed": "{app:s} estas jam instalita",
- "app_already_up_to_date": "{app:s} estas ĝisdata",
+ "app_already_up_to_date": "{app:s} estas jam ĝisdata",
"app_argument_required": "Parametro {name:s} estas bezonata",
"app_change_url_identical_domains": "Malnovaj kaj novaj domajno/URL estas la sama ('{domain:s}{path:s}'), nenio fareblas.",
- "app_change_url_success": "URL de appo {app:s} ŝanĝita al {domain:s}{path:s}",
- "app_extraction_failed": "Malebla malkompaktigi instaldosierojn",
- "app_id_invalid": "Nevalida apo id",
- "app_incompatible": "Apo {app} ne estas kongrua kun via YunoHost versio",
- "app_install_files_invalid": "Nevalidaj instaldosieroj",
- "app_location_already_used": "Apo {app} jam estas instalita al tiu loco ({path})",
- "user_updated": "Uzanto estas ĝisdatita",
+ "app_change_url_success": "{app:s} URL nun estas {domain:s} {path:s}",
+ "app_extraction_failed": "Ne povis ĉerpi la instalajn dosierojn",
+ "app_id_invalid": "Nevalida apo ID",
+ "app_install_files_invalid": "Ĉi tiuj dosieroj ne povas esti instalitaj",
+ "user_updated": "Uzantinformoj ŝanĝis",
"users_available": "Uzantoj disponeblaj :",
"yunohost_already_installed": "YunoHost estas jam instalita",
- "yunohost_ca_creation_failed": "Ne eblas krei atestan aŭtoritaton",
- "yunohost_ca_creation_success": "Loka atesta aŭtoritato estas kreita.",
+ "yunohost_ca_creation_failed": "Ne povis krei atestan aŭtoritaton",
+ "yunohost_ca_creation_success": "Loka atestila aŭtoritato kreiĝis.",
"yunohost_installing": "Instalante YunoHost…",
- "service_description_glances": "monitoras sisteminformojn de via servilo",
- "service_description_metronome": "mastrumas XMPP tujmesaĝilon kontojn",
- "service_description_mysql": "stokas aplikaĵojn datojn (SQL datumbazo)",
- "service_description_nginx": "servas aŭ permesas atingi ĉiujn retejojn gastigita sur via servilo",
- "service_description_nslcd": "mastrumas Yunohost uzantojn konektojn per komanda linio",
- "service_description_php7.0-fpm": "rulas aplikaĵojn skibita en PHP kun nginx",
- "service_description_postfix": "uzita por sendi kaj ricevi retpoŝtojn",
- "service_description_redis-server": "specialita datumbazo uzita por rapida datumo atingo, atendovicoj kaj komunikadoj inter programoj",
- "service_description_rmilter": "kontrolas diversajn parametrojn en retpoŝtoj",
- "service_description_rspamd": "filtras trudmesaĝojn, kaj aliaj funkcioj rilate al retpoŝto",
- "service_description_slapd": "stokas uzantojn, domajnojn kaj rilatajn informojn",
- "service_description_ssh": "permesas al vi konekti al via servilo kun fora terminalo (SSH protokolo)",
- "service_description_yunohost-api": "mastrumas interagojn inter la YunoHost retinterfaco kaj la sistemo",
- "service_description_yunohost-firewall": "mastrumas malfermitajn kaj fermitajn konektejojn al servoj",
- "service_disable_failed": "Neebla malaktivigi servon '{service:s}'\n\nFreŝaj protokoloj de la servo : {logs:s}",
- "service_disabled": "Servo '{service:s}' estas malaktivigita",
+ "service_description_metronome": "Mastrumas XMPP tujmesaĝilon kontojn",
+ "service_description_mysql": "Butikigas datumojn de programoj (SQL datumbazo)",
+ "service_description_nginx": "Servas aŭ permesas atingi ĉiujn retejojn gastigita sur via servilo",
+ "service_description_nslcd": "Mastrumas Yunohost uzantojn konektojn per komanda linio",
+ "service_description_php7.0-fpm": "Ekzekutas programojn skribitajn en PHP kun NGINX",
+ "service_description_postfix": "Uzita por sendi kaj ricevi retpoŝtojn",
+ "service_description_redis-server": "Specialita datumbazo uzita por rapida datumo atingo, atendovicoj kaj komunikadoj inter programoj",
+ "service_description_rspamd": "Filtras trudmesaĝojn, kaj aliaj funkcioj rilate al retpoŝto",
+ "service_description_slapd": "Stokas uzantojn, domajnojn kaj rilatajn informojn",
+ "service_description_ssh": "Permesas al vi konekti al via servilo kun fora terminalo (SSH protokolo)",
+ "service_description_yunohost-api": "Mastrumas interagojn inter la YunoHost retinterfaco kaj la sistemo",
+ "service_description_yunohost-firewall": "Administras malfermajn kaj fermajn konektajn havenojn al servoj",
+ "service_disable_failed": "Ne povis fari la servon '{service:s}' ne komenci ĉe la ekkuro.\n\nLastatempaj servaj protokoloj: {logs:s}",
+ "service_disabled": "La servo '{service:s}' ne plu komenciĝos kiam sistemo ekos.",
"action_invalid": "Nevalida ago « {action:s} »",
"admin_password": "Pasvorto de la estro",
"admin_password_too_long": "Bonvolu elekti pasvorton pli mallonga ol 127 signoj",
- "already_up_to_date": "Neniu estas farenda! Ĉiu jam estas ĝisdata!",
- "app_argument_choice_invalid": "Nevalida elekto por argumento « {name:s} », ĝi devas esti unu el {choices:s}",
- "app_argument_invalid": "Nevalida valoro por argumento « {name:s} » : {error:s}",
- "app_change_url_failed_nginx_reload": "Reŝargi nginx malsuksesis. Jen la eligo de « nginx -t » :\n{nginx_errors:s}"
+ "already_up_to_date": "Nenio por fari. Ĉio estas jam ĝisdatigita.",
+ "app_argument_choice_invalid": "Uzu unu el ĉi tiuj elektoj '{choices:s}' por la argumento '{name:s}'",
+ "app_argument_invalid": "Elektu validan valoron por la argumento '{name:s}': {error:s}",
+ "app_change_url_failed_nginx_reload": "Ne eblis reŝarĝi NGINX. Jen la eligo de 'nginx -t':\n{nginx_errors:s}",
+ "ask_new_admin_password": "Nova administrada pasvorto",
+ "app_action_broke_system": "Ĉi tiu ago ŝajne rompis ĉi tiujn gravajn servojn: {services}",
+ "app_unsupported_remote_type": "Malkontrolita fora speco uzita por la apliko",
+ "backup_archive_system_part_not_available": "Sistemo parto '{part:s}' ne haveblas en ĉi tiu rezervo",
+ "backup_abstract_method": "Ĉi tiu rezerva metodo ankoraŭ efektiviĝis",
+ "apps_already_up_to_date": "Ĉiuj aplikoj estas jam ĝisdatigitaj",
+ "backup_borg_not_implemented": "La kopia metodo de Borg ankoraŭ ne estas efektivigita",
+ "app_location_unavailable": "Ĉi tiu URL aŭ ne haveblas, aŭ konfliktas kun la jam instalita (j) apliko (j):\n{apps:s}",
+ "backup_archive_app_not_found": "Ne povis trovi la programon '{app:s}' en la rezerva ar archiveivo",
+ "backup_actually_backuping": "Krei rezervan ar archiveivon el la kolektitaj dosieroj …",
+ "backup_method_borg_finished": "Sekurkopio en Borg finiĝis",
+ "app_change_url_no_script": "La app '{app_name:s}' ankoraŭ ne subtenas URL-modifon. Eble vi devus altgradigi ĝin.",
+ "app_start_install": "Instali la programon '{app}' …",
+ "backup_created": "Sekurkopio kreita",
+ "app_make_default_location_already_used": "Ne povas igi la aplikon '{app}' defaŭlta sur la domajno, '{domain}' jam uziĝas de la alia app '{other_app}'",
+ "backup_method_copy_finished": "Rezerva kopio finis",
+ "app_not_properly_removed": "{app:s} ne estis ĝuste forigita",
+ "backup_archive_broken_link": "Ne povis aliri la rezervan ar archiveivon (rompita ligilo al {path:s})",
+ "app_requirements_checking": "Kontrolante postulatajn pakaĵojn por {app} …",
+ "app_not_installed": "Ne povis trovi la aplikon '{app:s}' en la listo de instalitaj programoj: {all_apps}",
+ "ask_new_path": "Nova vojo",
+ "backup_custom_mount_error": "Propra rezerva metodo ne povis preterpasi la paŝon 'monto'",
+ "app_upgrade_app_name": "Nun ĝisdatiganta {app} …",
+ "app_manifest_invalid": "Io misas pri la aplika manifesto: {error}",
+ "backup_cleaning_failed": "Ne povis purigi la provizoran rezervan dosierujon",
+ "backup_invalid_archive": "Ĉi tio ne estas rezerva ar archiveivo",
+ "backup_creation_failed": "Ne povis krei la rezervan ar archiveivon",
+ "backup_hook_unknown": "La rezerva hoko '{hook:s}' estas nekonata",
+ "backup_custom_backup_error": "Propra rezerva metodo ne povis preterpasi la paŝon \"sekurkopio\"",
+ "ask_main_domain": "Ĉefa domajno",
+ "backup_method_tar_finished": "TAR-rezerva ar archiveivo kreita",
+ "backup_cant_mount_uncompress_archive": "Ne povis munti la nekompresitan ar archiveivon kiel protektita kontraŭ skribo",
+ "app_action_cannot_be_ran_because_required_services_down": "Ĉi tiuj postulataj servoj devas funkcii por funkciigi ĉi tiun agon: {services}. Provu rekomenci ilin por daŭrigi (kaj eble esploru, kial ili malsupreniras).",
+ "backup_copying_to_organize_the_archive": "Kopiante {size:s} MB por organizi la ar archiveivon",
+ "backup_output_directory_forbidden": "Elektu malsaman elirejan dosierujon. Sekurkopioj ne povas esti kreitaj en sub-dosierujoj / bin, / boot, / dev, / ktp, / lib, / root, / run, / sbin, / sys, / usr, / var aŭ /home/yunohost.backup/archives",
+ "backup_no_uncompress_archive_dir": "Ne ekzistas tia nekompremita arkiva dosierujo",
+ "password_too_simple_1": "Pasvorto devas esti almenaŭ 8 signojn longa",
+ "app_upgrade_failed": "Ne povis ĝisdatigi {app:s}: {error}",
+ "app_upgrade_several_apps": "La sekvaj apliko estos altgradigitaj: {apps}",
+ "backup_archive_open_failed": "Ne povis malfermi la rezervan ar archiveivon",
+ "ask_lastname": "Familia nomo",
+ "app_start_backup": "Kolekti dosierojn por esti subtenata por la '{app}' …",
+ "backup_archive_name_exists": "Rezerva arkivo kun ĉi tiu nomo jam ekzistas.",
+ "backup_applying_method_tar": "Krei la rezervan TAR-ar archiveivon …",
+ "backup_method_custom_finished": "Propra rezerva metodo '{method:s}' finiĝis",
+ "app_already_installed_cant_change_url": "Ĉi tiu app estas jam instalita. La URL ne povas esti ŝanĝita nur per ĉi tiu funkcio. Rigardu \"app changeurl\" se ĝi haveblas.",
+ "app_not_correctly_installed": "{app:s} ŝajnas esti malĝuste instalita",
+ "app_removed": "{app:s} forigita",
+ "backup_delete_error": "Ne povis forigi '{path:s}'",
+ "backup_nothings_done": "Nenio por ŝpari",
+ "backup_applying_method_custom": "Nomante la kutiman rezervan metodon '{method:s}' …",
+ "backup_app_failed": "Ne eblis rezervi la programon '{app:s}'",
+ "app_upgrade_some_app_failed": "Iuj aplikoj ne povis esti altgradigitaj",
+ "app_start_remove": "Forigo de la apliko '{app}' …",
+ "backup_output_directory_not_empty": "Vi devas elekti malplenan eligitan dosierujon",
+ "backup_archive_writing_error": "Ne povis aldoni la dosierojn '{source:s}' (nomitaj en la ar theivo '{dest:s}') por esti rezervitaj en la kunpremita arkivo '{archive:s}'",
+ "ask_email": "Retpoŝta adreso",
+ "app_start_restore": "Restarigi la programon '{app}' …",
+ "backup_applying_method_copy": "Kopiante ĉiujn dosierojn al sekurkopio …",
+ "backup_couldnt_bind": "Ne povis ligi {src:s} al {dest:s}.",
+ "ask_password": "Pasvorto",
+ "app_requirements_unmeet": "Postuloj ne estas renkontitaj por {app}, la pakaĵo {pkgname} ({version}) devas esti {spec}",
+ "ask_firstname": "Antaŭnomo",
+ "backup_ask_for_copying_if_needed": "Ĉu vi volas realigi la sekurkopion uzante {size:s} MB provizore? (Ĉi tiu maniero estas uzata ĉar iuj dosieroj ne povus esti pretigitaj per pli efika metodo.)",
+ "backup_mount_archive_for_restore": "Preparante arkivon por restarigo …",
+ "backup_csv_creation_failed": "Ne povis krei la CSV-dosieron bezonatan por restarigo",
+ "backup_archive_name_unknown": "Nekonata loka rezerva ar archiveivo nomata '{name:s}'",
+ "backup_applying_method_borg": "Sendado de ĉiuj dosieroj al sekurkopio en borg-rezerva deponejo …",
+ "app_sources_fetch_failed": "Ne povis akiri fontajn dosierojn, ĉu la URL estas ĝusta?",
+ "ask_new_domain": "Nova domajno",
+ "app_unknown": "Nekonata apliko",
+ "app_not_upgraded": "La aplikaĵo '{failed_app}' ne ĝisdatigis, kaj pro tio la sekvaj ĝisdatigoj de aplikoj estis nuligitaj: {apps}",
+ "aborting": "Aborti.",
+ "app_upgraded": "{app:s} altgradigita",
+ "backup_deleted": "Rezerva forigita",
+ "backup_csv_addition_failed": "Ne povis aldoni dosierojn al sekurkopio en la CSV-dosiero",
+ "dpkg_lock_not_available": "Ĉi tiu komando ne povas funkcii nun ĉar alia programo uzas la seruron de dpkg (la administrilo de paka sistemo)",
+ "migration_0003_yunohost_upgrade": "Komenci la ĝisdatigon de YunoHost-pako ... La migrado finiĝos, sed la efektiva ĝisdatigo okazos tuj poste. Post kiam la operacio finiĝos, vi eble devos ensaluti denove sur la retpaĝo.",
+ "domain_dyndns_root_unknown": "Nekonata radika domajno DynDNS",
+ "field_invalid": "Nevalida kampo '{:s}'",
+ "log_app_makedefault": "Faru '{}' la defaŭlta apliko",
+ "migration_0003_still_on_jessie_after_main_upgrade": "Io okazis malbone dum la ĉefa ĝisdatigo: Ĉu la sistemo ankoraŭ estas en Jessie‽ Por esplori la aferon, bonvolu rigardi {log}:s …",
+ "migration_0011_can_not_backup_before_migration": "La sekurkopio de la sistemo ne povis finiĝi antaŭ ol la migrado malsukcesis. Eraro: {error:s}",
+ "migration_0011_create_group": "Krei grupon por ĉiu uzanto…",
+ "backup_system_part_failed": "Ne eblis sekurkopi la sistemon de '{part:s}'",
+ "global_settings_setting_security_postfix_compatibility": "Kongruo vs sekureca kompromiso por la Postfix-servilo. Afektas la ĉifradojn (kaj aliajn aspektojn pri sekureco)",
+ "group_unknown": "La grupo '{group:s}' estas nekonata",
+ "mailbox_disabled": "Retpoŝto malŝaltita por uzanto {user:s}",
+ "migration_description_0011_setup_group_permission": "Agordu uzantogrupon kaj starigu permeson por programoj kaj servoj",
+ "migration_0011_backup_before_migration": "Krei sekurkopion de LDAP-datumbazo kaj agordojn antaŭ la efektiva migrado.",
+ "migration_0011_migrate_permission": "Migrado de permesoj de agordoj al aplikoj al LDAP…",
+ "migration_0011_migration_failed_trying_to_rollback": "Ne povis migri ... provante redakti la sistemon.",
+ "migrations_dependencies_not_satisfied": "Rulu ĉi tiujn migradojn: '{dependencies_id}', antaŭ migrado {id}.",
+ "migrations_failed_to_load_migration": "Ne povis ŝarĝi migradon {id}: {error}",
+ "migrations_exclusive_options": "'--auto', '--skip' kaj '--force-rerun' estas reciproke ekskluzivaj ebloj.",
+ "migrations_must_provide_explicit_targets": "Vi devas provizi eksplicitajn celojn kiam vi uzas '--skip' aŭ '--force-rerun'",
+ "permission_update_failed": "Ne povis ĝisdatigi permeson '{permission}': {error}",
+ "permission_updated": "Ĝisdatigita \"{permission:s}\" rajtigita",
+ "permission_update_nothing_to_do": "Neniuj permesoj ĝisdatigi",
+ "tools_upgrade_cant_hold_critical_packages": "Ne povis teni kritikajn pakojn…",
+ "upnp_dev_not_found": "Neniu UPnP-aparato trovita",
+ "migration_description_0012_postgresql_password_to_md5_authentication": "Devigu PostgreSQL-aŭtentigon uzi MD5 por lokaj ligoj",
+ "migration_0011_done": "Migrado finiĝis. Vi nun kapablas administri uzantajn grupojn.",
+ "migration_0011_LDAP_update_failed": "Ne povis ĝisdatigi LDAP. Eraro: {error:s}",
+ "pattern_password": "Devas esti almenaŭ 3 signoj longaj",
+ "root_password_desynchronized": "La pasvorta administranto estis ŝanĝita, sed YunoHost ne povis propagandi ĉi tion al la radika pasvorto!",
+ "service_remove_failed": "Ne povis forigi la servon '{service:s}'",
+ "migration_0003_fail2ban_upgrade": "Komenci la ĝisdatigon Fail2Ban…",
+ "backup_permission": "Rezerva permeso por app {app:s}",
+ "log_user_group_delete": "Forigi grupon '{}'",
+ "log_user_group_update": "Ĝisdatigi grupon '{}'",
+ "migration_0005_postgresql_94_not_installed": "PostgreSQL ne estis instalita en via sistemo. Nenio por fari.",
+ "dyndns_provider_unreachable": "Ne povas atingi Dyndns-provizanton {provider}: ĉu via YunoHost ne estas ĝuste konektita al la interreto aŭ la dynette-servilo malŝaltiĝas.",
+ "good_practices_about_user_password": "Vi nun estas por difini novan uzantan pasvorton. La pasvorto devas esti almenaŭ 8 signoj - kvankam estas bone praktiki uzi pli longan pasvorton (t.e. pasfrazon) kaj / aŭ variaĵon de signoj (majuskloj, minuskloj, ciferoj kaj specialaj signoj).",
+ "group_updated": "Ĝisdatigita \"{group}\" grupo",
+ "group_already_exist": "Grupo {group} jam ekzistas",
+ "group_already_exist_on_system": "Grupo {group} jam ekzistas en la sistemaj grupoj",
+ "group_cannot_be_deleted": "La grupo {group} ne povas esti forigita permane.",
+ "group_update_failed": "Ne povis ĝisdatigi la grupon '{group}': {error}",
+ "group_user_already_in_group": "Uzanto {user} jam estas en grupo {group}",
+ "group_user_not_in_group": "Uzanto {user} ne estas en grupo {group}",
+ "installation_complete": "Kompleta instalado",
+ "log_category_404": "La loga kategorio '{category}' ne ekzistas",
+ "log_permission_create": "Krei permeson '{}'",
+ "log_permission_delete": "Forigi permeson '{}'",
+ "log_user_group_create": "Krei grupon '{}'",
+ "log_user_permission_update": "Mise à jour des accès pour la permission '{}'",
+ "log_user_permission_reset": "Restarigi permeson '{}'",
+ "mail_forward_remove_failed": "Ne povis forigi retpoŝton plusendante '{mail:s}'",
+ "migration_0011_rollback_success": "Sistemo ruliĝis reen.",
+ "migration_0011_update_LDAP_database": "Ĝisdatigante LDAP-datumbazon…",
+ "migration_0011_update_LDAP_schema": "Ĝisdatigante LDAP-skemon…",
+ "migration_0011_failed_to_remove_stale_object": "Ne povis forigi neuzatan objekton {dn}: {error}",
+ "migrations_already_ran": "Tiuj migradoj estas jam faritaj: {ids}",
+ "migrations_no_such_migration": "Estas neniu migrado nomata '{id}'",
+ "permission_already_allowed": "Grupo '{group}' jam havas rajtigitan permeson '{permission}'",
+ "permission_already_disallowed": "Grupo '{group}' jam havas permeson '{permission}' malebligita'",
+ "permission_cannot_remove_main": "Forigo de ĉefa permeso ne rajtas",
+ "permission_creation_failed": "Ne povis krei permeson '{permission}': {error}",
+ "user_already_exists": "La uzanto '{user}' jam ekzistas",
+ "migrations_pending_cant_rerun": "Tiuj migradoj ankoraŭ estas pritraktataj, do ne plu rajtas esti ekzekutitaj: {ids}",
+ "migrations_running_forward": "Kuranta migrado {id}…",
+ "migrations_success_forward": "Migrado {id} kompletigita",
+ "operation_interrupted": "La operacio estis permane interrompita?",
+ "permission_created": "Permesita '{permission:s}' kreita",
+ "permission_deleted": "Permesita \"{permission:s}\" forigita",
+ "permission_deletion_failed": "Ne povis forigi permeson '{permission}': {error}",
+ "permission_not_found": "Permesita \"{permission:s}\" ne trovita",
+ "restore_not_enough_disk_space": "Ne sufiĉa spaco (spaco: {free_space:d} B, necesa spaco: {needed_space:d} B, sekureca marĝeno: {margin:d} B)",
+ "tools_upgrade_regular_packages": "Nun ĝisdatigi 'regulajn' (ne-yunohost-rilatajn) pakojn …",
+ "tools_upgrade_special_packages_explanation": "La speciala ĝisdatigo daŭros en fono. Bonvolu ne komenci aliajn agojn en via servilo la sekvajn ~ 10 minutojn (depende de la aparata rapideco). Post tio, vi eble devos re-ensaluti sur la retadreso. La ĝisdatiga registro estos havebla en Iloj → Ensaluto (en la retadreso) aŭ uzante 'yunohost-logliston' (el la komandlinio).",
+ "unrestore_app": "App '{app:s}' ne restarigos",
+ "group_created": "Grupo '{group}' kreita",
+ "group_creation_failed": "Ne povis krei la grupon '{group}': {error}",
+ "group_deleted": "Grupo '{group}' forigita",
+ "group_deletion_failed": "Ne povis forigi la grupon '{group}': {error}",
+ "migrations_not_pending_cant_skip": "Tiuj migradoj ankoraŭ ne estas pritraktataj, do ne eblas preterlasi: {ids}",
+ "permission_already_exist": "Permesita '{permission}' jam ekzistas",
+ "domain_created": "Domajno kreita",
+ "migrate_tsig_wait_2": "2 minutoj …",
+ "log_user_create": "Aldonu uzanton '{}'",
+ "ip6tables_unavailable": "Vi ne povas ludi kun ip6tabloj ĉi tie. Vi estas en ujo aŭ via kerno ne subtenas ĝin",
+ "mail_unavailable": "Ĉi tiu retpoŝta adreso estas rezervita kaj aŭtomate estos atribuita al la unua uzanto",
+ "certmanager_domain_dns_ip_differs_from_public_ip": "La DNS 'A' rekordo por la domajno '{domain:s}' diferencas de ĉi tiu IP-servilo. Se vi lastatempe modifis vian A-registron, bonvolu atendi ĝin propagandi (iuj DNS-disvastigaj kontroliloj estas disponeblaj interrete). (Se vi scias, kion vi faras, uzu '--no-checks' por malŝalti tiujn ĉekojn.)",
+ "tools_upgrade_special_packages_completed": "Plenumis la ĝisdatigon de pakaĵoj de YunoHost.\nPremu [Enter] por retrovi la komandlinion",
+ "log_remove_on_failed_install": "Forigu '{}' post malsukcesa instalado",
+ "regenconf_file_manually_modified": "La agorddosiero '{conf}' estis modifita permane kaj ne estos ĝisdatigita",
+ "regenconf_would_be_updated": "La agordo estus aktualigita por la kategorio '{category}'",
+ "certmanager_cert_install_success_selfsigned": "Mem-subskribita atestilo nun instalita por la domajno '{domain:s}'",
+ "global_settings_unknown_setting_from_settings_file": "Nekonata ŝlosilo en agordoj: '{setting_key:s}', forĵetu ĝin kaj konservu ĝin en /etc/yunohost/settings-unknown.json",
+ "regenconf_file_backed_up": "Agordodosiero '{conf}' estis rezervita al '{backup}'",
+ "migration_0007_cannot_restart": "SSH ne rekomencas post provi nuligi la migradan numeron 6.",
+ "migration_description_0006_sync_admin_and_root_passwords": "Sinkronigu admin kaj radikajn pasvortojn",
+ "iptables_unavailable": "Vi ne povas ludi kun iptables ĉi tie. Vi estas en ujo aŭ via kerno ne subtenas ĝin",
+ "global_settings_cant_write_settings": "Ne eblis konservi agordojn, tial: {reason:s}",
+ "service_added": "La servo '{service:s}' aldonis",
+ "upnp_disabled": "UPnP malŝaltis",
+ "service_started": "Servo '{service:s}' komenciĝis",
+ "port_already_opened": "Haveno {port:d} estas jam malfermita por {ip_version:s} rilatoj",
+ "installation_failed": "Io okazis malbone kun la instalado",
+ "migrate_tsig_wait_3": "1 minuto …",
+ "certmanager_conflicting_nginx_file": "Ne povis prepari domajnon por ACME-defio: la agordo de NGINX {filepath:s} konfliktas kaj unue devas esti forigita",
+ "upgrading_packages": "Ĝisdatigi pakojn…",
+ "custom_app_url_required": "Vi devas provizi URL por altgradigi vian kutimon app {app:s}",
+ "service_reload_failed": "Ne povis reŝargi la servon '{service:s}'\n\nLastatempaj servaj protokoloj: {logs:s}",
+ "packages_upgrade_failed": "Ne povis ĝisdatigi ĉiujn pakojn",
+ "hook_json_return_error": "Ne povis legi revenon de hoko {path:s}. Eraro: {msg:s}. Kruda enhavo: {raw_content}",
+ "dyndns_cron_removed": "DynDNS cron-laboro forigita",
+ "dyndns_key_not_found": "DNS-ŝlosilo ne trovita por la domajno",
+ "tools_upgrade_regular_packages_failed": "Ne povis ĝisdatigi pakojn: {packages_list}",
+ "service_start_failed": "Ne povis komenci la servon '{service:s}'\n\nLastatempaj servaj protokoloj: {logs:s}",
+ "service_reloaded": "Servo '{service:s}' reŝargita",
+ "system_upgraded": "Sistemo ĝisdatigita",
+ "domain_deleted": "Domajno forigita",
+ "certmanager_acme_not_configured_for_domain": "Atestilo por la domajno '{domain:s}' ne ŝajnas esti ĝuste instalita. Bonvolu ekzekuti 'cert-instali' por ĉi tiu regado unue.",
+ "migration_description_0009_decouple_regenconf_from_services": "Malkonstruu la regen-konf-mekanismon de servoj",
+ "user_update_failed": "Ne povis ĝisdatigi uzanton {user}: {error}",
+ "migration_description_0008_ssh_conf_managed_by_yunohost_step2": "Lasu la SSH-agordon estu administrata de YunoHost (paŝo 2, manlibro)",
+ "restore_confirm_yunohost_installed": "Ĉu vi vere volas restarigi jam instalitan sistemon? [{answers:s}]",
+ "pattern_positive_number": "Devas esti pozitiva nombro",
+ "certmanager_error_no_A_record": "Neniu DNS 'A' rekordo trovita por '{domain:s}'. Vi bezonas atentigi vian domajnan nomon al via maŝino por povi instali atestilon Lasu-Ĉifri. (Se vi scias, kion vi faras, uzu '--no-checks' por malŝalti tiujn ĉekojn.)",
+ "update_apt_cache_failed": "Ne eblis ĝisdatigi la kaŝmemoron de APT (paka administranto de Debian). Jen rubujo de la sources.list-linioj, kiuj povus helpi identigi problemajn liniojn:\n{sourceslist}",
+ "migrations_no_migrations_to_run": "Neniuj migradoj por funkcii",
+ "executing_command": "Plenumanta komandon '{command:s}' …",
+ "certmanager_attempt_to_renew_nonLE_cert": "La atestilo por la domajno '{domain:s}' ne estas elsendita de Let's Encrypt. Ne eblas renovigi ĝin aŭtomate!",
+ "global_settings_setting_example_bool": "Ekzemplo bulea elekto",
+ "domain_dyndns_already_subscribed": "Vi jam abonis DynDNS-domajnon",
+ "log_letsencrypt_cert_renew": "Renovigu '{}' Let's Encrypt atestilon",
+ "migrate_tsig_start": "Detektita ŝlosila algoritmo nesufiĉa por TSIG-subskribo de la domajno '{domain}', komencanta migradon al la pli sekura HMAC-SHA-512",
+ "ldap_init_failed_to_create_admin": "LDAP-iniciato ne povis krei administran uzanton",
+ "backup_output_directory_required": "Vi devas provizi elirejan dosierujon por la sekurkopio",
+ "tools_upgrade_cant_unhold_critical_packages": "Ne povis malŝalti kritikajn pakojn…",
+ "log_link_to_log": "Plena ŝtipo de ĉi tiu operacio: ' {desc} '",
+ "global_settings_cant_serialize_settings": "Ne eblis serialigi datumojn pri agordoj, motivo: {reason:s}",
+ "backup_running_hooks": "Kurado de apogaj hokoj …",
+ "certmanager_domain_unknown": "Nekonata domajno '{domain:s}'",
+ "unexpected_error": "Io neatendita iris malbone: {error}",
+ "password_listed": "Ĉi tiu pasvorto estas inter la plej uzataj pasvortoj en la mondo. Bonvolu elekti ion pli unikan.",
+ "migration_description_0007_ssh_conf_managed_by_yunohost_step1": "Lasu la SSH-agordon estu administrata de YunoHost (paŝo 1, aŭtomata)",
+ "migration_0009_not_needed": "Ĉi tiu migrado jam iel okazis ... (?) Saltado.",
+ "ssowat_conf_generated": "SSOwat-agordo generita",
+ "migrate_tsig_wait": "Atendante tri minutojn por ke la servilo DynDNS enkalkulu la novan ŝlosilon …",
+ "log_remove_on_failed_restore": "Forigu '{}' post malsukcesa restarigo de rezerva ar archiveivo",
+ "dpkg_is_broken": "Vi ne povas fari ĉi tion nun ĉar dpkg/APT (la administrantoj pri pakaĵaj sistemoj) ŝajnas esti rompita stato ... Vi povas provi solvi ĉi tiun problemon per konekto per SSH kaj funkcianta `sudo dpkg --configure -a`.",
+ "certmanager_cert_signing_failed": "Ne povis subskribi la novan atestilon",
+ "migration_description_0003_migrate_to_stretch": "Altgradigu la sistemon al Debian Stretch kaj YunoHost 3.0",
+ "log_tools_upgrade": "Ĝisdatigu sistemajn pakaĵojn",
+ "log_available_on_yunopaste": "Ĉi tiu protokolo nun haveblas per {url}",
+ "certmanager_http_check_timeout": "Ekdifinita kiam servilo provis kontakti sin per HTTP per publika IP-adreso (domajno '{domain:s}' kun IP '{ip:s}'). Vi eble spertas haŭtoproblemon, aŭ la fajroŝirmilo / enkursigilo antaŭ via servilo miskonfiguras.",
+ "pattern_port_or_range": "Devas esti valida haveno-nombro (t.e. 0-65535) aŭ gamo da havenoj (t.e. 100:200)",
+ "migrations_loading_migration": "Ŝarĝante migradon {id}…",
+ "pattern_mailbox_quota": "Devas esti grandeco kun la sufikso b/k/M/G/T aŭ 0 por ne havi kvoton",
+ "migration_0008_general_disclaimer": "Por plibonigi la sekurecon de via servilo, rekomendas lasi YunoHost administri la SSH-agordon. Via nuna SSH-aranĝo diferencas de la rekomendo. Se vi lasas YunoHost agordi ĝin, la maniero per kiu vi konektas al via servilo per SSH ŝanĝiĝos tiel:",
+ "user_deletion_failed": "Ne povis forigi uzanton {user}: {error}",
+ "backup_with_no_backup_script_for_app": "La app '{app:s}' ne havas sekretan skripton. Ignorante.",
+ "service_regen_conf_is_deprecated": "'yunohost service regen-conf' malakceptas! Bonvolu uzi anstataŭe 'yunohost tools regen-conf'.",
+ "global_settings_key_doesnt_exists": "La ŝlosilo '{settings_key:s}' ne ekzistas en la tutmondaj agordoj, vi povas vidi ĉiujn disponeblajn klavojn per uzado de 'yunohost settings list'",
+ "dyndns_no_domain_registered": "Neniu domajno registrita ĉe DynDNS",
+ "dyndns_could_not_check_available": "Ne povis kontroli ĉu {domain:s} haveblas sur {provider:s}.",
+ "global_settings_setting_example_enum": "Ekzemplo enum elekto",
+ "hook_exec_not_terminated": "Skripto ne finiĝis ĝuste: {path:s}",
+ "service_stopped": "Servo '{service:s}' ĉesis",
+ "restore_failed": "Ne povis restarigi sistemon",
+ "confirm_app_install_danger": "Danĝero! Ĉi tiu apliko estas konata ankoraŭ eksperimenta (se ne eksplicite ne funkcias)! Vi probable ne devas instali ĝin krom se vi scias kion vi faras. NENIU SUBTENO estos provizita se ĉi tiu app ne funkcias aŭ rompas vian sistemon ... Se vi pretas riski ĉiuokaze, tajpu '{answers:s}'",
+ "log_operation_unit_unclosed_properly": "Operaciumo ne estis fermita ĝuste",
+ "upgrade_complete": "Ĝisdatigo kompleta",
+ "upnp_enabled": "UPnP ŝaltis",
+ "mailbox_used_space_dovecot_down": "La retpoŝta servo de Dovecot devas funkcii, se vi volas akcepti uzitan poŝtan spacon",
+ "restore_system_part_failed": "Ne povis restarigi la sisteman parton '{part:s}'",
+ "service_stop_failed": "Ne povis maldaŭrigi la servon '{service:s}'\n\nLastatempaj servaj protokoloj: {logs:s}",
+ "unbackup_app": "App '{app:s}' ne konserviĝos",
+ "updating_apt_cache": "Akirante haveblajn ĝisdatigojn por sistemaj pakoj…",
+ "tools_upgrade_at_least_one": "Bonvolu specifi '--apps' aŭ '--system'",
+ "service_already_stopped": "La servo '{service:s}' jam ĉesis",
+ "migration_0003_modified_files": "Bonvolu noti, ke la jenaj dosieroj estis trovitaj mane kaj modifitaj kaj povus esti anstataŭigitaj sekve de la ĝisdatigo: {manually_modified_files}",
+ "tools_upgrade_cant_both": "Ne eblas ĝisdatigi ambaŭ sistemon kaj programojn samtempe",
+ "restore_extracting": "Eltirante bezonatajn dosierojn el la ar theivo…",
+ "upnp_port_open_failed": "Ne povis malfermi havenon per UPnP",
+ "log_app_upgrade": "Ĝisdatigu la aplikon '{}'",
+ "log_help_to_get_failed_log": "La operacio '{desc}' ne povis finiĝi. Bonvolu dividi la plenan ŝtipon de ĉi tiu operacio per la komando 'yunohost log display {name} --share' por akiri helpon",
+ "migration_description_0002_migrate_to_tsig_sha256": "Plibonigu sekurecon de DynDNS TSIG-ĝisdatigoj per SHA-512 anstataŭ MD5",
+ "port_already_closed": "Haveno {port:d} estas jam fermita por {ip_version:s} rilatoj",
+ "hook_name_unknown": "Nekonata hoko-nomo '{name:s}'",
+ "migration_0003_system_not_fully_up_to_date": "Via sistemo ne estas plene ĝisdata. Bonvolu plenumi regulan ĝisdatigon antaŭ ol ruli la migradon al Stretch.",
+ "dyndns_could_not_check_provide": "Ne povis kontroli ĉu {provider:s} povas provizi {domain:s}.",
+ "dyndns_cron_remove_failed": "Ne povis forigi la cron-laboron DynDNS ĉar: {error}",
+ "restore_nothings_done": "Nenio estis restarigita",
+ "log_tools_postinstall": "Afiŝu vian servilon YunoHost",
+ "dyndns_unavailable": "La domajno '{domain:s}' ne haveblas.",
+ "experimental_feature": "Averto: Ĉi tiu funkcio estas eksperimenta kaj ne konsiderata stabila, vi ne uzu ĝin krom se vi scias kion vi faras.",
+ "root_password_replaced_by_admin_password": "Via radika pasvorto estis anstataŭigita per via administra pasvorto.",
+ "migration_description_0005_postgresql_9p4_to_9p6": "Migru datumbazojn de PostgreSQL 9.4 al 9.6",
+ "migration_0008_root": "• Vi ne povos konekti kiel radiko per SSH. Anstataŭe vi uzu la administran uzanton;",
+ "package_unknown": "Nekonata pako '{pkgname}'",
+ "domain_unknown": "Nekonata domajno",
+ "global_settings_setting_security_password_user_strength": "Uzanto pasvorta forto",
+ "restore_may_be_not_enough_disk_space": "Via sistemo ŝajnas ne havi sufiĉe da spaco (free:{free_space:d} B, necesa spaco: {needed_space:d} B, sekureca marĝeno: {margin:d} B)",
+ "log_corrupted_md_file": "La YAD-metadata dosiero asociita kun protokoloj estas damaĝita: '{md_file}\nEraro: {error} '",
+ "downloading": "Elŝutante …",
+ "user_deleted": "Uzanto forigita",
+ "service_enable_failed": "Ne povis fari la servon '{service:s}' aŭtomate komenci ĉe la ekkuro.\n\nLastatempaj servaj protokoloj: {logs:s}",
+ "tools_upgrade_special_packages": "Nun ĝisdatigi 'specialajn' (rilatajn al yunohost)…",
+ "domains_available": "Haveblaj domajnoj:",
+ "dyndns_registered": "Registrita domajno DynDNS",
+ "service_description_fail2ban": "Protektas kontraŭ bruta forto kaj aliaj specoj de atakoj de la interreto",
+ "file_does_not_exist": "La dosiero {path:s} ne ekzistas.",
+ "yunohost_not_installed": "YunoHost ne estas ĝuste instalita. Bonvolu prilabori 'yunohost tools postinstall'",
+ "migration_0005_postgresql_96_not_installed": "PostgreSQL 9.4 estas instalita, sed ne postgresql 9.6‽ Io stranga eble okazis en via sistemo: (…",
+ "restore_removing_tmp_dir_failed": "Ne povis forigi malnovan provizoran dosierujon",
+ "certmanager_cannot_read_cert": "Io malbona okazis, kiam mi provis malfermi aktualan atestilon por domajno {domain:s} (dosiero: {file:s}), kialo: {reason:s}",
+ "service_removed": "Servo '{service:s}' forigita",
+ "certmanager_hit_rate_limit": "Tro multaj atestiloj jam eldonitaj por ĉi tiu ĝusta aro de domajnoj {domain:s} antaŭ nelonge. Bonvolu reprovi poste. Vidu https://letsencrypt.org/docs/rate-limits/ por pliaj detaloj",
+ "migration_0005_not_enough_space": "Disponigu sufiĉan spacon en {path} por ruli la migradon.",
+ "pattern_firstname": "Devas esti valida antaŭnomo",
+ "migration_description_0010_migrate_to_apps_json": "Forigu malvalorigitajn katalogajn programojn kaj uzu anstataŭe la novan unuigitan liston de \"apps.json\" (malaktuale anstataŭita per migrado 13)",
+ "domain_cert_gen_failed": "Ne povis generi atestilon",
+ "regenconf_file_kept_back": "La agorda dosiero '{conf}' estas atendita forigi per regen-conf (kategorio {category}), sed ĝi estis konservita.",
+ "migrate_tsig_wait_4": "30 sekundoj …",
+ "backup_with_no_restore_script_for_app": "La apliko \"{app:s}\" ne havas restarigan skripton, vi ne povos aŭtomate restarigi la sekurkopion de ĉi tiu apliko.",
+ "log_letsencrypt_cert_install": "Instalu atestilon Let's Encrypt sur '{}' regado",
+ "log_dyndns_update": "Ĝisdatigu la IP asociita kun via subdominio YunoHost '{}'",
+ "firewall_reload_failed": "Ne eblis reŝargi la firewall",
+ "confirm_app_install_warning": "Averto: Ĉi tiu aplikaĵo povas funkcii, sed ne bone integras en YunoHost. Iuj funkcioj kiel ekzemple aliĝilo kaj sekurkopio / restarigo eble ne haveblos. Instali ĉiuokaze? [{answers:s}] ",
+ "log_user_delete": "Forigi uzanton '{}'",
+ "dyndns_ip_updated": "Ĝisdatigis vian IP sur DynDNS",
+ "regenconf_up_to_date": "La agordo jam estas ĝisdatigita por kategorio '{category}'",
+ "migration_0003_patching_sources_list": "Patching the sources.lists …",
+ "global_settings_setting_security_ssh_compatibility": "Kongruo vs sekureca kompromiso por la SSH-servilo. Afektas la ĉifradojn (kaj aliajn aspektojn pri sekureco)",
+ "migrations_need_to_accept_disclaimer": "Por funkciigi la migradon {id}, via devas akcepti la sekvan malakcepton:\n---\n{disclaimer}\n---\nSe vi akceptas funkcii la migradon, bonvolu rekonduki la komandon kun la opcio '--accept-disclaimer'.",
+ "regenconf_file_remove_failed": "Ne povis forigi la agordodosieron '{conf}'",
+ "not_enough_disk_space": "Ne sufiĉe libera spaco sur '{path:s}'",
+ "migration_0006_disclaimer": "YunoHost nun atendas, ke la pasvortoj de admin kaj radiko estos sinkronigitaj. Ĉi tiu migrado anstataŭigas vian radikan pasvorton kun la administran pasvorton.",
+ "dyndns_ip_update_failed": "Ne povis ĝisdatigi IP-adreson al DynDNS",
+ "migration_description_0004_php5_to_php7_pools": "Rekonfigu la PHP-naĝejojn por uzi PHP 7 anstataŭ 5",
+ "ssowat_conf_updated": "SSOwat-agordo ĝisdatigita",
+ "log_link_to_failed_log": "Ne povis plenumi la operacion '{desc}'. Bonvolu provizi la plenan protokolon de ĉi tiu operacio per alklakante ĉi tie por akiri helpon",
+ "user_home_creation_failed": "Ne povis krei dosierujon \"home\" por uzanto",
+ "pattern_backup_archive_name": "Devas esti valida dosiernomo kun maksimume 30 signoj, alfanombraj kaj -_. signoj nur",
+ "restore_cleaning_failed": "Ne eblis purigi la adresaron de provizora restarigo",
+ "dyndns_registration_failed": "Ne povis registri DynDNS-domajnon: {error:s}",
+ "migration_0003_not_jessie": "La nuna Debian-distribuo ne estas Jessie!",
+ "user_unknown": "Nekonata uzanto: {user:s}",
+ "migrations_to_be_ran_manually": "Migrado {id} devas funkcii permane. Bonvolu iri al Iloj → Migradoj en la retpaĝa paĝo, aŭ kuri `yunohost tools migrations migrate`.",
+ "migration_0008_warning": "Se vi komprenas tiujn avertojn kaj volas ke YunoHost preterlasu vian nunan agordon, faru la migradon. Alie, vi ankaŭ povas salti la migradon, kvankam ĝi ne rekomendas.",
+ "certmanager_cert_renew_success": "Ni Ĉifru atestilon renovigitan por la domajno '{domain:s}'",
+ "global_settings_reset_success": "Antaŭaj agordoj nun estas rezervitaj al {path:s}",
+ "pattern_domain": "Devas esti valida domajna nomo (t.e. mia-domino.org)",
+ "dyndns_key_generating": "Generi DNS-ŝlosilon ... Eble daŭros iom da tempo.",
+ "restore_running_app_script": "Restarigi la programon '{app:s}'…",
+ "migrations_skip_migration": "Salti migradon {id}…",
+ "regenconf_file_removed": "Agordodosiero '{conf}' forigita",
+ "log_tools_shutdown": "Enŝaltu vian servilon",
+ "password_too_simple_3": "La pasvorto bezonas almenaŭ 8 signojn kaj enhavas ciferon, majusklon, pli malaltan kaj specialajn signojn",
+ "migration_0003_general_warning": "Bonvolu noti, ke ĉi tiu migrado estas delikata operacio. La teamo de YunoHost faris sian plej bonan revizii kaj testi ĝin, sed la migrado eble ankoraŭ rompos partojn de la sistemo aŭ ĝiaj programoj.\n\nTial oni rekomendas al:\n - Elfari kopion de iuj kritikaj datumoj aŭ app. Pliaj informoj pri https://yunohost.org/backup;\n - Paciencu post lanĉo de la migrado: Depende de via interreta konekto kaj aparataro, eble daŭros kelkaj horoj ĝis ĉio ĝisdatigi.\n\nAldone, la haveno por SMTP, uzata de eksteraj retpoŝtaj klientoj (kiel Thunderbird aŭ K9-Mail) estis ŝanĝita de 465 (SSL / TLS) al 587 (STARTTLS). La malnova haveno (465) aŭtomate fermiĝos, kaj la nova haveno (587) malfermiĝos en la fajrejo. Vi kaj viaj uzantoj * devos adapti la agordon de viaj retpoŝtaj klientoj laŭe.",
+ "global_settings_setting_example_int": "Ekzemple int elekto",
+ "backup_output_symlink_dir_broken": "Via arkiva dosierujo '{path:s}' estas rompita ligilo. Eble vi forgesis restarigi aŭ munti aŭ enŝovi la stokadon, al kiu ĝi notas.",
+ "good_practices_about_admin_password": "Vi nun estas por difini novan administran pasvorton. La pasvorto devas esti almenaŭ 8 signoj - kvankam estas bone praktiki uzi pli longan pasvorton (t.e. pasfrazon) kaj / aŭ uzi variaĵon de signoj (majuskloj, minuskloj, ciferoj kaj specialaj signoj).",
+ "certmanager_attempt_to_renew_valid_cert": "La atestilo por la domajno '{domain:s}' ne finiĝos! (Vi eble uzos --force se vi scias kion vi faras)",
+ "restore_running_hooks": "Kurantaj restarigaj hokoj…",
+ "regenconf_pending_applying": "Aplikante pritraktata agordo por kategorio '{category}'…",
+ "service_description_dovecot": "Permesas al retpoŝtaj klientoj aliri / serĉi retpoŝton (per IMAP kaj POP3)",
+ "domain_dns_conf_is_just_a_recommendation": "Ĉi tiu komando montras al vi la *rekomenditan* agordon. Ĝi efektive ne agordas la DNS-agordon por vi. Via respondeco agordi vian DNS-zonon en via registristo laŭ ĉi tiu rekomendo.",
+ "backup_php5_to_php7_migration_may_fail": "Ne povis konverti vian ar archiveivon por subteni PHP 7, vi eble ne povas restarigi viajn PHP-programojn (kialo: {error:s})",
+ "log_backup_restore_system": "Restarigi sistemon de rezerva arkivo",
+ "log_app_change_url": "Ŝanĝu la URL de la apliko '{}'",
+ "service_already_started": "La servo '{service:s}' jam funkcias",
+ "global_settings_setting_security_password_admin_strength": "Admin pasvorta forto",
+ "service_reload_or_restart_failed": "Ne povis reŝargi aŭ rekomenci la servon '{service:s}'\n\nLastatempaj servaj protokoloj: {logs:s}",
+ "migrations_list_conflict_pending_done": "Vi ne povas uzi ambaŭ '--previous' kaj '--done' samtempe.",
+ "server_shutdown_confirm": "La servilo haltos tuj, ĉu vi certas? [{answers:s}]",
+ "log_backup_restore_app": "Restarigu '{}' de rezerva ar archiveivo",
+ "log_does_exists": "Ne estas operacio-registro kun la nomo '{log}', uzu 'yunohost loglist' por vidi ĉiujn disponeblajn operaciojn",
+ "service_add_failed": "Ne povis aldoni la servon '{service:s}'",
+ "pattern_password_app": "Bedaŭrinde, pasvortoj ne povas enhavi jenajn signojn: {forbidden_chars}",
+ "this_action_broke_dpkg": "Ĉi tiu ago rompis dpkg / APT (la administrantoj pri la paka sistemo) ... Vi povas provi solvi ĉi tiun problemon per konekto per SSH kaj funkcianta `sudo dpkg --configure -a`.",
+ "log_regen_conf": "Regeneri sistemajn agordojn '{}'",
+ "restore_hook_unavailable": "La restariga skripto por '{part:s}' ne haveblas en via sistemo kaj ankaŭ ne en la ar theivo",
+ "log_dyndns_subscribe": "Aboni al YunoHost-subdominio '{}'",
+ "password_too_simple_4": "La pasvorto bezonas almenaŭ 12 signojn kaj enhavas ciferon, majuskle, pli malaltan kaj specialajn signojn",
+ "migration_0003_main_upgrade": "Komencanta ĉefa ĝisdatigo …",
+ "regenconf_file_updated": "Agordodosiero '{conf}' ĝisdatigita",
+ "log_help_to_get_log": "Por vidi la protokolon de la operacio '{desc}', uzu la komandon 'yunohost log display {name}'",
+ "global_settings_setting_security_nginx_compatibility": "Kongruo vs sekureca kompromiso por la TTT-servilo NGINX. Afektas la ĉifradojn (kaj aliajn aspektojn pri sekureco)",
+ "no_internet_connection": "La servilo ne estas konektita al la interreto",
+ "migration_0008_dsa": "• La DSA-ŝlosilo estos malŝaltita. Tial vi eble bezonos nuligi spuran averton de via SSH-kliento kaj revizii la fingrospuron de via servilo;",
+ "migration_0003_restoring_origin_nginx_conf": "Fileia dosiero /etc/nginx/nginx.conf estis iel redaktita. La migrado reaperos unue al sia originala stato ... La antaŭa dosiero estos havebla kiel {backup_dest}.",
+ "migrate_tsig_end": "Migrado al HMAC-SHA-512 finiĝis",
+ "restore_complete": "Restarigita",
+ "certmanager_couldnt_fetch_intermediate_cert": "Ekvilibrigita kiam vi provis ricevi interajn atestilojn de Let's Encrypt. Atestita instalado / renovigo nuligita - bonvolu reprovi poste.",
+ "hook_exec_failed": "Ne povis funkcii skripto: {path:s}",
+ "global_settings_cant_open_settings": "Ne eblis malfermi agordojn, tial: {reason:s}",
+ "user_created": "Uzanto kreita",
+ "service_description_avahi-daemon": "Permesas al vi atingi vian servilon uzante 'yunohost.local' en via loka reto",
+ "certmanager_attempt_to_replace_valid_cert": "Vi provas anstataŭigi bonan kaj validan atestilon por domajno {domain:s}! (Uzu --forte pretervidi)",
+ "regenconf_updated": "Agordo ĝisdatigita por '{category}'",
+ "update_apt_cache_warning": "Io iris malbone dum la ĝisdatigo de la kaŝmemoro de APT (paka administranto de Debian). Jen rubujo de la sources.list-linioj, kiuj povus helpi identigi problemajn liniojn:\n{sourceslist}",
+ "regenconf_dry_pending_applying": "Kontrolado de pritraktata agordo, kiu estus aplikita por kategorio '{category}'…",
+ "regenconf_file_copy_failed": "Ne povis kopii la novan agordodosieron '{new}' al '{conf}'",
+ "global_settings_setting_example_string": "Ekzemple korda elekto",
+ "restore_already_installed_app": "App kun la ID '{app:s}' estas jam instalita",
+ "mail_domain_unknown": "Nevalida retadreso por domajno '{domain:s}'. Bonvolu uzi domajnon administritan de ĉi tiu servilo.",
+ "migrations_cant_reach_migration_file": "Ne povis aliri migrajn dosierojn ĉe la vojo '% s'",
+ "pattern_email": "Devas esti valida retpoŝta adreso (t.e. iu@ekzemple.com)",
+ "mail_alias_remove_failed": "Ne povis forigi retpoŝton alias '{mail:s}'",
+ "regenconf_file_manually_removed": "La dosiero de agordo '{conf}' estis forigita permane, kaj ne estos kreita",
+ "domain_exists": "La domajno jam ekzistas",
+ "migration_description_0001_change_cert_group_to_sslcert": "Ŝanĝu grupajn permesojn de 'metronomo' al 'ssl-cert'",
+ "ldap_initialized": "LDAP inicializis",
+ "migrate_tsig_not_needed": "Vi ne ŝajnas uzi DynDNS-domajnon, do neniu migrado necesas.",
+ "certmanager_domain_cert_not_selfsigned": "La atestilo por domajno {domain:s} ne estas mem-subskribita. Ĉu vi certas, ke vi volas anstataŭigi ĝin? (Uzu '--force' por fari tion.)",
+ "certmanager_unable_to_parse_self_CA_name": "Ne povis trapasi nomon de mem-subskribinta aŭtoritato (dosiero: {file:s})",
+ "log_selfsigned_cert_install": "Instalu mem-subskribitan atestilon sur '{}' domajno",
+ "log_tools_reboot": "Reklamu vian servilon",
+ "certmanager_cert_install_success": "Ni Ĉifru atestilon nun instalitan por la domajno '{domain:s}'",
+ "global_settings_bad_choice_for_enum": "Malbona elekto por agordo {setting:s}, ricevita '{choice:s}', sed disponeblaj elektoj estas: {available_choices:s}",
+ "server_shutdown": "La servilo haltos",
+ "log_tools_migrations_migrate_forward": "Migri antaŭen",
+ "migration_0008_no_warning": "Supersalti vian SSH-agordon estu sekura, kvankam ĉi tio ne povas esti promesita! Ekfunkciu la migradon por superregi ĝin. Alie, vi ankaŭ povas salti la migradon, kvankam ĝi ne rekomendas.",
+ "regenconf_now_managed_by_yunohost": "La agorda dosiero '{conf}' nun estas administrata de YunoHost (kategorio {category}).",
+ "server_reboot_confirm": "Ĉu la servilo rekomencos tuj, ĉu vi certas? [{answers:s}]",
+ "log_app_install": "Instalu la aplikon '{}'",
+ "service_description_dnsmasq": "Traktas rezolucion de domajna nomo (DNS)",
+ "global_settings_unknown_type": "Neatendita situacio, la agordo {setting:s} ŝajnas havi la tipon {unknown_type:s} sed ĝi ne estas tipo subtenata de la sistemo.",
+ "migration_0003_problematic_apps_warning": "Bonvolu noti, ke la sekvaj eventuale problemaj instalitaj programoj estis detektitaj. Ŝajnas, ke tiuj ne estis instalitaj el app_katalogo aŭ ne estas markitaj kiel \"funkciantaj\". Tial ne eblas garantii, ke ili ankoraŭ funkcios post la ĝisdatigo: {problematic_apps}",
+ "domain_hostname_failed": "Ne povis agordi novan gastigilon. Ĉi tio eble kaŭzos problemon poste (eble bone).",
+ "server_reboot": "La servilo rekomenciĝos",
+ "regenconf_failed": "Ne povis regeneri la agordon por kategorio(j): {categories}",
+ "domain_uninstall_app_first": "Unu aŭ pluraj programoj estas instalitaj en ĉi tiu domajno. Bonvolu malinstali ilin antaŭ ol daŭrigi la domajnan forigon",
+ "service_unknown": "Nekonata servo '{service:s}'",
+ "migration_0003_start": "Komencante migradon al Stretch. La protokoloj haveblos en {logfile}.",
+ "domain_deletion_failed": "Ne eblas forigi domajnon {domain}: {error}",
+ "log_user_update": "Ĝisdatigu uzantinformojn de '{}'",
+ "user_creation_failed": "Ne povis krei uzanton {user}: {error}",
+ "migrations_migration_has_failed": "Migrado {id} ne kompletigis, abolis. Eraro: {exception}",
+ "done": "Farita",
+ "log_domain_remove": "Forigi domon '{}' de agordo de sistemo",
+ "hook_list_by_invalid": "Ĉi tiu posedaĵo ne povas esti uzata por listigi hokojn",
+ "confirm_app_install_thirdparty": "Danĝero! Ĉi tiu apliko ne estas parto de la aplika katalogo de Yunohost. Instali triajn aplikojn povas kompromiti la integrecon kaj sekurecon de via sistemo. Vi probable ne devas instali ĝin krom se vi scias kion vi faras. NENIU SUBTENO estos provizita se ĉi tiu app ne funkcias aŭ rompas vian sistemon ... Se vi pretas riski ĉiuokaze, tajpu '{answers:s}'",
+ "global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "Permesu uzon de (malaktuala) DSA-hostkey por la agordo de daemon SSH",
+ "dyndns_domain_not_provided": "Provizanto DynDNS {provider:s} ne povas provizi domajnon {domain:s}.",
+ "backup_unable_to_organize_files": "Ne povis uzi la rapidan metodon por organizi dosierojn en la ar archiveivo",
+ "password_too_simple_2": "La pasvorto bezonas almenaŭ 8 signojn kaj enhavas ciferon, majusklojn kaj minusklojn",
+ "executing_script": "Plenumanta skripto '{script:s}' …",
+ "service_cmd_exec_failed": "Ne povis plenumi la komandon '{command:s}'",
+ "migration_0007_cancelled": "Ne povis plibonigi la manieron kiel via SSH-agordo estas administrita.",
+ "migrate_tsig_failed": "Ne povis migri la DynDNS-domajnon '{domain}' al HMAC-SHA-512, ruliĝante. Eraro: {error_code}, {error}",
+ "pattern_lastname": "Devas esti valida familinomo",
+ "service_enabled": "La servo '{service:s}' nun aŭtomate komenciĝos dum sistemaj botoj.",
+ "certmanager_no_cert_file": "Ne povis legi la atestan dosieron por la domajno {domain:s} (dosiero: {file:s})",
+ "migration_0008_port": "• Vi devos konekti uzante la havenon 22 anstataŭ via nuna kutimo SSH-haveno. Sentu vin libera reconfiguri ĝin;",
+ "domain_creation_failed": "Ne eblas krei domajnon {domain}: {error}",
+ "certmanager_domain_http_not_working": "Ŝajnas ke la domajno {domain:s} ne atingeblas per HTTP. Kontrolu, ke via DNS kaj NGINX-agordo ĝustas",
+ "domain_cannot_remove_main": "Vi ne povas forigi '{domain:s}' ĉar ĝi estas la ĉefa domajno, vi bezonas unue agordi alian domajnon kiel la ĉefan domajnon per uzado de 'yunohost domain main-domain -n ', jen la listo de kandidataj domajnoj. : {other_domains:s}",
+ "service_reloaded_or_restarted": "La servo '{service:s}' estis reŝarĝita aŭ rekomencita",
+ "log_domain_add": "Aldonu '{}' domajnon en sisteman agordon",
+ "global_settings_bad_type_for_setting": "Malbona tipo por agordo {setting:s}, ricevita {received_type:s}, atendata {expected_type:s}",
+ "unlimit": "Neniu kvoto",
+ "dyndns_cron_installed": "Kreita laboro DynDNS cron",
+ "system_username_exists": "Uzantnomo jam ekzistas en la listo de uzantoj de sistemo",
+ "firewall_reloaded": "Fajroŝirmilo reŝarĝis",
+ "service_restarted": "Servo '{service:s}' rekomencis",
+ "pattern_username": "Devas esti minuskulaj literoj kaj minuskloj nur",
+ "extracting": "Eltirante…",
+ "restore_app_failed": "Ne povis restarigi la programon '{app:s}'",
+ "yunohost_configured": "YunoHost nun estas agordita",
+ "certmanager_self_ca_conf_file_not_found": "Ne povis trovi agorddosieron por mem-subskriba aŭtoritato (dosiero: {file:s})",
+ "log_app_remove": "Forigu la aplikon '{}'",
+ "service_restart_failed": "Ne povis rekomenci la servon '{service:s}'\n\nLastatempaj servaj protokoloj: {logs:s}",
+ "firewall_rules_cmd_failed": "Iuj komandoj pri fajroŝirmilo malsukcesis. Pliaj informoj en ensaluto.",
+ "certmanager_certificate_fetching_or_enabling_failed": "Provante uzi la novan atestilon por {domain:s} ne funkciis …",
+ "app_full_domain_unavailable": "Bedaŭrinde, ĉi tiu app devas esti instalita sur propra domajno, sed aliaj programoj jam estas instalitaj sur la domajno '{domain}'. Vi povus uzi subdominon dediĉitan al ĉi tiu app anstataŭe.",
+ "migration_0011_slapd_config_will_be_overwritten": "Ŝajnas ke vi permane redaktis la slapd-agordon. Por ĉi tiu kritika migrado, YunoHost bezonas devigi la ĝisdatigon de la slapd-agordo. La originalaj dosieroj estos rezervitaj en {conf_backup_folder}.",
+ "group_cannot_edit_all_users": "La grupo 'all_users' ne povas esti redaktita permane. Ĝi estas speciala grupo celita enhavi ĉiujn uzantojn registritajn en YunoHost",
+ "group_cannot_edit_visitors": "La grupo 'vizitantoj' ne povas esti redaktita permane. Ĝi estas speciala grupo reprezentanta anonimajn vizitantojn",
+ "group_cannot_edit_primary_group": "La grupo '{group}' ne povas esti redaktita permane. Ĝi estas la primara grupo celita enhavi nur unu specifan uzanton.",
+ "log_permission_url": "Ĝisdatigu url-rilataj al permeso '{}'",
+ "permission_already_up_to_date": "La permeso ne estis ĝisdatigita ĉar la petoj pri aldono/forigo jam kongruas kun la aktuala stato.",
+ "permission_currently_allowed_for_all_users": "Ĉi tiu permeso estas nuntempe donita al ĉiuj uzantoj aldone al aliaj grupoj. Vi probable volas aŭ forigi la permeson \"all_users\" aŭ forigi la aliajn grupojn, kiujn ĝi nuntempe donas.",
+ "app_install_failed": "Ne povis instali {app} : {error}",
+ "app_install_script_failed": "Eraro okazis en la skripto de instalado de la app",
+ "app_remove_after_failed_install": "Forigado de la app post la instala fiasko …",
+ "diagnosis_basesystem_host": "Servilo funkcias Debian {debian_version}.",
+ "apps_catalog_init_success": "Aplikoj katalogsistemo inicializita !",
+ "apps_catalog_updating": "Ĝisdatigante katalogo de aplikoj ...",
+ "apps_catalog_failed_to_download": "Ne eblas elŝuti la katalogon de {apps_catalog}: {error}",
+ "apps_catalog_obsolete_cache": "La kaŝmemoro de la katalogo de programoj estas malplena aŭ malaktuala.",
+ "apps_catalog_update_success": "La aplika katalogo estis ĝisdatigita!",
+ "diagnosis_basesystem_kernel": "Servilo funkcias Linuksan kernon {kernel_version}",
+ "diagnosis_basesystem_ynh_single_version": "{package} versio: {version} ({repo})",
+ "diagnosis_basesystem_ynh_main_version": "Servilo funkcias YunoHost {main_version} ({repo})",
+ "diagnosis_basesystem_ynh_inconsistent_versions": "Vi prizorgas malkonsekvencajn versiojn de la YunoHost-pakoj... plej probable pro malsukcesa aŭ parta ĝisdatigo.",
+ "diagnosis_display_tip_web": "Vi povas iri al la sekcio Diagnozo (en la hejmekrano) por vidi la trovitajn problemojn.",
+ "diagnosis_cache_still_valid": "(Kaŝmemoro ankoraŭ validas por {category} diagnozo. Ankoraŭ ne re-diagnoza!)",
+ "diagnosis_cant_run_because_of_dep": "Ne eblas fari diagnozon por {category} dum estas gravaj problemoj rilataj al {dep}.",
+ "diagnosis_display_tip_cli": "Vi povas aranĝi 'yunohost diagnosis show --issues' por aperigi la trovitajn problemojn.",
+ "diagnosis_failed_for_category": "Diagnozo malsukcesis por kategorio '{category}': {error}",
+ "app_upgrade_script_failed": "Eraro okazis en la skripto pri ĝisdatiga programo",
+ "diagnosis_diskusage_verylow": "Stokado {mountpoint} (sur aparato {device)) restas nur {free} ({free_percent}%) spaco. Vi vere konsideru purigi iom da spaco.",
+ "diagnosis_ram_verylow": "La sistemo nur restas {available} ({available_percent}%) RAM! (el {total})",
+ "diagnosis_mail_outgoing_port_25_blocked": "Eliranta haveno 25 ŝajnas esti blokita. Vi devas provi malŝlosi ĝin en via agorda panelo de provizanto (aŭ gastiganto). Dume la servilo ne povos sendi retpoŝtojn al aliaj serviloj.",
+ "diagnosis_http_bad_status_code": "Ne povis atingi vian servilon kiel atendite, ĝi redonis malbonan statuskodon. Povas esti, ke alia maŝino respondis anstataŭ via servilo. Vi devus kontroli, ke vi ĝuste redonas la havenon 80, ke via nginx-agordo ĝisdatigas kaj ke reverso-prokuro ne interbatalas.",
+ "main_domain_changed": "La ĉefa domajno estis ŝanĝita",
+ "yunohost_postinstall_end_tip": "La post-instalado finiĝis! Por fini vian agordon, bonvolu konsideri:\n - aldonado de unua uzanto tra la sekcio 'Uzantoj' de la retadreso (aŭ 'yunohost user create ' en komandlinio);\n - diagnozi problemojn atendantajn solvi por ke via servilo funkciu kiel eble plej glate tra la sekcio 'Diagnosis' de la retadministrado (aŭ 'yunohost diagnosis run' en komandlinio);\n - legante la partojn 'Finigi vian agordon' kaj 'Ekkoni Yunohost' en la administra dokumentado: https://yunohost.org/admindoc.",
+ "migration_description_0014_remove_app_status_json": "Forigi heredajn dosierojn",
+ "diagnosis_ip_connected_ipv4": "La servilo estas konektita al la interreto per IPv4 !",
+ "diagnosis_ip_no_ipv4": "La servilo ne havas funkciantan IPv4.",
+ "diagnosis_ip_connected_ipv6": "La servilo estas konektita al la interreto per IPv6 !",
+ "diagnosis_ip_no_ipv6": "La servilo ne havas funkciantan IPv6.",
+ "diagnosis_ip_not_connected_at_all": "La servilo tute ne ŝajnas esti konektita al la Interreto !?",
+ "diagnosis_ip_dnsresolution_working": "Rezolucio pri domajna nomo funkcias !",
+ "diagnosis_ip_weird_resolvconf": "DNS-rezolucio ŝajnas funkcii, sed atentu, ke vi ŝajnas uzi kutimon /etc/resolv.conf.",
+ "diagnosis_ip_weird_resolvconf_details": "Anstataŭe, ĉi tiu dosiero estu ligilo kun /etc/resolvconf/run/resolv.conf mem montrante al 127.0.0.1 (dnsmasq). La efektivaj solvantoj devas agordi per /etc/resolv.dnsmasq.conf.",
+ "diagnosis_dns_good_conf": "Bona DNS-agordo por domajno {domain} (kategorio {category})",
+ "diagnosis_dns_bad_conf": "Malbona / mankas DNS-agordo por domajno {domain} (kategorio {category})",
+ "diagnosis_ram_ok": "La sistemo ankoraŭ havas {available} ({available_percent}%) RAM forlasita de {total}.",
+ "diagnosis_swap_none": "La sistemo tute ne havas interŝanĝon. Vi devus pripensi aldoni almenaŭ {recommended} da interŝanĝo por eviti situaciojn en kiuj la sistemo restas sen memoro.",
+ "diagnosis_swap_notsomuch": "La sistemo havas nur {total}-interŝanĝon. Vi konsideru havi almenaŭ {recommended} por eviti situaciojn en kiuj la sistemo restas sen memoro.",
+ "diagnosis_regenconf_manually_modified_details": "Ĉi tio probable estas bona tiel longe kiel vi scias kion vi faras;)!",
+ "diagnosis_regenconf_manually_modified_debian": "Agordodosiero {file} estis modifita permane kompare kun la defaŭlta Debian.",
+ "diagnosis_regenconf_manually_modified_debian_details": "Ĉi tio probable estas bona, sed devas observi ĝin...",
+ "diagnosis_security_all_good": "Neniu kritika sekureca vundebleco estis trovita.",
+ "diagnosis_security_vulnerable_to_meltdown": "Vi ŝajnas vundebla al la kritiko-vundebleco de Meltdown",
+ "diagnosis_no_cache": "Neniu diagnoza kaŝmemoro por kategorio '{category}'",
+ "diagnosis_ip_broken_dnsresolution": "Rezolucio pri domajna nomo rompiĝas pro iu kialo ... Ĉu fajroŝirmilo blokas DNS-petojn ?",
+ "diagnosis_ip_broken_resolvconf": "Rezolucio pri domajna nomo ŝajnas esti rompita en via servilo, kiu ŝajnas rilata al /etc/resolv.conf ne notante 127.0.0.1.",
+ "diagnosis_dns_missing_record": "Laŭ la rekomendita DNS-agordo, vi devas aldoni DNS-registron kun\ntipo: {type}\nnomo: {name}\nvaloro: {value}",
+ "diagnosis_dns_discrepancy": "La DNS-registro kun tipo {type} kaj nomo {name} ne kongruas kun la rekomendita agordo.\nNuna valoro: {current}\nEsceptita valoro: {value}",
+ "diagnosis_services_conf_broken": "Agordo estas rompita por servo {service} !",
+ "diagnosis_services_bad_status": "Servo {service} estas {status} :(",
+ "diagnosis_ram_low": "La sistemo havas {available} ({available_percent}%) RAM forlasita de {total}. Estu zorgema.",
+ "diagnosis_swap_ok": "La sistemo havas {total} da interŝanĝoj!",
+ "diagnosis_mail_ougoing_port_25_ok": "Eliranta haveno 25 ne estas blokita kaj retpoŝto povas esti sendita al aliaj serviloj.",
+ "diagnosis_regenconf_allgood": "Ĉiuj agordaj dosieroj kongruas kun la rekomendita agordo!",
+ "diagnosis_regenconf_manually_modified": "Agordodosiero {file} estis permane modifita.",
+ "diagnosis_description_ip": "Interreta konektebleco",
+ "diagnosis_description_dnsrecords": "Registroj DNS",
+ "diagnosis_description_services": "Servo kontrolas staton",
+ "diagnosis_description_systemresources": "Rimedaj sistemoj",
+ "diagnosis_description_security": "Sekurecaj kontroloj",
+ "diagnosis_ports_could_not_diagnose": "Ne povis diagnozi, ĉu haveblaj havenoj de ekstere.",
+ "diagnosis_ports_could_not_diagnose_details": "Eraro: {error}",
+ "diagnosis_services_bad_status_tip": "Vi povas provi rekomenci la servon, kaj se ĝi ne funkcias, trarigardu la servajn protokolojn uzante 'yunohost service log {service}' aŭ tra la sekcio 'Servoj' de la retadreso.",
+ "diagnosis_security_vulnerable_to_meltdown_details": "Por ripari tion, vi devas ĝisdatigi vian sistemon kaj rekomenci por ŝarĝi la novan linux-kernon (aŭ kontaktu vian servilan provizanton se ĉi tio ne funkcias). Vidu https://meltdownattack.com/ por pliaj informoj.",
+ "diagnosis_description_basesystem": "Baza sistemo",
+ "diagnosis_description_regenconf": "Sistemaj agordoj",
+ "main_domain_change_failed": "Ne eblas ŝanĝi la ĉefan domajnon",
+ "log_domain_main_domain": "Faru '{}' kiel ĉefa domajno",
+ "diagnosis_http_timeout": "Tempolimigita dum provado kontakti vian servilon de ekstere. Ĝi ŝajnas esti neatingebla. Vi devus kontroli, ke vi ĝuste redonas la havenon 80, ke nginx funkcias kaj ke fajroŝirmilo ne interbatalas.",
+ "diagnosis_http_connection_error": "Rilata eraro: ne povis konektiĝi al la petita domajno, tre probable ĝi estas neatingebla.",
+ "migration_description_0013_futureproof_apps_catalog_system": "Migru al la nova katalogosistemo pri estontecaj programoj",
+ "diagnosis_ignored_issues": "(+ {nb_ignored} ignorataj aferoj))",
+ "diagnosis_found_errors": "Trovis {errors} signifa(j) afero(j) rilata al {category}!",
+ "diagnosis_found_errors_and_warnings": "Trovis {errors} signifaj problemo (j) (kaj {warnings} averto) rilataj al {category}!",
+ "diagnosis_diskusage_low": "Stokado {mountpoint} (sur aparato {device)) restas nur {free} ({free_percent}%) spaco. Estu zorgema.",
+ "diagnosis_diskusage_ok": "Stokado {mountpoint} (sur aparato {device) ankoraŭ restas {free} ({free_percent}%) spaco!",
+ "global_settings_setting_pop3_enabled": "Ebligu la protokolon POP3 por la poŝta servilo",
+ "diagnosis_unknown_categories": "La jenaj kategorioj estas nekonataj: {categories}",
+ "diagnosis_services_running": "Servo {service} funkcias!",
+ "diagnosis_ports_unreachable": "Haveno {port} ne atingeblas de ekstere.",
+ "diagnosis_ports_ok": "Haveno {port} atingeblas de ekstere.",
+ "diagnosis_ports_needed_by": "Eksponi ĉi tiun havenon necesas por servo {service}",
+ "diagnosis_ports_forwarding_tip": "Por solvi ĉi tiun problemon, plej probable vi devas agordi la plusendon de haveno en via interreta enkursigilo kiel priskribite en https://yunohost.org/isp_box_config",
+ "diagnosis_http_could_not_diagnose": "Ne povis diagnozi, ĉu atingeblas domajno de ekstere.",
+ "diagnosis_http_could_not_diagnose_details": "Eraro: {error}",
+ "diagnosis_http_ok": "Domajno {domain} atingeblas de ekstere.",
+ "diagnosis_http_unreachable": "Domajno {domain} estas atingebla per HTTP de ekstere.",
+ "domain_cannot_remove_main_add_new_one": "Vi ne povas forigi '{domain:s}' ĉar ĝi estas la ĉefa domajno kaj via sola domajno, vi devas unue aldoni alian domajnon uzante ''yunohost domain add ', tiam agordi kiel ĉefan domajnon uzante 'yunohost domain main-domain -n ' kaj tiam vi povas forigi la domajnon' {domain:s} 'uzante' yunohost domain remove {domain:s} '.'",
+ "permission_require_account": "Permesilo {permission} nur havas sencon por uzantoj, kiuj havas konton, kaj tial ne rajtas esti ebligitaj por vizitantoj.",
+ "diagnosis_found_warnings": "Trovitaj {warnings} ero (j) kiuj povus esti plibonigitaj por {category}.",
+ "diagnosis_everything_ok": "Ĉio aspektas bone por {category}!",
+ "diagnosis_failed": "Malsukcesis preni la diagnozan rezulton por kategorio '{category}': {error}",
+ "diagnosis_description_ports": "Ekspoziciaj havenoj",
+ "diagnosis_description_mail": "Retpoŝto",
+ "log_app_action_run": "Funkciigu agon de la apliko '{}'",
+ "log_app_config_show_panel": "Montri la agordan panelon de la apliko '{}'",
+ "log_app_config_apply": "Apliki agordon al la apliko '{}'"
}
diff --git a/locales/es.json b/locales/es.json
index d219b36e1..15b4b5316 100644
--- a/locales/es.json
+++ b/locales/es.json
@@ -9,82 +9,53 @@
"app_argument_required": "Se requiere el argumento '{name:s} 7'",
"app_extraction_failed": "No se pudieron extraer los archivos de instalación",
"app_id_invalid": "ID de la aplicación no válida",
- "app_incompatible": "La aplicación {app} no es compatible con su versión de YunoHost",
"app_install_files_invalid": "Estos archivos no se pueden instalar",
- "app_location_already_used": "La aplicación «{app}» ya está instalada en ({path})",
- "app_location_install_failed": "No se puede instalar la aplicación ahí porque entra en conflicto con la aplicación «{other_app}» ya instalada en «{other_path}»",
"app_manifest_invalid": "Algo va mal con el manifiesto de la aplicación: {error}",
- "app_no_upgrade": "Todas las aplicaciones están ya actualizadas",
"app_not_correctly_installed": "La aplicación {app:s} 8 parece estar incorrectamente instalada",
"app_not_installed": "No se pudo encontrar la aplicación «{app:s}» en la lista de aplicaciones instaladas: {all_apps}",
"app_not_properly_removed": "La {app:s} 0 no ha sido desinstalada correctamente",
- "app_package_need_update": "El paquete de la aplicación {app} necesita ser actualizada debido a los cambios en YunoHost",
- "app_recent_version_required": "{:s} requiere una versión más reciente de moulinette ",
"app_removed": "Eliminado {app:s}",
"app_requirements_checking": "Comprobando los paquetes necesarios para {app}…",
- "app_requirements_failed": "No se cumplen algunos requisitos para {app}: {error}",
"app_requirements_unmeet": "No se cumplen los requisitos para {app}, el paquete {pkgname} ({version}) debe ser {spec}",
"app_sources_fetch_failed": "No se pudieron obtener los archivos con el código fuente, ¿es el URL correcto?",
"app_unknown": "Aplicación desconocida",
"app_unsupported_remote_type": "Tipo remoto no soportado por la aplicación",
- "app_upgrade_failed": "No se pudo actualizar {app:s}",
+ "app_upgrade_failed": "No se pudo actualizar {app:s}: {error}",
"app_upgraded": "Actualizado {app:s}",
- "appslist_fetched": "Obtenida lista de aplicaciones {appslist:s} actualizada",
- "appslist_removed": "Eliminada la lista de aplicaciones {appslist:s}",
- "appslist_retrieve_error": "No se puede recuperar la lista remota de aplicaciones {appslist:s}: {error:s}",
- "appslist_unknown": "Lista de aplicaciones {appslist:s} desconocida.",
- "ask_current_admin_password": "Contraseña administrativa actual",
"ask_email": "Dirección de correo electrónico",
"ask_firstname": "Nombre",
"ask_lastname": "Apellido",
- "ask_list_to_remove": "Lista para desinstalar",
"ask_main_domain": "Dominio principal",
"ask_new_admin_password": "Nueva contraseña administrativa",
"ask_password": "Contraseña",
- "backup_action_required": "Debe especificar algo que guardar",
"backup_app_failed": "No se pudo respaldar la aplicación «{app:s}»",
"backup_archive_app_not_found": "No se pudo encontrar la aplicación «{app:s}» en el archivo de respaldo",
- "backup_archive_hook_not_exec": "El hook {hook:s} no ha sido ejecutado en esta copia de seguridad",
"backup_archive_name_exists": "Ya existe un archivo de respaldo con este nombre.",
"backup_archive_name_unknown": "Copia de seguridad local desconocida '{name:s}'",
"backup_archive_open_failed": "No se pudo abrir el archivo de respaldo",
"backup_cleaning_failed": "No se pudo limpiar la carpeta de respaldo temporal",
"backup_created": "Se ha creado la copia de seguridad",
- "backup_creating_archive": "Creando el archivo de copia de seguridad…",
"backup_creation_failed": "No se pudo crear el archivo de respaldo",
"backup_delete_error": "No se pudo eliminar «{path:s}»",
"backup_deleted": "Eliminada la copia de seguridad",
- "backup_extracting_archive": "Extrayendo el archivo de respaldo…",
"backup_hook_unknown": "El gancho «{hook:s}» de la copia de seguridad es desconocido",
"backup_invalid_archive": "Esto no es un archivo de respaldo",
"backup_nothings_done": "Nada que guardar",
"backup_output_directory_forbidden": "Elija un directorio de salida diferente. No se pueden crear copias de seguridad en las subcarpetas de /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var o /home/yunohost.backup/archives",
"backup_output_directory_not_empty": "Debe elegir un directorio de salida vacío",
"backup_output_directory_required": "Debe proporcionar un directorio de salida para la copia de seguridad",
- "backup_running_app_script": "Ejecutando la script de copia de seguridad de la aplicación '{app:s}'...",
"backup_running_hooks": "Ejecutando los hooks de copia de seguridad...",
"custom_app_url_required": "Debe proporcionar una URL para actualizar su aplicación personalizada {app:s}",
- "custom_appslist_name_required": "Debe proporcionar un nombre para su lista de aplicaciones personalizadas",
- "diagnosis_debian_version_error": "No se pudo obtener la versión de Debian: {error}",
- "diagnosis_kernel_version_error": "No se pudo obtener la versión del núcleo: {error}",
- "diagnosis_monitor_disk_error": "No se pudieron monitorizar los discos: {error}",
- "diagnosis_monitor_network_error": "No se pudo monitorizar la red: {error}",
- "diagnosis_monitor_system_error": "No se pudo monitorizar el sistema: {error}",
- "diagnosis_no_apps": "Aplicación no instalada",
- "dnsmasq_isnt_installed": "Parece que dnsmasq no está instalado, ejecute «apt-get remove bind9 && apt-get install it»",
"domain_cert_gen_failed": "No se pudo generar el certificado",
"domain_created": "Dominio creado",
- "domain_creation_failed": "No se pudo crear el dominio",
+ "domain_creation_failed": "No se pudo crear el dominio {domain}: {error}",
"domain_deleted": "Dominio eliminado",
- "domain_deletion_failed": "No se pudo eliminar el dominio",
+ "domain_deletion_failed": "No se pudo eliminar el dominio {domain}: {error}",
"domain_dyndns_already_subscribed": "Ya se ha suscrito a un dominio de DynDNS",
- "domain_dyndns_invalid": "Este dominio no se puede usar con DynDNS",
"domain_dyndns_root_unknown": "Dominio raíz de DynDNS desconocido",
"domain_exists": "El dominio ya existe",
"domain_uninstall_app_first": "Una o más aplicaciones están instaladas en este dominio. Debe desinstalarlas antes de eliminar el dominio",
"domain_unknown": "Dominio desconocido",
- "domain_zone_exists": "El archivo de zona del DNS ya existe",
- "domain_zone_not_found": "No se ha encontrado el archivo de zona del DNS para el dominio [:s]",
"done": "Hecho.",
"downloading": "Descargando…",
"dyndns_cron_installed": "Creado el trabajo de cron de DynDNS",
@@ -104,10 +75,7 @@
"field_invalid": "Campo no válido '{:s}'",
"firewall_reload_failed": "No se pudo recargar el cortafuegos",
"firewall_reloaded": "Cortafuegos recargado",
- "firewall_rules_cmd_failed": "Algunas órdenes de las reglas del cortafuegos han fallado. Más información en el registro.",
- "format_datetime_short": "%d/%m/%Y %I:%M %p",
- "hook_argument_missing": "Falta un parámetro '{:s}'",
- "hook_choice_invalid": "Selección inválida '{:s}'",
+ "firewall_rules_cmd_failed": "Algunos comandos para aplicar reglas del cortafuegos han fallado. Más información en el registro.",
"hook_exec_failed": "No se pudo ejecutar el guión: {path:s}",
"hook_exec_not_terminated": "El guión no terminó correctamente:{path:s}",
"hook_list_by_invalid": "Esta propiedad no se puede usar para enumerar ganchos («hooks»)",
@@ -117,98 +85,52 @@
"ip6tables_unavailable": "No puede modificar ip6tables aquí. O bien está en un 'container' o su kernel no soporta esta opción",
"iptables_unavailable": "No puede modificar iptables aquí. O bien está en un 'container' o su kernel no soporta esta opción",
"ldap_initialized": "Inicializado LDAP",
- "license_undefined": "indefinido",
"mail_alias_remove_failed": "No se pudo eliminar el alias de correo «{mail:s}»",
- "mail_domain_unknown": "Dirección de correo desconocida para el dominio «{domain:s}»",
+ "mail_domain_unknown": "Dirección de correo no válida para el dominio «{domain:s}». Use un dominio administrado por este servidor.",
"mail_forward_remove_failed": "No se pudo eliminar el reenvío de correo «{mail:s}»",
- "maindomain_change_failed": "No se pudo cambiar el dominio principal",
- "maindomain_changed": "El dominio principal ha cambiado",
- "monitor_disabled": "Desactivada la monitorización del servidor",
- "monitor_enabled": "Activada la monitorización del servidor",
- "monitor_glances_con_failed": "No se pudo conectar al servidor de Glances",
- "monitor_not_enabled": "La monitorización del servidor está apagada",
- "monitor_period_invalid": "Período de tiempo no válido",
- "monitor_stats_file_not_found": "No se pudo encontrar el archivo de estadísticas",
- "monitor_stats_no_update": "No hay estadísticas de monitorización para actualizar",
- "monitor_stats_period_unavailable": "No hay estadísticas para el período",
- "mountpoint_unknown": "Punto de montaje desconocido",
- "mysql_db_creation_failed": "Error al crear la base de datos de MySQL",
- "mysql_db_init_failed": "Error al iniciar la base de datos de MySQL",
- "mysql_db_initialized": "Inicializada la base de datos MySQL",
- "network_check_mx_ko": "El registro DNS MX no está configurado",
- "network_check_smtp_ko": "El correo saliente (SMTP puerto 25) parece estar bloqueado por su red",
- "network_check_smtp_ok": "El correo saliente (SMTP puerto 25) no está bloqueado",
- "new_domain_required": "Debe proporcionar el nuevo dominio principal",
- "no_appslist_found": "No se ha encontrado ninguna lista de aplicaciones",
+ "main_domain_change_failed": "No se pudo cambiar el dominio principal",
+ "main_domain_changed": "El dominio principal ha cambiado",
"no_internet_connection": "El servidor no está conectado a Internet",
- "no_ipv6_connectivity": "La conexión por IPv6 no está disponible",
- "no_restore_script": "No se ha encontrado un script de restauración para la aplicación '{app:s}'",
"not_enough_disk_space": "No hay espacio libre suficiente en «{path:s}»",
- "package_not_installed": "El paquete '{pkgname}' no está instalado",
- "package_unexpected_error": "Ha ocurrido un error inesperado procesando el paquete '{pkgname}'",
"package_unknown": "Paquete desconocido '{pkgname}'",
- "packages_no_upgrade": "No hay paquetes para actualizar",
- "packages_upgrade_critical_later": "Los paquetes críticos ({packages:s}) serán actualizados más tarde",
"packages_upgrade_failed": "No se pudieron actualizar todos los paquetes",
- "path_removal_failed": "No se pudo eliminar la ruta {:s}",
"pattern_backup_archive_name": "Debe ser un nombre de archivo válido con un máximo de 30 caracteres, solo se admiten caracteres alfanuméricos y los caracteres -_. (guiones y punto)",
"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 (p.ej. alguien@example.com)",
"pattern_firstname": "Debe ser un nombre válido",
"pattern_lastname": "Debe ser un apellido válido",
- "pattern_listname": "Solo se pueden usar caracteres alfanuméricos y el guion bajo",
"pattern_mailbox_quota": "Debe ser un tamaño con el sufijo «b/k/M/G/T» o «0» para no tener una cuota",
"pattern_password": "Debe contener al menos 3 caracteres",
- "pattern_port": "Debe ser un número de puerto válido (es decir, entre 0-65535)",
"pattern_port_or_range": "Debe ser un número de puerto válido (es decir entre 0-65535) o un intervalo de puertos (por ejemplo 100:200)",
"pattern_positive_number": "Deber ser un número positivo",
"pattern_username": "Solo puede contener caracteres alfanuméricos o el guión bajo",
"port_already_closed": "El puerto {port:d} ya está cerrado para las conexiones {ip_version:s}",
"port_already_opened": "El puerto {port:d} ya está abierto para las conexiones {ip_version:s}",
- "port_available": "El puerto {port:d} está disponible",
- "port_unavailable": "El puerto {port:d} no está disponible",
- "restore_action_required": "Debe escoger algo que restaurar",
"restore_already_installed_app": "Una aplicación con el ID «{app:s}» ya está instalada",
"restore_app_failed": "No se pudo restaurar la aplicación «{app:s}»",
"restore_cleaning_failed": "No se pudo limpiar el directorio temporal de restauración",
"restore_complete": "Restaurada",
"restore_confirm_yunohost_installed": "¿Realmente desea restaurar un sistema ya instalado? [{answers:s}]",
"restore_failed": "No se pudo restaurar el sistema",
- "restore_hook_unavailable": "El guión de restauración para «{part:s}» no está disponible en su sistema y tampoco en el archivo",
+ "restore_hook_unavailable": "El script de restauración para «{part:s}» no está disponible en su sistema y tampoco en el archivo",
"restore_nothings_done": "No se ha restaurado nada",
"restore_running_app_script": "Restaurando la aplicación «{app:s}»…",
"restore_running_hooks": "Ejecutando los ganchos de restauración…",
"service_add_failed": "No se pudo añadir el servicio «{service:s}»",
"service_added": "Añadido el servicio «{service:s}»",
- "service_already_started": "El servicio «{service:s}» ya ha sido iniciado",
+ "service_already_started": "El servicio «{service:s}» ya está funcionando",
"service_already_stopped": "El servicio «{service:s}» ya ha sido detenido",
"service_cmd_exec_failed": "No se pudo ejecutar la orden «{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_copy_failed": "No se puede copiar el nuevo archivo de configuración '{new}' a {conf}",
- "service_conf_file_manually_modified": "El archivo de configuración '{conf}' ha sido modificado manualmente y no será actualizado",
- "service_conf_file_manually_removed": "El archivo de configuración '{conf}' ha sido eliminado manualmente y no será creado",
- "service_conf_file_not_managed": "El archivo de configuración '{conf}' no está gestionado y no será actualizado",
- "service_conf_file_remove_failed": "No se puede eliminar el archivo de configuración '{conf}'",
- "service_conf_file_removed": "El archivo de configuración '{conf}' ha sido eliminado",
- "service_conf_file_updated": "El archivo de configuración '{conf}' ha sido actualizado",
- "service_conf_up_to_date": "La configuración del servicio '{service}' ya está actualizada",
- "service_conf_updated": "La configuración ha sido actualizada para el servicio '{service}'",
- "service_conf_would_be_updated": "La configuración podría haber sido actualizada para el servicio '{service} 1'",
"service_disable_failed": "No se pudo desactivar el servicio «{service:s}»\n\nRegistro de servicios recientes:{logs:s}",
- "service_disabled": "Desactivado el servicio «{service:s}»",
+ "service_disabled": "El servicio «{service:s}» ha sido desactivado",
"service_enable_failed": "No se pudo activar el servicio «{service:s}»\n\nRegistro de servicios recientes:{logs:s}",
- "service_enabled": "Activado 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 pendiente que podría haber sido aplicada al servicio '{service}'...",
- "service_regenconf_failed": "No se puede regenerar la configuración para el servicio(s): {services}",
- "service_regenconf_pending_applying": "Aplicando la configuración pendiente para el servicio '{service}'...",
+ "service_enabled": "El servicio «{service:s}» ha sido desactivado",
"service_remove_failed": "No se pudo eliminar el servicio «{service:s}»",
"service_removed": "Eliminado el servicio «{service:s}»",
"service_start_failed": "No se pudo iniciar el servicio «{service:s}»\n\nRegistro de servicios recientes:{logs:s}",
"service_started": "Iniciado el servicio «{service:s}»",
- "service_status_failed": "No se pudo determinar el estado del servicio «{service:s}»",
"service_stop_failed": "No se pudo detener el servicio «{service:s}»\n\nRegistro de servicios recientes:{logs:s}",
- "service_stopped": "Detenido el servicio «{service:s}»",
+ "service_stopped": "El servicio «{service:s}» se detuvo",
"service_unknown": "Servicio desconocido '{service:s}'",
"ssowat_conf_generated": "Generada la configuración de SSOwat",
"ssowat_conf_updated": "Actualizada la configuración de SSOwat",
@@ -216,10 +138,8 @@
"system_username_exists": "El nombre de usuario ya existe en la lista de usuarios del sistema",
"unbackup_app": "La aplicación '{app:s}' no se guardará",
"unexpected_error": "Algo inesperado salió mal: {error}",
- "unit_unknown": "Unidad desconocida '{unit:s}'",
"unlimit": "Sin cuota",
"unrestore_app": "La aplicación '{app:s}' no será restaurada",
- "update_cache_failed": "No se pudo actualizar la caché de APT",
"updating_apt_cache": "Obteniendo las actualizaciones disponibles para los paquetes del sistema…",
"upgrade_complete": "Actualización finalizada",
"upgrading_packages": "Actualizando paquetes…",
@@ -228,23 +148,20 @@
"upnp_enabled": "UPnP activado",
"upnp_port_open_failed": "No se pudo abrir el puerto vía UPnP",
"user_created": "Usuario creado",
- "user_creation_failed": "No se pudo crear el usuario",
+ "user_creation_failed": "No se pudo crear el usuario {user}: {error}",
"user_deleted": "Usuario eliminado",
- "user_deletion_failed": "No se pudo eliminar el usuario",
+ "user_deletion_failed": "No se pudo eliminar el usuario {user}: {error}",
"user_home_creation_failed": "No se pudo crear la carpeta «home» para el usuario",
- "user_info_failed": "No se pudo obtener la información del usuario",
"user_unknown": "Usuario desconocido: {user:s}",
- "user_update_failed": "No se pudo cambiar la información del usuario",
+ "user_update_failed": "No se pudo actualizar el usuario {user}: {error}",
"user_updated": "Cambiada la información de usuario",
"yunohost_already_installed": "YunoHost ya está instalado",
"yunohost_ca_creation_failed": "No se pudo crear la autoridad de certificación",
- "yunohost_configured": "YunoHost está configurado",
+ "yunohost_configured": "YunoHost está ahora configurado",
"yunohost_installing": "Instalando YunoHost…",
"yunohost_not_installed": "YunoHost no está correctamente instalado. Ejecute «yunohost tools postinstall»",
"ldap_init_failed_to_create_admin": "La inicialización de LDAP no pudo crear el usuario «admin»",
"mailbox_used_space_dovecot_down": "El servicio de correo Dovecot debe estar funcionando si desea obtener el espacio usado por el buzón de correo",
- "ssowat_persistent_conf_read_error": "No se pudo 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": "No se pudo 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 hacerlo)",
@@ -258,50 +175,38 @@
"certmanager_cert_install_success_selfsigned": "Instalado correctamente un certificado autofirmado para el dominio «{domain:s}»",
"certmanager_cert_install_success": "Instalado correctamente un certificado de Let's Encrypt para el dominio «{domain:s}»",
"certmanager_cert_renew_success": "Renovado correctamente el certificado de 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 este conjunto exacto de dominios {domain:s}. Pruebe de nuevo más tarde. Vea para más detalles https://letsencrypt.org/docs/rate-limits/",
"certmanager_cert_signing_failed": "No se pudo firmar el nuevo certificado",
"certmanager_no_cert_file": "No se pudo leer el certificado para el dominio {domain:s} (archivo: {file:s})",
"certmanager_conflicting_nginx_file": "No se pudo preparar el dominio para el desafío ACME: el archivo de configuración de NGINX {filepath:s} está en conflicto y debe ser eliminado primero",
- "domain_cannot_remove_main": "No se puede eliminar el dominio principal. Configure uno primero",
+ "domain_cannot_remove_main": "No se puede eliminar el dominio principal. Primero debes configurar otro utilizando la linea de comando 'yunohost domain main-domain -n ' donde es parte de esta lista: {other_domains:s}",
"certmanager_self_ca_conf_file_not_found": "No se pudo encontrar el archivo de configuración para la autoridad de autofirma (archivo: {file:s})",
"certmanager_unable_to_parse_self_CA_name": "No se pudo procesar el nombre de la autoridad de autofirma (archivo: {file:s})",
"domains_available": "Dominios disponibles:",
"backup_archive_broken_link": "No se pudo acceder al archivo de respaldo (enlace roto a {path:s})",
- "certmanager_domain_not_resolved_locally": "El dominio {domain:s} no puede ser resuelto desde su servidor de YunoHost. Esto puede suceder si ha modificado su registro DNS recientemente. De ser así, espere unas horas para que se propague. Si el problema continúa, considere añadir {domain:s} a /etc/hosts. (Si sabe lo que está haciendo, use «--no-checks» para desactivar esas comprobaciones.)",
"certmanager_acme_not_configured_for_domain": "El certificado para el dominio «{domain:s}» no parece que esté instalado correctamente. Ejecute primero «cert-install» para este dominio.",
"certmanager_http_check_timeout": "Tiempo de espera agotado cuando el servidor intentaba conectarse consigo mismo a través de HTTP usando una dirección IP pública (dominio «{domain:s}» con IP «{ip:s}»). Puede que esté experimentando un problema de redirección («hairpinning»), o que el cortafuegos o el enrutador de su servidor esté mal configurado.",
"certmanager_couldnt_fetch_intermediate_cert": "Tiempo de espera agotado intentando obtener el certificado intermedio de Let's Encrypt. Cancelada la instalación o renovación del certificado. Vuelva a intentarlo más tarde.",
- "appslist_retrieve_bad_format": "No se pudo leer la lista de aplicaciones obtenida {appslist:s}",
"domain_hostname_failed": "No se pudo establecer un nuevo nombre de anfitrión («hostname»). Esto podría causar problemas más tarde (no es seguro... podría ir bien).",
"yunohost_ca_creation_success": "Creada la autoridad de certificación local.",
"app_already_installed_cant_change_url": "Esta aplicación ya está instalada. No se puede cambiar el URL únicamente mediante esta función. Compruebe si está disponible la opción `app changeurl`.",
- "app_change_no_change_url_script": "La aplicacion {app_name:s} aún no permite cambiar su URL, es posible que deba actualizarla.",
"app_change_url_failed_nginx_reload": "No se pudo recargar NGINX. Esta es la salida de «nginx -t»:\n{nginx_errors:s}",
"app_change_url_identical_domains": "El antiguo y nuevo dominio/url_path son idénticos ('{domain:s} {path:s}'), no se realizarán cambios.",
- "app_change_url_no_script": "Esta aplicación «{app_name:s}» aún no permite la modificación de URLs. Quizás debería actualizarla.",
+ "app_change_url_no_script": "La aplicación «{app_name:s}» aún no permite la modificación de URLs. Quizás debería actualizarla.",
"app_change_url_success": "El URL de la aplicación {app:s} es ahora {domain:s} {path:s}",
"app_location_unavailable": "Este URL o no está disponible o está en conflicto con otra(s) aplicación(es) instalada(s):\n{apps:s}",
"app_already_up_to_date": "La aplicación {app:s} ya está actualizada",
- "appslist_name_already_tracked": "Ya existe una lista de aplicaciones registradas con el nombre {name:s}.",
- "appslist_url_already_tracked": "Ya existe una lista de aplicaciones registradas con el URL {url:s}.",
- "appslist_migrating": "Migrando la lista de aplicaciones {appslist:s}…",
- "appslist_could_not_migrate": "¡No se pudo migrar la lista de aplicaciones {appslist:s}! No se pudo analizar el URL… El antiguo trabajo de cron se mantuvo en {bkp_file:s}.",
- "appslist_corrupted_json": "No se pudieron cargar las listas de aplicaciones. Parece que {filename:s} está dañado.",
- "invalid_url_format": "Algo va mal con el URL",
"app_upgrade_some_app_failed": "No se pudieron actualizar algunas aplicaciones",
- "app_make_default_location_already_used": "No puede hacer que la aplicación «{app}» sea la predeterminada en el dominio, {domain} ya está siendo usado por otra aplicación «{other_app}»",
+ "app_make_default_location_already_used": "No puede hacer que la aplicación «{app}» sea la predeterminada en el dominio, «{domain}» ya está siendo usado por otra aplicación «{other_app}»",
"app_upgrade_app_name": "Actualizando ahora {app}…",
- "ask_path": "Camino",
"backup_abstract_method": "Este método de respaldo aún no se ha implementado",
"backup_applying_method_borg": "Enviando todos los archivos para la copia de seguridad al repositorio de borg-backup…",
"backup_applying_method_copy": "Copiando todos los archivos a la copia de seguridad…",
"backup_applying_method_custom": "Llamando al método de copia de seguridad personalizado «{method:s}»…",
"backup_applying_method_tar": "Creando el archivo TAR de respaldo…",
- "backup_archive_mount_failed": "No se pudo montar el archivo de respaldo",
"backup_archive_system_part_not_available": "La parte del sistema «{part:s}» no está disponible en esta copia de seguridad",
"backup_archive_writing_error": "No se pudieron añadir los archivos «{source:s}» (llamados en el archivo «{dest:s}») para ser respaldados en el archivo comprimido «{archive:s}»",
- "backup_ask_for_copying_if_needed": "No se pudieron preparar algunos archivos para la copia de seguridad usando el método que evita desperdiciar espacio temporalmente en el sistema. Para hacer la copia de seguridad, {size:s}MB se usarán temporalmente. ¿Está de acuerdo?",
+ "backup_ask_for_copying_if_needed": "¿Quiere realizar la copia de seguridad usando {size:s} MB temporalmente? (Se usa este modo ya que algunos archivos no se pudieron preparar usando un método más eficiente.)",
"backup_borg_not_implemented": "El método de respaldo de Borg aún no ha sido implementado",
"backup_cant_mount_uncompress_archive": "No se pudo montar el archivo descomprimido como protegido contra escritura",
"backup_copying_to_organize_the_archive": "Copiando {size:s}MB para organizar el archivo",
@@ -309,7 +214,6 @@
"backup_csv_addition_failed": "No se pudo añadir archivos para respaldar en el archivo CSV",
"backup_csv_creation_failed": "No se pudo crear el archivo CSV necesario para la restauración",
"backup_custom_mount_error": "El método de respaldo personalizado no pudo superar el paso «mount»",
- "backup_custom_need_mount_error": "El método de respaldo personalizado no pudo superar el paso «need_mount»",
"backup_no_uncompress_archive_dir": "No existe tal directorio de archivos sin comprimir",
"backup_php5_to_php7_migration_may_fail": "No se pudo convertir su archivo para que sea compatible con PHP 7, puede que no pueda restaurar sus aplicaciones de PHP (motivo: {error:s})",
"backup_system_part_failed": "No se pudo respaldar la parte del sistema «{part:s}»",
@@ -320,33 +224,28 @@
"experimental_feature": "Aviso : esta funcionalidad es experimental y no se considera estable, no debería usarla a menos que sepa lo que está haciendo.",
"good_practices_about_user_password": "Está a punto de establecer una nueva contraseña de usuario. La contraseña debería de ser de al menos 8 caracteres, aunque es una buena práctica usar una contraseña más extensa (básicamente una frase) y/o usar caracteres de varias clases (mayúsculas, minúsculas, números y caracteres especiales).",
"password_listed": "Esta contraseña es una de las más usadas en el mundo. Elija algo más único.",
- "password_too_simple_1": "La contraseña tiene que ser de al menos 8 caracteres de longitud",
+ "password_too_simple_1": "La contraseña debe tener al menos 8 caracteres de longitud",
"password_too_simple_2": "La contraseña tiene que ser de al menos 8 caracteres de longitud e incluir un número y caracteres en mayúsculas y minúsculas",
"password_too_simple_3": "La contraseña tiene que ser de al menos 8 caracteres de longitud e incluir un número, mayúsculas, minúsculas y caracteres especiales",
"password_too_simple_4": "La contraseña tiene que ser de al menos 12 caracteres de longitud e incluir un número, mayúsculas, minúsculas y caracteres especiales",
"users_available": "Usuarios disponibles:",
- "user_not_in_group": "El usuario «{user:s}» no está en el grupo «{group:s}»",
- "user_already_in_group": "El usuario «{user:}» ya está en el grupo «{group:s}»",
- "updating_app_lists": "Obteniendo actualizaciones disponibles para las aplicaciones…",
"update_apt_cache_warning": "Algo fue mal durante la actualización de la caché de APT (gestor de paquetes de Debian). Aquí tiene un volcado de las líneas de sources.list que podría ayudarle a identificar las líneas problemáticas:\n{sourceslist}",
"update_apt_cache_failed": "No se pudo actualizar la caché de APT (gestor de paquetes de Debian). Aquí tiene un volcado de las líneas de sources.list que podría ayudarle a identificar las líneas problemáticas:\n{sourceslist}",
"tools_upgrade_special_packages_completed": "Actualización de paquetes de YunoHost completada.\nPulse [Intro] para regresar a la línea de órdenes",
- "tools_upgrade_special_packages_explanation": "Esta acción terminará pero la actualización especial real continuará en segundo plano. No inicie ninguna otra acción en su servidor en aproximadamente 10 minutos (dependiendo de la velocidad de su hardware). Una vez que esté hecho, podría tener que volver a iniciar sesión en la administración web. El registro de actualización estará disponible en Herramientas → Registro (en la página de administración web) o mediante «yunohost log list» (desde la línea de órdenes).",
+ "tools_upgrade_special_packages_explanation": "Esta acción terminará pero la actualización especial real continuará en segundo plano. No inicie ninguna otra acción en su servidor en aproximadamente 10 minutos (dependiendo de la velocidad de su hardware). Una vez hecho, podría tener que volver a iniciar sesión en la administración web. El registro de actualización estará disponible en Herramientas → Registro (en la página de administración web) o mediante «yunohost log list» (desde la línea de órdenes).",
"tools_upgrade_special_packages": "Actualizando ahora paquetes «especiales» (relacionados con YunoHost)…",
"tools_upgrade_regular_packages_failed": "No se pudieron actualizar los paquetes: {packages_list}",
"tools_upgrade_regular_packages": "Actualizando ahora paquetes «normales» (no relacionados con YunoHost)…",
- "tools_upgrade_cant_unhold_critical_packages": "No se pudieron liberar los paquetes críticos…",
+ "tools_upgrade_cant_unhold_critical_packages": "No se pudo liberar los paquetes críticos…",
"tools_upgrade_cant_hold_critical_packages": "No se pudieron retener los paquetes críticos…",
"tools_upgrade_cant_both": "No se puede actualizar el sistema y las aplicaciones al mismo tiempo",
"tools_upgrade_at_least_one": "Especifique «--apps», o «--system»",
- "tools_update_failed_to_app_fetchlist": "No se pudo actualizar la lista de aplicaciones de YunoHost porque: {error}",
"this_action_broke_dpkg": "Esta acción rompió dpkg/APT(los gestores de paquetes del sistema)… Puede tratar de solucionar este problema conectando mediante SSH y ejecutando `sudo dpkg --configure -a`.",
- "system_groupname_exists": "El nombre de grupo ya existe en el grupo del sistema",
- "service_reloaded_or_restarted": "Recargado o reiniciado el servicio «{service:s}»",
+ "service_reloaded_or_restarted": "El servicio «{service:s}» ha sido recargado o reiniciado",
"service_reload_or_restart_failed": "No se pudo recargar o reiniciar el servicio «{service:s}»\n\nRegistro de servicios recientes:{logs:s}",
"service_restarted": "Reiniciado el servicio «{service:s}»",
"service_restart_failed": "No se pudo reiniciar el servicio «{service:s}»\n\nRegistro de servicios recientes:{logs:s}",
- "service_reloaded": "Recargado el servicio «{service:s}»",
+ "service_reloaded": "El servicio «{service:s}» ha sido recargado",
"service_reload_failed": "No se pudo recargar el servicio «{service:s}»\n\nRegistro de servicios recientes:{logs:s}",
"service_regen_conf_is_deprecated": "¡«yunohost service regen-conf» está obsoleto! Use «yunohost tools regen-conf» en su lugar.",
"service_description_yunohost-firewall": "Gestiona los puertos de conexiones abiertos y cerrados a los servicios",
@@ -354,15 +253,13 @@
"service_description_ssh": "Permite conectar a su servidor remotamente mediante un terminal (protocolo SSH)",
"service_description_slapd": "Almacena usuarios, dominios e información relacionada",
"service_description_rspamd": "Filtra correo no deseado y otras características relacionadas con el correo",
- "service_description_rmilter": "Comprueba varios parámetros en el correo",
"service_description_redis-server": "Una base de datos especializada usada para el acceso rápido de datos, cola de tareas y comunicación entre programas",
"service_description_postfix": "Usado para enviar y recibir correos",
"service_description_php7.0-fpm": "Ejecuta aplicaciones escritas en PHP con NGINX",
"service_description_nslcd": "Maneja la conexión del intérprete de órdenes («shell») de usuario de YunoHost",
"service_description_nginx": "Sirve o proporciona acceso a todos los sitios web alojados en su servidor",
- "service_description_mysql": "Almacena los datos de las aplicaciones (base de datos SQL)",
+ "service_description_mysql": "Almacena los datos de la aplicación (base de datos SQL)",
"service_description_metronome": "Gestionar las cuentas XMPP de mensajería instantánea",
- "service_description_glances": "Supervisa la información del sistema en su servidor",
"service_description_fail2ban": "Protege contra ataques de fuerza bruta y otras clases de ataques desde Internet",
"service_description_dovecot": "Permite a los clientes de correo acceder/obtener correo (vía IMAP y POP3)",
"service_description_dnsmasq": "Maneja la resolución de nombres de dominio (DNS)",
@@ -377,13 +274,13 @@
"restore_removing_tmp_dir_failed": "No se pudo eliminar un directorio temporal antiguo",
"restore_not_enough_disk_space": "Espacio insuficiente (espacio: {free_space:d} B, espacio necesario: {needed_space:d} B, margen de seguridad: {margin:d} B)",
"restore_mounting_archive": "Montando archivo en «{path:s}»",
- "restore_may_be_not_enough_disk_space": "Parece que su sistema no tiene suficiente espacio (libre: {free_space:d} B, espacio necesario: {needed_space:d} B, margen de seguridad: {margin:d} B)",
+ "restore_may_be_not_enough_disk_space": "Parece que su sistema no tiene suficiente espacio libre (libre: {free_space:d} B, espacio necesario: {needed_space:d} B, margen de seguridad: {margin:d} B)",
"restore_extracting": "Extrayendo los archivos necesarios para el archivo…",
"regenconf_pending_applying": "Aplicando la configuración pendiente para la categoría «{category}»…",
"regenconf_failed": "No se pudo regenerar la configuración para la(s) categoría(s): {categories}",
"regenconf_dry_pending_applying": "Comprobando la configuración pendiente que habría sido aplicada para la categoría «{category}»…",
"regenconf_would_be_updated": "La configuración habría sido actualizada para la categoría «{category}»",
- "regenconf_updated": "Actualizada la configuración para la categoría «{category}»",
+ "regenconf_updated": "Actualizada la configuración para la categoría '{category}'",
"regenconf_up_to_date": "Ya está actualizada la configuración para la categoría «{category}»",
"regenconf_now_managed_by_yunohost": "El archivo de configuración «{conf}» está gestionado ahora por YunoHost (categoría {category}).",
"regenconf_file_updated": "Actualizado el archivo de configuración «{conf}»",
@@ -394,30 +291,25 @@
"regenconf_file_kept_back": "Se espera que el archivo de configuración «{conf}» sea eliminado por regen-conf (categoría {category}) pero ha sido retenido.",
"regenconf_file_copy_failed": "No se pudo copiar el nuevo archivo de configuración «{new}» a «{conf}»",
"regenconf_file_backed_up": "Archivo de configuración «{conf}» respaldado en «{backup}»",
- "remove_user_of_group_not_allowed": "No tiene permiso para eliminar al usuario «{user:s}» en el grupo «{group:s}»",
- "remove_main_permission_not_allowed": "No se permite eliminar el permiso principal",
- "recommend_to_add_first_user": "La posinstalación ha terminado pero YunoHost necesita al menos un usuario para funcionar correctamente, debe añadir uno ejecutando «yunohost user create » o usando la interfaz de administración.",
"permission_update_nothing_to_do": "No hay permisos para actualizar",
- "permission_updated": "Actualizado el permiso «{permission:s}» para la aplicación «{app:s}»",
+ "permission_updated": "Actualizado el permiso «{permission:s}»",
"permission_generated": "Actualizada la base de datos de permisos",
- "permission_update_failed": "No se pudo actualizar el permiso",
+ "permission_update_failed": "No se pudo actualizar el permiso '{permission}': {error}",
"permission_name_not_valid": "Elija un nombre de permiso permitido para «{permission:s}",
- "permission_not_found": "No se encontró el permiso «{permission:s}» para la aplicación «{app:s}»",
- "permission_deletion_failed": "Falta el permiso «{permission:s}» para eliminar la aplicación «{app:s}»",
- "permission_deleted": "Eliminado el permiso «{permission:s}» para la aplicación {app:s}",
- "permission_creation_failed": "No se pudo conceder el permiso",
- "permission_created": "Creado el permiso «{permission:s}» para la aplicación {app:s}",
- "permission_already_exist": "El permiso «{permission:s}» para la aplicación {app:s} ya existe",
- "permission_already_clear": "El permiso «{permission:s}» ya está definido para la aplicación {app:s}",
+ "permission_not_found": "No se encontró el permiso «{permission:s}»",
+ "permission_deletion_failed": "No se pudo eliminar el permiso «{permission}»: {error}",
+ "permission_deleted": "Eliminado el permiso «{permission:s}»",
+ "permission_creation_failed": "No se pudo crear el permiso «{permission}»: {error}",
+ "permission_created": "Creado el permiso «{permission:s}»",
+ "permission_already_exist": "El permiso «{permission}» ya existe",
"pattern_password_app": "Las contraseñas no pueden incluir los siguientes caracteres: {forbidden_chars}",
- "need_define_permission_before": "Redefina los permisos ejecutando «yunohost user permission add -u USUARIO» antes de eliminar un grupo permitido",
"migrations_to_be_ran_manually": "La migración {id} hay que ejecutarla manualmente. Vaya a Herramientas → Migraciones en la página web de administración o ejecute `yunohost tools migrations migrate`.",
"migrations_success_forward": "Migración {id} completada",
"migrations_skip_migration": "Omitiendo migración {id}…",
"migrations_running_forward": "Ejecutando migración {id}…",
"migrations_pending_cant_rerun": "Esas migraciones están aún pendientes, así que no se pueden volver a ejecutar: {ids}",
"migrations_not_pending_cant_skip": "Esas migraciones no están pendientes, así que no pueden ser omitidas: {ids}",
- "migrations_no_such_migration": "No hay ninguna migración llamada {id}",
+ "migrations_no_such_migration": "No hay ninguna migración llamada «{id}»",
"migrations_no_migrations_to_run": "No hay migraciones que ejecutar",
"migrations_need_to_accept_disclaimer": "Para ejecutar la migración {id} debe aceptar el siguiente descargo de responsabilidad:\n---\n{disclaimer}\n---\nSi acepta ejecutar la migración, vuelva a ejecutar la orden con la opción «--accept-disclaimer».",
"migrations_must_provide_explicit_targets": "Necesita proporcionar objetivos explícitos al usar «--skip» or «--force-rerun»",
@@ -426,47 +318,46 @@
"migrations_list_conflict_pending_done": "No puede usar «--previous» y «--done» al mismo tiempo.",
"migrations_exclusive_options": "«--auto», «--skip», and «--force-rerun» son opciones mutuamente excluyentes.",
"migrations_failed_to_load_migration": "No se pudo cargar la migración {id}: {error}",
- "migrations_dependencies_not_satisfied": "No se puede ejecutar la migración {id} porque primero necesita ejecutar estas migraciones: {dependencies_id}",
- "migrations_cant_reach_migration_file": "No se pudo acceder los archivos de migración en la ruta %s",
+ "migrations_dependencies_not_satisfied": "Ejecutar estas migraciones: «{dependencies_id}» antes de migrar {id}.",
+ "migrations_cant_reach_migration_file": "No se pudo acceder a los archivos de migración en la ruta «%s»",
"migrations_already_ran": "Esas migraciones ya se han realizado: {ids}",
"migration_0011_update_LDAP_schema": "Actualizando el esquema de LDAP…",
"migration_0011_update_LDAP_database": "Actualizando la base de datos de LDAP…",
"migration_0011_rollback_success": "Sistema revertido.",
- "migration_0011_migration_failed_trying_to_rollback": "Migración fallida… intentando revertir el sistema.",
+ "migration_0011_migration_failed_trying_to_rollback": "No se pudo migrar… intentando revertir el sistema.",
"migration_0011_migrate_permission": "Migrando permisos desde la configuración de las aplicaciones a LDAP…",
"migration_0011_LDAP_update_failed": "No se pudo actualizar LDAP. Error: {error:s}",
- "migration_0011_LDAP_config_dirty": "Parece que ha personalizado la configuración de LDAP. Para esta migración se necesita actualizar la configuración de LDAP.\nNecesita guardar su configuración actual, reiniciar la configuración original ejecutando «yunohost tools regen-conf -f» y reintentar la migración",
- "migration_0011_done": "Migración correcta. Ahora puede gestionar los grupos de usuarios.",
+ "migration_0011_done": "Migración finalizada. Ahora puede gestionar los grupos de usuarios.",
"migration_0011_create_group": "Creando un grupo para cada usuario…",
- "migration_0011_can_not_backup_before_migration": "No se pudo respaldar el sistema antes de la migración. Error: {error:s}",
+ "migration_0011_can_not_backup_before_migration": "El respaldo del sistema no se pudo completar antes de que la migración fallase. Error: {error:s}",
"migration_0011_backup_before_migration": "Creando un respaldo de la base de datos de LDAP y de la configuración de las aplicaciones antes de la migración real.",
"migration_0009_not_needed": "La migración ya ocurrió de algún modo… (?) Omitiendo.",
- "migration_0008_no_warning": "No se ha detectado ningún riesgo importante con respecto a la anulación de su configuración SSH ¡sin embargo uno nunca puede estar absolutamente seguro ;)! Ejecute la migración para anularla. Por otra parte, puede omitir la migración aunque no esté recomendado.",
- "migration_0008_warning": "Si entiende esos avisos y permite a YunoHost anular su configuración actual, ejecute la migración. Por otra parte puede omitir la migración, aunque no se recomienda.",
+ "migration_0008_no_warning": "Sobre escribir su configuración SSH debería ser seguro ¡aunque esto no se puede prometer! Ejecute la migración para ignorarla. Por otra parte puede omitir la migración, aunque no se recomienda.",
+ "migration_0008_warning": "Si entiende esos avisos y quiere que YunoHost ignore su configuración actual, ejecute la migración. Por otra parte puede omitir la migración, aunque no se recomienda.",
"migration_0008_dsa": "• Se desactivará la clave DSA. Así que podría tener que anular un aviso espeluznante de su cliente SSH y volver a comprobar la huella de su servidor;",
"migration_0008_root": "• No podrá conectarse como «root» a través de SSH. En su lugar debe usar el usuario «admin»;",
"migration_0008_port": "• Tendrá que conectarse usando el puerto 22 en vez de su actual puerto SSH personalizado. No dude en reconfigurarlo;",
"migration_0008_general_disclaimer": "Para mejorar la seguridad de su servidor, es recomendable permitir a YunoHost gestionar la configuración de SSH. Su actual configuración de SSH difiere de la recomendación. Si permite a YunoHost reconfigurarla, la manera en la que conecta con su servidor a través de SSH cambiará así:",
"migration_0007_cannot_restart": "No se puede reiniciar SSH después de intentar cancelar la migración número 6.",
- "migration_0007_cancelled": "YunoHost no ha podido mejorar el modo en el que se gestiona su configuración de SSH.",
- "migration_0006_disclaimer": "YunoHost espera ahora que las contraseñas de «admin» y «root» estén sincronizadas. Al ejecutar esta migración, su contraseña de «root» será reemplazada por la contraseña de administración.",
+ "migration_0007_cancelled": "No se pudo mejorar el modo en el que se gestiona su configuración de SSH.",
+ "migration_0006_disclaimer": "YunoHost espera ahora que las contraseñas de «admin» y «root» estén sincronizadas. Esta migración reemplaza su contraseña de «root» por la contraseña de «admin».",
"migration_0005_not_enough_space": "Tenga suficiente espacio libre disponible en {path} para ejecutar la migración.",
- "migration_0005_postgresql_96_not_installed": "⸘PostgreSQL 9.4 está instalado pero no PostgreSQL 9.6‽ Algo raro podría haber ocurrido en su sistema:(…",
+ "migration_0005_postgresql_96_not_installed": "PostgreSQL 9.4 está instalado pero no PostgreSQL 9.6. Algo raro podría haber ocurrido en su sistema:(…",
"migration_0005_postgresql_94_not_installed": "PostgreSQL no estaba instalado en su sistema. Nada que hacer.",
"migration_0003_modified_files": "Tenga en cuenta que se encontró que los siguientes archivos fueron modificados manualmente y podrían ser sobrescritos después de la actualización: {manually_modified_files}",
- "migration_0003_problematic_apps_warning": "Tenga en cuenta que se detectaron las siguientes aplicaciones instaladas posiblemente problemáticas. Parece que no fueron instaladas desde una lista de aplicaciones o no estaban etiquetadas como «funciona». Así que no hay garantía de que aún funcionen después de la actualización: {problematic_apps}",
+ "migration_0003_problematic_apps_warning": "Tenga en cuenta que las aplicaciones listadas mas abajo fueron detectadas como 'posiblemente problemáticas'. Parece que no fueron instaladas desde una lista de aplicaciones o no estaban etiquetadas como 'funcional'. Así que no hay garantía de que aún funcionen después de la actualización: {problematic_apps}",
"migration_0003_general_warning": "Tenga en cuenta que esta migración es una operación delicada. El equipo de YunoHost ha hecho todo lo posible para revisarla y probarla, pero la migración aún podría romper parte del sistema o de sus aplicaciones.\n\nPor lo tanto, se recomienda que:\n - Realice una copia de seguridad de cualquier dato crítico o aplicación. Más información en https://yunohost.org/backup;\n - Tenga paciencia tras iniciar la migración: dependiendo de su conexión a Internet y de su hardware, podría tardar unas cuantas horas hasta que todo se actualice.\n\nAdemás, el puerto para SMTP usado por los clientes de correo externos (como Thunderbird o K9-Mail) cambió de 465 (SSL/TLS) a 587 (STARTTLS). El antiguo puerto (465) se cerrará automáticamente y el nuevo puerto (587) se abrirá en el cortafuegos. Todos los usuarios *tendrán* que adaptar la configuración de sus clientes de correo por lo tanto.",
"migration_0003_still_on_jessie_after_main_upgrade": "Algo fue mal durante la actualización principal: ⸘el sistema está aún en Jessie‽ Para investigar el problema, vea {log}:s…",
"migration_0003_system_not_fully_up_to_date": "Su sistema no está totalmente actualizado. Realice una actualización normal antes de ejecutar la migración a Stretch.",
"migration_0003_not_jessie": "¡La distribución de Debian actual no es Jessie!",
- "migration_0003_yunohost_upgrade": "Iniciando la actualización del paquete YunoHost… La migración finalizará pero la actualización real ocurrirá inmediatamente después. Después de que la operación esté completada, podría tener que iniciar sesión en la página de administración de nuevo.",
- "migration_0003_restoring_origin_nginx_conf": "Su archivo /etc/nginx/nginx.conf ha sido editado de algún modo. La migración lo devolverá a su estado original primero… El archivo anterior estará disponible como {backup_dest}.",
+ "migration_0003_yunohost_upgrade": "Iniciando la actualización del paquete YunoHost… la actualización ocurrirá inmediatamente después de que la migración finalizará. Después de que la operación esté completada, podría tener que iniciar sesión en la página de administración de nuevo.",
+ "migration_0003_restoring_origin_nginx_conf": "Su archivo /etc/nginx/nginx.conf ha sido editado. La migración lo devolverá a su estado original… El archivo anterior estará disponible como {backup_dest}.",
"migration_0003_fail2ban_upgrade": "Iniciando la actualización de Fail2Ban…",
"migration_0003_main_upgrade": "Iniciando la actualización principal…",
"migration_0003_patching_sources_list": "Corrigiendo «sources.lists»…",
"migration_0003_start": "Iniciando migración a Stretch. El registro estará disponible en {logfile}.",
"migration_description_0012_postgresql_password_to_md5_authentication": "Forzar a la autentificación de PostgreSQL a usar MD5 para las conexiones locales",
- "migration_description_0011_setup_group_permission": "Configurar grupo de usuario y configurar permisos para aplicaciones y servicios",
+ "migration_description_0011_setup_group_permission": "Configurar grupo de usuario y permisos para aplicaciones y servicios",
"migration_description_0010_migrate_to_apps_json": "Eliminar las listas de aplicaciones («appslists») obsoletas y usar en su lugar la nueva lista unificada «apps.json»",
"migration_description_0009_decouple_regenconf_from_services": "Separar el mecanismo «regen-conf» de los servicios",
"migration_description_0008_ssh_conf_managed_by_yunohost_step2": "Permitir que la configuración de SSH la gestione YunoHost (paso 2, manual)",
@@ -491,23 +382,16 @@
"log_tools_shutdown": "Apagar el servidor",
"log_tools_upgrade": "Actualizar paquetes del sistema",
"log_tools_postinstall": "Posinstalación del servidor YunoHost",
- "log_tools_migrations_migrate_forward": "Migrar hacia adelante",
- "log_tools_maindomain": "Convertir «{}» en el dominio principal",
- "log_user_permission_remove": "Actualizar permiso «{}»",
- "log_user_permission_add": "Actualizar permiso «{}»",
+ "log_tools_migrations_migrate_forward": "Inicializa la migración",
"log_user_update": "Actualizar la información de usuario de «{}»",
"log_user_group_update": "Actualizar grupo «{}»",
"log_user_group_delete": "Eliminar grupo «{}»",
- "log_user_group_add": "Añadir grupo «{}»",
"log_user_delete": "Eliminar usuario «{}»",
"log_user_create": "Añadir usuario «{}»",
"log_regen_conf": "Regenerar la configuración del sistema «{}»",
- "log_letsencrypt_cert_renew": "Renovar el certificado «{}» de Let's encrypt",
- "log_selfsigned_cert_install": "Instalar certificado autofirmado en el dominio «{}»",
- "log_permission_update": "Actualizar permiso «{}» para la aplicación «{}»",
- "log_permission_remove": "Eliminar permiso «{}»",
- "log_permission_add": "Añadir el permiso «{}» para la aplicación «{}»",
- "log_letsencrypt_cert_install": "Instalar un certificado de Let's encrypt en el dominio «{}»",
+ "log_letsencrypt_cert_renew": "Renovar el certificado «{}» de Let's Encrypt",
+ "log_selfsigned_cert_install": "Instalar el certificado auto-firmado en el dominio '{}'",
+ "log_letsencrypt_cert_install": "Instalar un certificado de Let's Encrypt en el dominio «{}»",
"log_dyndns_update": "Actualizar la IP asociada con su subdominio de YunoHost «{}»",
"log_dyndns_subscribe": "Subscribirse a un subdomino de YunoHost «{}»",
"log_domain_remove": "Eliminar el dominio «{}» de la configuración del sistema",
@@ -522,13 +406,8 @@
"log_app_remove": "Eliminar la aplicación «{}»",
"log_app_install": "Instalar la aplicación «{}»",
"log_app_change_url": "Cambiar el URL de la aplicación «{}»",
- "log_app_removelist": "Eliminar una lista de aplicaciones",
- "log_app_fetchlist": "Añadir una lista de aplicaciones",
- "log_app_clearaccess": "Eliminar todos los accesos a «{}»",
- "log_app_removeaccess": "Eliminar acceso a «{}»",
- "log_app_addaccess": "Añadir acceso a «{}»",
"log_operation_unit_unclosed_properly": "La unidad de operación no se ha cerrado correctamente",
- "log_does_exists": "No existe ningún registro de actividades con el nombre «{log}», ejecute «yunohost log list» para ver todos los registros de actividades disponibles",
+ "log_does_exists": "No existe ningún registro de actividades con el nombre '{log}', ejecute 'yunohost log list' para ver todos los registros de actividades disponibles",
"log_help_to_get_failed_log": "No se pudo completar la operación «{desc}». Para obtener ayuda, comparta el registro completo de esta operación ejecutando la orden «yunohost log display {name} --share»",
"log_link_to_failed_log": "No se pudo completar la operación «{desc}». Para obtener ayuda, proporcione el registro completo de esta operación pulsando aquí",
"log_help_to_get_log": "Para ver el registro de la operación «{desc}», ejecute la orden «yunohost log display {name}»",
@@ -536,18 +415,13 @@
"log_category_404": "La categoría de registro «{category}» no existe",
"log_corrupted_md_file": "El archivo de metadatos YAML asociado con el registro está dañado: «{md_file}\nError: {error}»",
"hook_json_return_error": "No se pudo leer la respuesta del gancho {path:s}. Error: {msg:s}. Contenido sin procesar: {raw_content}",
- "group_update_failed": "No se pudo actualizar el grupo «{group}»",
+ "group_update_failed": "No se pudo actualizar el grupo «{group}»: {error}",
"group_updated": "Grupo «{group}» actualizado",
"group_unknown": "El grupo «{group:s}» es desconocido",
- "group_info_failed": "No se pudo mostrar la información del grupo",
- "group_deletion_not_allowed": "No se puede eliminar el grupo {group:s} manualmente.",
- "group_deletion_failed": "No se pudo eliminar el grupo «{group}»",
+ "group_deletion_failed": "No se pudo eliminar el grupo «{group}»: {error}",
"group_deleted": "Eliminado el grupo «{group}»",
- "group_creation_failed": "No se pudo crear el grupo «{group}»",
+ "group_creation_failed": "No se pudo crear el grupo «{group}»: {error}",
"group_created": "Creado el grupo «{group}»",
- "group_name_already_exist": "El grupo {name:s} ya existe",
- "group_already_disallowed": "El grupo «{group:s}» ya tiene desactivado el permiso «{permission:s}» para la aplicación «{app:s}»",
- "group_already_allowed": "El grupo «{group:s}» ya tiene activado el permiso «{permission:s}» para la aplicación «{app:s}»",
"good_practices_about_admin_password": "Va a establecer una nueva contraseña de administración. La contraseña debería tener al menos 8 caracteres, aunque es una buena práctica usar una contraseña más extensa (básicamente una frase) y/o usar caracteres de varias clases (mayúsculas, minúsculas, números y caracteres especiales).",
"global_settings_unknown_type": "Situación imprevista, la configuración {setting:s} parece tener el tipo {unknown_type:s} pero no es un tipo compatible con el sistema.",
"global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "Permitir el uso de la llave (obsoleta) DSA para la configuración del demonio SSH",
@@ -569,20 +443,16 @@
"global_settings_bad_type_for_setting": "Tipo erróneo para la configuración {setting:s}, obtuvo {received_type:s}, esperado {expected_type:s}",
"global_settings_bad_choice_for_enum": "Opción errónea para la configuración {setting:s}, obtuvo «{choice:s}» pero las opciones disponibles son: {available_choices:s}",
"file_does_not_exist": "El archivo {path:s} no existe.",
- "error_when_removing_sftpuser_group": "No se pudo eliminar el grupo sftpusers",
- "edit_permission_with_group_all_users_not_allowed": "No puede editar el permiso para el grupo «all_users», utilice «yunohost user permission clear APLICACIÓN» o «yunohost user permission add APLICACIÓN -u USUARIO».",
- "edit_group_not_allowed": "No tiene permiso para editar el grupo {group:s}",
"dyndns_could_not_check_available": "No se pudo comprobar si {domain:s} está disponible en {provider:s}.",
- "domain_dyndns_dynette_is_unreachable": "No se pudo conectar a dynette de YunoHost. O su YunoHost no está correctamente conectado a Internet o el servidor dynette está caído. Error: {error}",
"domain_dns_conf_is_just_a_recommendation": "Esta orden muestra la configuración *recomendada*. No configura el DNS en realidad. Es su responsabilidad configurar la zona de DNS en su registrador según esta recomendación.",
- "dpkg_lock_not_available": "Esta orden no se puede ejecutar en este momento porque otro programa parece que está usando el bloqueo de dpkg (el gestor de paquetes del sistema)",
+ "dpkg_lock_not_available": "Esta orden no se puede ejecutar en este momento ,parece que programa está usando el bloqueo de dpkg (el gestor de paquetes del sistema)",
"dpkg_is_broken": "No puede hacer esto en este momento porque dpkg/apt (los gestores de paquetes del sistema) parecen estar en un estado roto... Puede tratar de solucionar este problema conectando a través de SSH y ejecutando `sudo dpkg --configure -a`.",
- "confirm_app_install_thirdparty": "¡AVISO! Instalar aplicaciones de terceros podría comprometer la integridad y seguridad de su sistema. Probablemente NO debería instalarlas salvo que sepa lo que está haciendo. ¿Está dispuesto a correr ese riesgo? [{answers:s}] ",
- "confirm_app_install_danger": "¡AVISO! Esta aplicación es aún experimental (si no está funcionando expresamente) y ¡es probable que rompa su sistema! Probablemente NO debería instalarla salvo que sepa lo que está haciendo. ¿Está dispuesto a correr ese riesgo? [{answers:s}] ",
+ "confirm_app_install_thirdparty": "¡PELIGRO! Esta aplicación no forma parte del catálogo de aplicaciones de YunoHost. Instalar aplicaciones de terceros podría comprometer la integridad y seguridad de su sistema. Probablemente NO debería instalarla salvo que sepa lo que está haciendo. No tendrá NINGUNA AYUDA si esta aplicación no funciona o rompe su sistema… Si está dispuesto a aceptar ese riesgo de todas formas, escriba «{answers:s}»",
+ "confirm_app_install_danger": "¡PELIGRO! ¡Esta aplicación es conocida por ser aún experimental (o no funciona explícitamente)! Probablemente NO debería instalarla salvo que sepa lo que está haciendo. No tendrá NINGUNA AYUDA si esta aplicación no funciona o rompe su sistema… Si está dispuesto a aceptar ese riesgo de todas formas, escriba «{answers:s}»",
"confirm_app_install_warning": "Aviso: esta aplicación puede funcionar pero no está bien integrada en YunoHost. Algunas herramientas como la autentificación única y respaldo/restauración podrían no estar disponibles. ¿Instalar de todos modos? [{answers:s}] ",
"backup_unable_to_organize_files": "No se pudo usar el método rápido de organización de los archivos en el archivo",
"backup_permission": "Permiso de respaldo para la aplicación {app:s}",
- "backup_output_symlink_dir_broken": "Tiene un enlace simbólico roto en vez del directorio «{path:s}» de su archivo. Puede que tenga una configuración específica para respaldar sus datos en otro sistema de archivos, en este caso probablemente olvidó remontar o conectar su disco duro o clave USB.",
+ "backup_output_symlink_dir_broken": "El directorio de su archivo «{path:s}» es un enlace simbólico roto. Tal vez olvidó (re)montarlo o conectarlo al medio de almacenamiento al que apunta.",
"backup_mount_archive_for_restore": "Preparando el archivo para la restauración…",
"backup_method_tar_finished": "Creado el archivo TAR de respaldo",
"backup_method_custom_finished": "Terminado el método «{method:s}» de respaldo personalizado",
@@ -592,19 +462,141 @@
"backup_actually_backuping": "Creando un archivo de respaldo de los archivos obtenidos…",
"ask_new_path": "Nueva ruta",
"ask_new_domain": "Nuevo dominio",
- "apps_permission_restoration_failed": "Otorgar el permiso «{permission:s}» para restaurar {app:s}",
- "apps_permission_not_found": "No se han encontrado permisos para las aplicaciones instaladas",
"app_upgrade_several_apps": "Las siguientes aplicaciones se actualizarán: {apps}",
- "app_start_restore": "Restaurando aplicación {app}…",
- "app_start_backup": "Obteniendo archivos para el respaldo de {app}…",
- "app_start_remove": "Eliminando aplicación {app}…",
- "app_start_install": "Instalando aplicación {app}…",
+ "app_start_restore": "Restaurando aplicación «{app}»…",
+ "app_start_backup": "Obteniendo archivos para el respaldo de «{app}»…",
+ "app_start_remove": "Eliminando aplicación «{app}»…",
+ "app_start_install": "Instalando aplicación «{app}»…",
"app_not_upgraded": "Error al actualizar la aplicación «{failed_app}» y como consecuencia se han cancelado las actualizaciones de las siguientes aplicaciones: {apps}",
- "app_action_cannot_be_ran_because_required_services_down": "Esta aplicación necesita algunos servicios que no están funcionando ahora. Antes de continuar, debería intentar reiniciar los siguientes servicios (y posiblemente investigar por qué no funcionan): {services}",
+ "app_action_cannot_be_ran_because_required_services_down": "Estos servicios necesarios deberían estar funcionando para ejecutar esta acción: {services}. Pruebe a reiniciarlos para continuar (y posiblemente investigar por qué están caídos).",
"already_up_to_date": "Nada que hacer. Todo está actualizado.",
"admin_password_too_long": "Elija una contraseña de menos de 127 caracteres",
"aborting": "Cancelando.",
- "app_upgrade_stopped": "Se ha detenido la actualización de todas las aplicaciones para prevenir un posible daño porque la aplicación anterior no se pudo actualizar",
- "app_action_broke_system": "Esta acción parece que ha roto estos importantes servicios: {services}",
- "operation_interrupted": "¿Ha sido interrumpida la operación manualmente?"
+ "app_action_broke_system": "Esta acción parece que ha roto estos servicios importantes: {services}",
+ "operation_interrupted": "¿Ha sido interrumpida la operación manualmente?",
+ "apps_already_up_to_date": "Todas las aplicaciones están ya actualizadas",
+ "dyndns_provider_unreachable": "No se puede conectar con el proveedor de Dyndns {provider}: o su YunoHost no está correctamente conectado a Internet o el servidor dynette está caído.",
+ "group_already_exist": "El grupo {group} ya existe",
+ "group_already_exist_on_system": "El grupo {group} ya existe en los grupos del sistema",
+ "group_cannot_be_deleted": "El grupo {group} no se puede eliminar manualmente.",
+ "group_user_already_in_group": "El usuario {user} ya está en el grupo {group}",
+ "group_user_not_in_group": "El usuario {user} no está en el grupo {group}",
+ "log_permission_create": "Crear permiso «{}»",
+ "log_permission_delete": "Eliminar permiso «{}»",
+ "log_user_group_create": "Crear grupo «{}»",
+ "log_user_permission_update": "Actualizar los accesos para el permiso «{}»",
+ "log_user_permission_reset": "Restablecer permiso «{}»",
+ "migration_0011_failed_to_remove_stale_object": "No se pudo eliminar el objeto obsoleto {dn}: {error}",
+ "permission_already_allowed": "El grupo «{group}» ya tiene el permiso «{permission}» activado",
+ "permission_already_disallowed": "El grupo «{group}» ya tiene el permiso «{permission}» desactivado",
+ "permission_cannot_remove_main": "No está permitido eliminar un permiso principal",
+ "user_already_exists": "El usuario «{user}» ya existe",
+ "app_full_domain_unavailable": "Lamentablemente esta aplicación tiene que instalarse en un dominio propio pero ya hay otras aplicaciones instaladas en el dominio «{domain}». Podría usar un subdomino dedicado a esta aplicación en su lugar.",
+ "app_install_failed": "No se pudo instalar {app}: {error}",
+ "app_install_script_failed": "Ha ocurrido un error en el guión de instalación de la aplicación",
+ "group_cannot_edit_all_users": "El grupo «all_users» no se puede editar manualmente. Es un grupo especial destinado a contener todos los usuarios registrados en YunoHost",
+ "group_cannot_edit_visitors": "El grupo «visitors» no se puede editar manualmente. Es un grupo especial que representa a los visitantes anónimos",
+ "group_cannot_edit_primary_group": "El grupo «{group}» no se puede editar manualmente. Es el grupo primario destinado a contener solo un usuario específico.",
+ "log_permission_url": "Actualizar la URL relacionada con el permiso «{}»",
+ "migration_0011_slapd_config_will_be_overwritten": "Parece que ha editado manualmente la configuración de slapd. Para esta migración crítica, YunoHost necesita forzar la actualización de la configuración de slapd. Los archivos originales se respaldarán en {conf_backup_folder}.",
+ "permission_already_up_to_date": "El permiso no se ha actualizado porque las peticiones de incorporación o eliminación ya coinciden con el estado actual.",
+ "permission_currently_allowed_for_all_users": "Este permiso se concede actualmente a todos los usuarios además de los otros grupos. Probablemente quiere o eliminar el permiso de «all_users» o eliminar los otros grupos a los que está otorgado actualmente.",
+ "permission_require_account": "El permiso {permission} solo tiene sentido para usuarios con una cuenta y, por lo tanto, no se puede activar para visitantes.",
+ "app_remove_after_failed_install": "Eliminando la aplicación tras el fallo de instalación…",
+ "diagnosis_basesystem_host": "El servidor está ejecutando Debian {debian_version}.",
+ "diagnosis_basesystem_kernel": "El servidor está ejecutando el núcleo de Linux {kernel_version}",
+ "diagnosis_basesystem_ynh_single_version": "{package} versión: {version} ({repo})",
+ "diagnosis_basesystem_ynh_main_version": "El servidor está ejecutando YunoHost {main_version} ({repo})",
+ "diagnosis_basesystem_ynh_inconsistent_versions": "Está ejecutando versiones incoherentes de los paquetes de YunoHost... probablemente por una actualización errónea o parcial.",
+ "diagnosis_failed_for_category": "Diagnóstico fallido para la categoría «{category}» : {error}",
+ "diagnosis_cache_still_valid": "(Caché aún válida para el diagnóstico de {category}. ¡Aún no se ha rediagnosticado!)",
+ "diagnosis_found_errors_and_warnings": "¡Encontrado(s) error(es) significativo(s) {errors} (y aviso(s) {warnings}) relacionado(s) con {category}!",
+ "diagnosis_display_tip_web": "Puede ir a la sección de diagnóstico (en la pantalla principal) para ver los problemas encontrados.",
+ "diagnosis_display_tip_cli": "Puede ejecutar «yunohost diagnosis show --issues» para mostrar los problemas encontrados.",
+ "apps_catalog_init_success": "¡Sistema de catálogo de aplicaciones inicializado!",
+ "apps_catalog_updating": "Actualizando catálogo de aplicaciones...",
+ "apps_catalog_failed_to_download": "No se pudo descargar el catálogo de aplicaciones {apps_catalog}: {error}",
+ "apps_catalog_obsolete_cache": "La caché del catálogo de aplicaciones está vacía u obsoleta.",
+ "apps_catalog_update_success": "¡El catálogo de aplicaciones ha sido actualizado!",
+ "diagnosis_cant_run_because_of_dep": "No se puede ejecutar el diagnóstico para {category} mientras haya problemas importantes relacionados con {dep}.",
+ "diagnosis_ignored_issues": "(+ {nb_ignored} problema(s) ignorado(s))",
+ "diagnosis_found_errors": "¡Encontrado(s) error(es) significativo(s) {errors} relacionado(s) con {category}!",
+ "diagnosis_found_warnings": "Encontrado elemento(s) {warnings} que puede(n) ser mejorado(s) para {category}.",
+ "diagnosis_everything_ok": "¡Todo se ve bien para {category}!",
+ "app_upgrade_script_failed": "Ha ocurrido un error en el script de actualización de la app",
+ "diagnosis_no_cache": "Todavía no hay una caché de diagnóstico para la categoría '{category}'",
+ "diagnosis_ip_no_ipv4": "El servidor no cuenta con ipv4 funcional.",
+ "diagnosis_ip_not_connected_at_all": "¿¡Está conectado el servidor a internet!?",
+ "diagnosis_ip_broken_resolvconf": "DNS parece no funcionar en tu servidor, lo que parece estar relacionado con /etc/resolv.conf no apuntando a 127.0.0.1.",
+ "diagnosis_dns_missing_record": "Según la configuración DNS recomendada, deberías añadir un registro DNS\ntipo: {type}\nnombre: {name}\nvalor: {value}",
+ "diagnosis_diskusage_low": "El almacenamiento {mountpoint} (en dispositivo {device}) solo tiene {free} ({free_percent}%) de espacio disponible. Ten cuidado.",
+ "diagnosis_services_bad_status_tip": "Puedes intentar reiniciar el servicio, y si no funciona, echar un vistazo a los logs del servicio usando 'yunohost service log {service}' o a través de la sección 'Servicios' en webadmin.",
+ "diagnosis_ip_connected_ipv6": "¡El servidor está conectado a internet a través de IPv6!",
+ "diagnosis_ip_no_ipv6": "El servidor no cuenta con IPv6 funcional.",
+ "diagnosis_ip_dnsresolution_working": "¡DNS no está funcionando!",
+ "diagnosis_ip_broken_dnsresolution": "Parece que no funciona la resolución de nombre de dominio por alguna razón... ¿Hay algún firewall bloqueando peticiones DNS?",
+ "diagnosis_ip_weird_resolvconf": "Parece que DNS funciona, pero ten cuidado, porque estás utilizando /etc/resolv.conf modificado.",
+ "diagnosis_ip_weird_resolvconf_details": "En su lugar, este fichero debería ser un enlace simbólico a /etc/resolvconf/run/resolv.conf apuntando a 127.0.0.1 (dnsmasq). Los servidores de nombre de domino deben configurarse a través de /etc/resolv.dnsmasq.conf.",
+ "diagnosis_dns_good_conf": "Buena configuración DNS para el dominio {domain} (categoría {category})",
+ "diagnosis_dns_bad_conf": "Configuración mala o faltante de los DNS para el dominio {domain} (categoría {category})",
+ "diagnosis_dns_discrepancy": "El registro DNS con tipo {type} y nombre {name} no se corresponde a la configuración recomendada.\nValor actual: {current}\nValor esperado: {value}",
+ "diagnosis_services_bad_status": "El servicio {service} está {status} :(",
+ "diagnosis_diskusage_verylow": "El almacenamiento {mountpoint} (en el dispositivo {device}) sólo tiene {free} ({free_percent}%) de espacio disponible. Deberías considerar la posibilidad de limpiar algo de espacio.",
+ "diagnosis_diskusage_ok": "¡El almacenamiento {mountpoint} (en el dispositivo {device}) todavía tiene {free} ({free_percent}%) de espacio libre!",
+ "diagnosis_services_conf_broken": "¡Mala configuración para el servicio {service}!",
+ "diagnosis_services_running": "¡El servicio {service} está en ejecución!",
+ "diagnosis_failed": "No se ha podido obtener el resultado del diagnóstico para la categoría '{category}': {error}",
+ "diagnosis_ip_connected_ipv4": "¡El servidor está conectado a internet a través de IPv4!",
+ "diagnosis_security_vulnerable_to_meltdown_details": "Para corregir esto, debieras actualizar y reiniciar tu sistema para cargar el nuevo kernel de Linux (o contacta tu proveedor si esto no funciona). Mas información en https://meltdownattack.com/",
+ "diagnosis_ram_verylow": "Al sistema le queda solamente {available} ({available_percent}%) de RAM! (De un total de {total})",
+ "diagnosis_ram_low": "Al sistema le queda {available} ({available_percent}%) de RAM de un total de {total}. Cuidado.",
+ "diagnosis_ram_ok": "El sistema aun tiene {available} ({available_percent}%) de RAM de un total de {total}.",
+ "diagnosis_swap_none": "El sistema no tiene mas espacio de intercambio. Considera agregar por lo menos {recommended} de espacio de intercambio para evitar que el sistema se quede sin memoria.",
+ "diagnosis_swap_notsomuch": "Al sistema le queda solamente {total} de espacio de intercambio. Considera agregar al menos {recommended} para evitar que el sistema se quede sin memoria.",
+ "diagnosis_mail_ougoing_port_25_ok": "El puerto de salida 25 no esta bloqueado y los correos electrónicos pueden ser enviados a otros servidores.",
+ "diagnosis_mail_outgoing_port_25_blocked": "El puerto de salida 25 parece estar bloqueado. Intenta desbloquearlo con el panel de configuración de tu proveedor de servicios de Internet (o proveedor de halbergue). Mientras tanto, el servidor no podrá enviar correos electrónicos a otros servidores.",
+ "diagnosis_regenconf_allgood": "Todos los archivos de configuración están en linea con la configuración recomendada!",
+ "diagnosis_regenconf_manually_modified": "El archivo de configuración {file} fue modificado manualmente.",
+ "diagnosis_regenconf_manually_modified_details": "Esto este probablemente BIEN siempre y cuando sepas lo que estas haciendo ;) !",
+ "diagnosis_regenconf_manually_modified_debian": "El archivos de configuración {file} fue modificado manualmente comparado con el valor predeterminado de Debian.",
+ "diagnosis_regenconf_manually_modified_debian_details": "Esto este probablemente BIEN, pero igual no lo pierdas de vista...",
+ "diagnosis_security_all_good": "Ninguna vulnerabilidad critica de seguridad fue encontrada.",
+ "diagnosis_security_vulnerable_to_meltdown": "Pareces vulnerable a el colapso de vulnerabilidad critica de seguridad.",
+ "diagnosis_description_basesystem": "Sistema de base",
+ "diagnosis_description_ip": "Conectividad a Internet",
+ "diagnosis_description_dnsrecords": "Registro DNS",
+ "diagnosis_description_services": "Comprobación del estado de los servicios",
+ "diagnosis_description_ports": "Exposición de puertos",
+ "diagnosis_description_systemresources": "Recursos del sistema",
+ "diagnosis_swap_ok": "El sistema tiene {total} de espacio de intercambio!",
+ "diagnosis_ports_needed_by": "La apertura de este puerto es requerida para la funcionalidad {category} (service {service})",
+ "diagnosis_ports_ok": "El puerto {port} es accesible desde internet.",
+ "diagnosis_ports_unreachable": "El puerto {port} no es accesible desde internet.",
+ "diagnosis_ports_could_not_diagnose": "No se puede comprobar si los puertos están accesibles desde el exterior.",
+ "diagnosis_ports_could_not_diagnose_details": "Error: {error}",
+ "diagnosis_description_security": "Validación de seguridad",
+ "diagnosis_description_regenconf": "Configuraciones de sistema",
+ "diagnosis_description_mail": "Correo electrónico",
+ "diagnosis_description_web": "Web",
+ "diagnosis_basesystem_hardware_board": "El modelo de placa del servidor es {model}",
+ "diagnosis_basesystem_hardware": "La arquitectura material del servidor es {virt} {arch}",
+ "migration_description_0014_remove_app_status_json": "Supresión del archivo de aplicaciones heredado status.json",
+ "migration_description_0013_futureproof_apps_catalog_system": "Migración hacia el nuevo sistema de catalogo de aplicación a prueba del futuro",
+ "log_domain_main_domain": "Hacer de '{}' el dominio principal",
+ "log_app_config_apply": "Aplica la configuración de la aplicación '{}'",
+ "log_app_config_show_panel": "Muestra el panel de configuración de la aplicación '{}'",
+ "log_app_action_run": "Inicializa la acción de la aplicación '{}'",
+ "group_already_exist_on_system_but_removing_it": "El grupo {group} ya existe en el grupo de sistema, pero YunoHost lo suprimirá …",
+ "global_settings_setting_pop3_enabled": "Habilita el protocolo POP3 para el servidor de correo electrónico",
+ "domain_cannot_remove_main_add_new_one": "No se puede remover '{domain:s}' porque es su principal y único dominio. Primero debe agregar un nuevo dominio con la linea de comando 'yunohost domain add ', entonces configurarlo como dominio principal con 'yunohost domain main-domain -n ' y finalmente borrar el dominio '{domain:s}' con 'yunohost domain remove {domain:s}'.",
+ "diagnosis_never_ran_yet": "Este servidor todavía no tiene reportes de diagnostico. Puede iniciar un diagnostico completo desde la interface administrador web o con la linea de comando 'yunohost diagnosis run'.",
+ "diagnosis_unknown_categories": "Las siguientes categorías están desconocidas: {categories}",
+ "diagnosis_http_unreachable": "El dominio {domain} esta fuera de alcance desde internet y a través de HTTP.",
+ "diagnosis_http_bad_status_code": "El sistema de diagnostico no pudo comunicarse con su servidor. Puede ser otra maquina que contesto en lugar del servidor. Debería verificar en su firewall que el re-direccionamiento del puerto 80 esta correcto.",
+ "diagnosis_http_connection_error": "Error de conexión: Ne se pudo conectar al dominio solicitado,",
+ "diagnosis_http_timeout": "El intento de contactar a su servidor desde internet corrió fuera de tiempo. Al parece esta incomunicado. Debería verificar que nginx corre en el puerto 80, y que la redireción del puerto 80 no interfiere con en el firewall.",
+ "diagnosis_http_ok": "El Dominio {domain} es accesible desde internet a través de HTTP.",
+ "diagnosis_http_could_not_diagnose": "No se pudo verificar si el dominio es accesible desde internet.",
+ "diagnosis_http_could_not_diagnose_details": "Error: {error}",
+ "diagnosis_ports_forwarding_tip": "Para solucionar este incidente, debería configurar el \"port forwading\" en su router como especificado en https://yunohost.org/isp_box_config"
}
diff --git a/locales/eu.json b/locales/eu.json
index 0967ef424..1891e00a3 100644
--- a/locales/eu.json
+++ b/locales/eu.json
@@ -1 +1,3 @@
-{}
+{
+ "password_too_simple_1": "Pasahitzak gutxienez 8 karaktere izan behar ditu"
+}
\ No newline at end of file
diff --git a/locales/fr.json b/locales/fr.json
index c94406a07..80d9f07dd 100644
--- a/locales/fr.json
+++ b/locales/fr.json
@@ -6,97 +6,67 @@
"app_already_installed": "{app:s} est déjà installé",
"app_argument_choice_invalid": "Choix invalide pour le paramètre '{name:s}', il doit être l’un de {choices:s}",
"app_argument_invalid": "Valeur invalide pour le paramètre '{name:s}' : {error:s}",
- "app_argument_missing": "Paramètre manquant « {:s} »",
"app_argument_required": "Le paramètre '{name:s}' est requis",
"app_extraction_failed": "Impossible d’extraire les fichiers d’installation",
"app_id_invalid": "Identifiant d’application invalide",
- "app_incompatible": "L’application {app} est incompatible avec votre version de YunoHost",
"app_install_files_invalid": "Fichiers d’installation incorrects",
- "app_location_already_used": "L’application '{app}' est déjà installée à cet emplacement ({path})",
- "app_location_install_failed": "Impossible d’installer l’application à cet emplacement pour cause de conflit avec l’application '{other_app}' déjà installée sur '{other_path}'",
"app_manifest_invalid": "Manifeste d’application incorrect : {error}",
- "app_no_upgrade": "Aucune application à mettre à jour",
"app_not_correctly_installed": "{app:s} semble être mal installé",
- "app_not_installed": "L'application « {app:s} » n’est pas installée. Voici la liste des applications installées: {all_apps}",
+ "app_not_installed": "Nous n’avons pas trouvé l’application « {app:s} » dans la liste des applications installées: {all_apps}",
"app_not_properly_removed": "{app:s} n’a pas été supprimé correctement",
- "app_package_need_update": "Le paquet de l’application {app} doit être mis à jour pour être en adéquation avec les changements de YunoHost",
- "app_recent_version_required": "{app:s} nécessite une version plus récente de YunoHost",
- "app_removed": "{app:s} a été supprimé",
+ "app_removed": "{app:s} supprimé",
"app_requirements_checking": "Vérification des paquets requis pour {app} …",
- "app_requirements_failed": "Impossible de satisfaire les pré-requis pour {app} : {error}",
"app_requirements_unmeet": "Les pré-requis de {app} ne sont pas satisfaits, le paquet {pkgname} ({version}) doit être {spec}",
- "app_sources_fetch_failed": "Impossible de récupérer les fichiers sources, l'URL est-elle correcte ?",
+ "app_sources_fetch_failed": "Impossible de récupérer les fichiers sources, l’URL est-elle correcte ?",
"app_unknown": "Application inconnue",
- "app_unsupported_remote_type": "Ce type de commande à distance utilisé pour cette application n'est pas supporté",
- "app_upgrade_failed": "Impossible de mettre à jour {app:s}",
- "app_upgraded": "{app:s} a été mis à jour",
- "appslist_fetched": "La liste d’applications {appslist:s} a été récupérée",
- "appslist_removed": "La liste d’applications {appslist:s} a été supprimée",
- "appslist_retrieve_error": "Impossible de récupérer la liste d’applications distante {appslist:s} : {error:s}",
- "appslist_unknown": "La liste d’applications {appslist:s} est inconnue.",
- "ask_current_admin_password": "Mot de passe d’administration actuel",
+ "app_unsupported_remote_type": "Ce type de commande à distance utilisé pour cette application n’est pas supporté",
+ "app_upgrade_failed": "Impossible de mettre à jour {app:s} : {error}",
+ "app_upgraded": "{app:s} mis à jour",
"ask_email": "Adresse de courriel",
"ask_firstname": "Prénom",
"ask_lastname": "Nom",
- "ask_list_to_remove": "Liste à supprimer",
"ask_main_domain": "Domaine principal",
"ask_new_admin_password": "Nouveau mot de passe d’administration",
"ask_password": "Mot de passe",
- "backup_action_required": "Vous devez préciser ce qui est à sauvegarder",
"backup_app_failed": "Impossible de sauvegarder l’application '{app:s}'",
"backup_archive_app_not_found": "L’application '{app:s}' n’a pas été trouvée dans l’archive de la sauvegarde",
- "backup_archive_hook_not_exec": "Le script « {hook:s} » n'a pas été exécuté dans cette sauvegarde",
- "backup_archive_name_exists": "Une archive de sauvegarde avec ce nom existe déjà",
+ "backup_archive_name_exists": "Une archive de sauvegarde avec ce nom existe déjà.",
"backup_archive_name_unknown": "L’archive locale de sauvegarde nommée '{name:s}' est inconnue",
- "backup_archive_open_failed": "Impossible d’ouvrir l’archive de sauvegarde",
+ "backup_archive_open_failed": "Impossible d’ouvrir l’archive de la sauvegarde",
"backup_cleaning_failed": "Impossible de nettoyer le dossier temporaire de sauvegarde",
"backup_created": "Sauvegarde terminée",
- "backup_creating_archive": "Création de l’archive de sauvegarde …",
- "backup_creation_failed": "Impossible de créer la sauvegarde",
+ "backup_creation_failed": "Impossible de créer l’archive de la sauvegarde",
"backup_delete_error": "Impossible de supprimer '{path:s}'",
"backup_deleted": "La sauvegarde a été supprimée",
- "backup_extracting_archive": "Extraction de l’archive de sauvegarde …",
"backup_hook_unknown": "Script de sauvegarde '{hook:s}' inconnu",
"backup_invalid_archive": "Archive de sauvegarde invalide",
"backup_nothings_done": "Il n’y a rien à sauvegarder",
"backup_output_directory_forbidden": "Dossier de destination interdit. Les sauvegardes ne peuvent être créées dans les sous-dossiers /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var ou /home/yunohost.backup/archives",
- "backup_output_directory_not_empty": "Le répertoire de destination n'est pas vide",
+ "backup_output_directory_not_empty": "Le répertoire de destination n’est pas vide",
"backup_output_directory_required": "Vous devez spécifier un dossier de destination pour la sauvegarde",
- "backup_running_app_script": "Lancement du script de sauvegarde de l’application « {app:s} »...",
"backup_running_hooks": "Exécution des scripts de sauvegarde …",
"custom_app_url_required": "Vous devez spécifier une URL pour mettre à jour votre application personnalisée {app:s}",
- "custom_appslist_name_required": "Vous devez spécifier un nom pour votre liste d’applications personnalisées",
- "diagnosis_debian_version_error": "Impossible de déterminer la version de Debian : {error}",
- "diagnosis_kernel_version_error": "Impossible de récupérer la version du noyau : {error}",
- "diagnosis_monitor_disk_error": "Impossible de superviser les disques : {error}",
- "diagnosis_monitor_network_error": "Impossible de superviser le réseau : {error}",
- "diagnosis_monitor_system_error": "Impossible de superviser le système : {error}",
- "diagnosis_no_apps": "Aucune application installée",
- "dnsmasq_isnt_installed": "dnsmasq ne semble pas être installé, veuillez lancer 'apt-get remove bind9 && apt-get install dnsmasq'",
"domain_cert_gen_failed": "Impossible de générer le certificat",
"domain_created": "Le domaine a été créé",
- "domain_creation_failed": "Impossible de créer le domaine",
+ "domain_creation_failed": "Impossible de créer le domaine {domain}: {error}",
"domain_deleted": "Le domaine a été supprimé",
- "domain_deletion_failed": "Impossible de supprimer le domaine",
+ "domain_deletion_failed": "Impossible de supprimer le domaine {domain}:{error}",
"domain_dyndns_already_subscribed": "Vous avez déjà souscris à un domaine DynDNS",
- "domain_dyndns_invalid": "Domaine incorrect pour un usage avec DynDNS",
"domain_dyndns_root_unknown": "Domaine DynDNS principal inconnu",
"domain_exists": "Le domaine existe déjà",
"domain_uninstall_app_first": "Une ou plusieurs applications sont installées sur ce domaine. Veuillez d’abord les désinstaller avant de supprimer ce domaine",
"domain_unknown": "Domaine inconnu",
- "domain_zone_exists": "Le fichier de zone DNS existe déjà",
- "domain_zone_not_found": "Fichier de zone DNS introuvable pour le domaine {:s}",
"done": "Terminé",
"downloading": "Téléchargement en cours …",
- "dyndns_cron_installed": "La tâche cron pour le domaine DynDNS a été installée",
- "dyndns_cron_remove_failed": "Impossible de supprimer la tâche cron DynDNS parce que: {error}",
- "dyndns_cron_removed": "La tâche cron pour le domaine DynDNS a été enlevée",
+ "dyndns_cron_installed": "La tâche cron pour le domaine DynDNS a été créée",
+ "dyndns_cron_remove_failed": "Impossible de supprimer la tâche cron DynDNS parce que : {error}",
+ "dyndns_cron_removed": "La tâche cron pour le domaine DynDNS enlevée",
"dyndns_ip_update_failed": "Impossible de mettre à jour l’adresse IP sur le domaine DynDNS",
- "dyndns_ip_updated": "Votre adresse IP a été mise à jour pour le domaine DynDNS",
- "dyndns_key_generating": "La clé DNS est en cours de génération, cela peut prendre un certain temps …",
+ "dyndns_ip_updated": "Mise à jour de votre IP pour le domaine DynDNS",
+ "dyndns_key_generating": "Génération de la clé DNS …, cela peut prendre un certain temps.",
"dyndns_key_not_found": "Clé DNS introuvable pour le domaine",
- "dyndns_no_domain_registered": "Aucun domaine n’a été enregistré avec DynDNS",
- "dyndns_registered": "Le domaine DynDNS a été enregistré",
+ "dyndns_no_domain_registered": "Aucun domaine enregistré avec DynDNS",
+ "dyndns_registered": "Domaine DynDNS enregistré",
"dyndns_registration_failed": "Impossible d’enregistrer le domaine DynDNS : {error:s}",
"dyndns_unavailable": "Le domaine {domain:s} est indisponible.",
"executing_command": "Exécution de la commande '{command:s}' …",
@@ -104,340 +74,244 @@
"extracting": "Extraction en cours …",
"field_invalid": "Champ incorrect : '{:s}'",
"firewall_reload_failed": "Impossible de recharger le pare-feu",
- "firewall_reloaded": "Le pare-feu a été rechargé",
- "firewall_rules_cmd_failed": "Certaines règles du pare-feu n’ont pas pu être appliquées. Pour plus d’informations, consultez le journal.",
- "format_datetime_short": "%d/%m/%Y %H:%M",
- "hook_argument_missing": "Argument manquant : '{:s}'",
- "hook_choice_invalid": "Choix incorrect : '{:s}'",
+ "firewall_reloaded": "Pare-feu rechargé",
+ "firewall_rules_cmd_failed": "Certaines règles du pare-feu n’ont pas pu être appliquées. Plus d’info dans le journal de log.",
"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 correctement",
"hook_list_by_invalid": "Propriété invalide pour lister les actions par celle-ci",
- "hook_name_unknown": "Nom de l'action '{name:s}' inconnu",
+ "hook_name_unknown": "Nom de l’action '{name:s}' inconnu",
"installation_complete": "Installation terminée",
- "installation_failed": "Échec de l’installation",
+ "installation_failed": "Quelque chose s’est mal passé lors de l’installation",
"ip6tables_unavailable": "Vous ne pouvez pas jouer avec ip6tables ici. Vous êtes soit dans un conteneur, soit votre noyau ne le prend pas en charge",
"iptables_unavailable": "Vous ne pouvez pas jouer avec iptables ici. Vous êtes soit dans un conteneur, soit votre noyau ne le prend pas en charge",
- "ldap_initialized": "L’annuaire LDAP a été initialisé",
- "license_undefined": "indéfinie",
+ "ldap_initialized": "L’annuaire LDAP initialisé",
"mail_alias_remove_failed": "Impossible de supprimer l’alias de courriel '{mail:s}'",
- "mail_domain_unknown": "Le domaine '{domain:s}' pour l'adresse de courriel est inconnu",
+ "mail_domain_unknown": "Le domaine '{domain:s}' de cette adresse de courriel n’est pas valide. Merci d’utiliser un domaine administré par ce serveur.",
"mail_forward_remove_failed": "Impossible de supprimer le courriel de transfert '{mail:s}'",
- "maindomain_change_failed": "Impossible de modifier le domaine principal",
- "maindomain_changed": "Le domaine principal a été modifié",
- "monitor_disabled": "La supervision du serveur a été désactivé",
- "monitor_enabled": "La supervision du serveur a été activé",
- "monitor_glances_con_failed": "Impossible de se connecter au serveur Glances",
- "monitor_not_enabled": "Le suivi de l’état du serveur n’est pas activé",
- "monitor_period_invalid": "Période de temps incorrecte",
- "monitor_stats_file_not_found": "Le fichier de statistiques est introuvable",
- "monitor_stats_no_update": "Aucune donnée de l’état du serveur à mettre à jour",
- "monitor_stats_period_unavailable": "Aucune statistique n’est disponible pour la période",
- "mountpoint_unknown": "Point de montage inconnu",
- "mysql_db_creation_failed": "Impossible de créer la base de données MySQL",
- "mysql_db_init_failed": "Impossible d’initialiser la base de données MySQL",
- "mysql_db_initialized": "La base de données MySQL a été initialisée",
- "network_check_mx_ko": "L’enregistrement DNS MX n’est pas défini",
- "network_check_smtp_ko": "Le trafic courriel sortant (port 25 SMTP) semble bloqué par votre réseau",
- "network_check_smtp_ok": "Le trafic courriel sortant (port 25 SMTP) n’est pas bloqué",
- "new_domain_required": "Vous devez spécifier le nouveau domaine principal",
- "no_appslist_found": "Aucune liste d’applications n’a été trouvée",
+ "main_domain_change_failed": "Impossible de modifier le domaine principal",
+ "main_domain_changed": "Le domaine principal modifié",
"no_internet_connection": "Le serveur n’est pas connecté à Internet",
- "no_ipv6_connectivity": "La connectivité IPv6 n’est pas disponible",
- "no_restore_script": "Le script de sauvegarde n’a pas été trouvé pour l’application '{app:s}'",
- "no_such_conf_file": "Le fichier {file:s} n’existe pas, il ne peut pas être copié",
"not_enough_disk_space": "L’espace disque est insuffisant sur '{path:s}'",
- "package_not_installed": "Le paquet '{pkgname}' n’est pas installé",
- "package_unexpected_error": "Une erreur inattendue s'est produite lors du traitement du paquet '{pkgname}'",
"package_unknown": "Le paquet '{pkgname}' est inconnu",
- "packages_no_upgrade": "Il n’y a aucun paquet à mettre à jour",
- "packages_upgrade_critical_later": "Les paquets critiques ({packages:s}) seront mis à jour ultérieurement",
"packages_upgrade_failed": "Impossible de mettre à jour tous les paquets",
- "path_removal_failed": "Impossible de supprimer le chemin {:s}",
"pattern_backup_archive_name": "Doit être un nom de fichier valide avec un maximum de 30 caractères, et composé de caractères alphanumériques et -_. uniquement",
"pattern_domain": "Doit être un nom de domaine valide (ex : mon-domaine.fr)",
- "pattern_email": "Doit être une adresse de courriel valide (ex. : pseudo@domaine.fr)",
+ "pattern_email": "Doit être une adresse de courriel valide (ex. : pseudo@example.com)",
"pattern_firstname": "Doit être un prénom valide",
"pattern_lastname": "Doit être un nom valide",
- "pattern_listname": "Doit être composé uniquement de caractères alphanumériques et de tirets bas (aussi appelé tiret du 8 ou underscore)",
"pattern_mailbox_quota": "Doit avoir une taille suffixée avec b/k/M/G/T ou 0 pour désactiver le quota",
"pattern_password": "Doit être composé d’au moins 3 caractères",
- "pattern_port": "Doit être un numéro de port valide compris entre 0 et 65535",
"pattern_port_or_range": "Doit être un numéro de port valide compris entre 0 et 65535, ou une gamme de ports (exemple : 100:200)",
"pattern_positive_number": "Doit être un nombre positif",
"pattern_username": "Doit être composé uniquement de caractères alphanumériques minuscules et de tirets bas (aussi appelé tiret du 8 ou underscore)",
"port_already_closed": "Le port {port:d} est déjà fermé pour les connexions {ip_version:s}",
"port_already_opened": "Le port {port:d} est déjà ouvert pour les connexions {ip_version:s}",
- "port_available": "Le port {port:d} est disponible",
- "port_unavailable": "Le port {port:d} n’est pas disponible",
- "restore_action_required": "Vous devez préciser ce qui est à restaurer",
"restore_already_installed_app": "Une application est déjà installée avec l’identifiant '{app:s}'",
"restore_app_failed": "Impossible de restaurer l’application '{app:s}'",
"restore_cleaning_failed": "Impossible de nettoyer le dossier temporaire de restauration",
- "restore_complete": "Restauration terminée",
+ "restore_complete": "Restauré",
"restore_confirm_yunohost_installed": "Voulez-vous vraiment restaurer un système déjà installé ? [{answers:s}]",
"restore_failed": "Impossible de restaurer le système",
- "restore_hook_unavailable": "Le script de restauration '{part:s}' n’est pas disponible sur votre système, et ne l'est pas non plus dans l’archive",
+ "restore_hook_unavailable": "Le script de restauration '{part:s}' n’est pas disponible sur votre système, et ne l’est pas non plus dans l’archive",
"restore_nothings_done": "Rien n’a été restauré",
- "restore_running_app_script": "Exécution du script de restauration de l'application '{app:s}' .…",
+ "restore_running_app_script": "Exécution du script de restauration de l’application '{app:s}' …",
"restore_running_hooks": "Exécution des scripts de restauration …",
- "service_add_configuration": "Ajout du fichier de configuration {file:s}",
"service_add_failed": "Impossible d’ajouter le service '{service:s}'",
"service_added": "Le service '{service:s}' a été ajouté",
- "service_already_started": "Le service '{service:s}' est déjà démarré",
+ "service_already_started": "Le service '{service:s}' est déjà en cours d’exécution",
"service_already_stopped": "Le service '{service:s}' est déjà arrêté",
"service_cmd_exec_failed": "Impossible d’exécuter la commande '{command:s}'",
- "service_conf_file_backed_up": "Le fichier de configuration '{conf}' a été sauvegardé dans '{backup}'",
- "service_conf_file_copy_failed": "Impossible de copier le nouveau fichier de configuration '{new}' vers '{conf}'",
- "service_conf_file_manually_modified": "Le fichier de configuration '{conf}' a été modifié manuellement et ne sera pas mis à jour",
- "service_conf_file_manually_removed": "Le fichier de configuration '{conf}' a été supprimé manuellement et ne sera pas créé",
- "service_conf_file_not_managed": "Le fichier de configuration « {conf} » n'est pas géré pour l'instant et ne sera pas mis à jour",
- "service_conf_file_remove_failed": "Impossible de supprimer le fichier de configuration '{conf}'",
- "service_conf_file_removed": "Le fichier de configuration '{conf}' a été supprimé",
- "service_conf_file_updated": "Le fichier de configuration '{conf}' a été mis à jour",
- "service_conf_up_to_date": "La configuration du service '{service}' est déjà à jour",
- "service_conf_updated": "La configuration a été mise à jour pour le service '{service}'",
- "service_conf_would_be_updated": "La configuration du service '{service}' aurait été mise à jour",
- "service_configuration_conflict": "Le fichier {file:s} a été modifié depuis sa dernière génération. Veuillez y appliquer les modifications manuellement ou utiliser l’option --force (ce qui écrasera toutes les modifications effectuées sur le fichier).",
- "service_configured": "La configuration du service « {service:s} » a été générée avec succès",
- "service_configured_all": "La configuration de tous les services a été générée avec succès",
- "service_disable_failed": "Impossible de désactiver le service '{service:s}'\n\nJournaux historisés récents : {logs:s}",
- "service_disabled": "Le service '{service:s}' a été désactivé",
- "service_enable_failed": "Impossible d’activer le service '{service:s}'\n\nJournaux historisés récents : {logs:s}",
- "service_enabled": "Le service '{service:s}' a été activé",
- "service_no_log": "Aucun journal historisé à afficher pour le service '{service:s}'",
- "service_regenconf_dry_pending_applying": "Vérification des configurations en attentes qui pourraient être appliquées au le service '{service}' …",
- "service_regenconf_failed": "Impossible de régénérer la configuration pour les services : {services}",
- "service_regenconf_pending_applying": "Application des configurations en attentes pour le service '{service}' …",
+ "service_disable_failed": "Impossible de ne pas lancer le service « {service:s} » au démarrage.\n\nJournaux récents du service : {logs:s}",
+ "service_disabled": "Le service « {service:s} » ne sera plus lancé au démarrage du système.",
+ "service_enable_failed": "Impossible de lancer automatiquement le service « {service:s} » au démarrage.\n\nJournaux récents du service : {logs:s}",
+ "service_enabled": "Le service « {service:s} » sera désormais lancé automatiquement au démarrage du système.",
"service_remove_failed": "Impossible de supprimer le service '{service:s}'",
- "service_removed": "Le service '{service:s}' a été supprimé",
+ "service_removed": "Le service « {service:s} » a été supprimé",
"service_start_failed": "Impossible de démarrer le service '{service:s}'\n\nJournaux historisés récents : {logs:s}",
- "service_started": "Le service '{service:s}' a été démarré",
- "service_status_failed": "Impossible de déterminer le statut du service '{service:s}'",
+ "service_started": "Le service « {service:s} » a été démarré",
"service_stop_failed": "Impossible d’arrêter le service '{service:s}'\n\nJournaux historisés récents : {logs:s}",
- "service_stopped": "Le service '{service:s}' a été arrêté",
+ "service_stopped": "Le service « {service:s} » a été arrêté",
"service_unknown": "Le service '{service:s}' est inconnu",
- "services_configured": "La configuration a été générée avec succès",
- "show_diff": "Voici les différences :\n{diff:s}",
- "ssowat_conf_generated": "La configuration de SSOwat a été générée",
- "ssowat_conf_updated": "La configuration de SSOwat a été mise à jour",
- "system_upgraded": "Le système a été mis à jour",
+ "ssowat_conf_generated": "La configuration de SSOwat générée",
+ "ssowat_conf_updated": "La configuration de SSOwat mise à jour",
+ "system_upgraded": "Système mis à jour",
"system_username_exists": "Ce nom d’utilisateur existe déjà dans les utilisateurs système",
"unbackup_app": "L’application '{app:s}' ne sera pas sauvegardée",
"unexpected_error": "Une erreur inattendue est survenue : {error}",
- "unit_unknown": "L'unité '{unit:s}' est inconnue",
"unlimit": "Pas de quota",
"unrestore_app": "L’application '{app:s}' ne sera pas restaurée",
- "update_cache_failed": "Impossible de mettre à jour le cache de l'outil de gestion avancée des paquets (APT)",
"updating_apt_cache": "Récupération des mises à jour disponibles pour les paquets du système …",
"upgrade_complete": "Mise à jour terminée",
"upgrading_packages": "Mise à jour des paquets en cours …",
"upnp_dev_not_found": "Aucun périphérique compatible UPnP n’a été trouvé",
- "upnp_disabled": "UPnP a été désactivé",
- "upnp_enabled": "UPnP a été activé",
+ "upnp_disabled": "UPnP désactivé",
+ "upnp_enabled": "UPnP activé",
"upnp_port_open_failed": "Impossible d’ouvrir les ports UPnP",
- "user_created": "L’utilisateur a été créé",
- "user_creation_failed": "Impossible de créer l’utilisateur",
- "user_deleted": "L’utilisateur a été supprimé",
- "user_deletion_failed": "Impossible de supprimer l’utilisateur",
+ "user_created": "L’utilisateur créé",
+ "user_creation_failed": "Impossible de créer l’utilisateur {user}: {error}",
+ "user_deleted": "L’utilisateur supprimé",
+ "user_deletion_failed": "Impossible de supprimer l’utilisateur {user}: {error}",
"user_home_creation_failed": "Impossible de créer le dossier personnel de l’utilisateur",
- "user_info_failed": "Impossible de récupérer les informations de l’utilisateur",
- "user_unknown": "L'utilisateur {user:s} est inconnu",
- "user_update_failed": "Impossible de modifier l’utilisateur",
+ "user_unknown": "L’utilisateur {user:s} est inconnu",
+ "user_update_failed": "Impossible de mettre à jour l’utilisateur {user}: {error}",
"user_updated": "L’utilisateur a été modifié",
"yunohost_already_installed": "YunoHost est déjà installé",
"yunohost_ca_creation_failed": "Impossible de créer l’autorité de certification",
- "yunohost_configured": "YunoHost a été configuré",
- "yunohost_installing": "L'installation de YunoHost est en cours …",
- "yunohost_not_installed": "YunoHost n’est pas ou pas correctement installé. Veuillez exécuter 'yunohost tools postinstall'",
+ "yunohost_configured": "YunoHost est maintenant configuré",
+ "yunohost_installing": "L’installation de YunoHost est en cours …",
+ "yunohost_not_installed": "YunoHost n’est pas correctement installé. Veuillez exécuter 'yunohost tools postinstall'",
"certmanager_attempt_to_replace_valid_cert": "Vous êtes en train de vouloir remplacer un certificat correct et valide pour le domaine {domain:s} ! (Utilisez --force pour contourner cela)",
"certmanager_domain_unknown": "Domaine {domain:s} inconnu",
"certmanager_domain_cert_not_selfsigned": "Le certificat du domaine {domain:s} n’est pas auto-signé. Voulez-vous vraiment le remplacer ? (Utilisez --force pour cela)",
"certmanager_certificate_fetching_or_enabling_failed": "Il semble que l’activation du nouveau certificat pour {domain:s} a échoué …",
"certmanager_attempt_to_renew_nonLE_cert": "Le certificat pour le domaine {domain:s} n’est pas émis par Let’s Encrypt. Impossible de le renouveler automatiquement !",
- "certmanager_attempt_to_renew_valid_cert": "Le certificat pour le domaine {domain:s} n'est pas sur le point d’expirer ! (Vous pouvez utiliser --force si vous savez ce que vous faites)",
+ "certmanager_attempt_to_renew_valid_cert": "Le certificat pour le domaine {domain:s} n’est pas sur le point d’expirer ! (Vous pouvez utiliser --force si vous savez ce que vous faites)",
"certmanager_domain_http_not_working": "Il semble que le domaine {domain:s} ne soit pas accessible via HTTP. Veuillez vérifier que vos configuration DNS et Nginx sont correctes",
"certmanager_error_no_A_record": "Aucun enregistrement DNS 'A' n’a été trouvé pour {domain:s}. Vous devez faire pointer votre nom de domaine vers votre machine pour être en mesure d’installer un certificat Let’s Encrypt ! (Si vous savez ce que vous faites, utilisez --no-checks pour désactiver ces contrôles)",
- "certmanager_domain_dns_ip_differs_from_public_ip": "L’enregistrement DNS 'A' du domaine {domain:s} est différent de l’adresse IP de ce serveur. Si vous avez récemment modifié votre enregistrement 'A', veuillez attendre sa propagation (quelques vérificateur de propagation DNS sont disponibles en ligne). (Si vous savez ce que vous faites, utilisez --no-checks pour désactiver ces contrôles)",
+ "certmanager_domain_dns_ip_differs_from_public_ip": "L’enregistrement DNS 'A' du domaine {domain:s} est différent de l’adresse IP de ce serveur. Si vous avez récemment modifié votre enregistrement 'A', veuillez attendre sa propagation (quelques vérificateurs de propagation DNS sont disponibles en ligne). (Si vous savez ce que vous faites, utilisez --no-checks pour désactiver ces contrôles)",
"certmanager_cannot_read_cert": "Quelque chose s’est mal passé lors de la tentative d’ouverture du certificat actuel pour le domaine {domain:s} (fichier : {file:s}), la cause est : {reason:s}",
- "certmanager_cert_install_success_selfsigned": "Installation avec succès d’un certificat auto-signé pour le domaine {domain:s} !",
- "certmanager_cert_install_success": "Installation avec succès d’un certificat Let’s Encrypt pour le domaine {domain:s} !",
- "certmanager_cert_renew_success": "Renouvellement avec succès d’un certificat Let’s Encrypt pour le domaine {domain:s} !",
- "certmanager_old_letsencrypt_app_detected": "\nYunoHost a détecté que l’application « letsencrypt » est installé, ce qui est en conflit avec les nouvelles fonctionnalités de gestion intégrée de certificats dans YunoHost. Si vous souhaitez utiliser ces nouvelles fonctionnalités intégrées, veuillez lancer les commandes suivantes pour migrer votre installation :\n\n yunohost app remove letsencrypt\n yunohost domain cert-install\n\nN.B. : cela tentera de réinstaller les certificats de tous les domaines avec un certificat Let's Encrypt ou ceux auto-signés",
- "certmanager_cert_signing_failed": "La signature du nouveau certificat a échoué",
+ "certmanager_cert_install_success_selfsigned": "Le certificat auto-signé est maintenant installé pour le domaine « {domain:s} »",
+ "certmanager_cert_install_success": "Le certificat Let’s Encrypt est maintenant installé pour le domaine « {domain:s} »",
+ "certmanager_cert_renew_success": "Certificat Let’s Encrypt renouvelé pour le domaine '{domain:s}'",
+ "certmanager_cert_signing_failed": "Impossible de signer le nouveau certificat",
"certmanager_no_cert_file": "Impossible de lire le fichier du certificat pour le domaine {domain:s} (fichier : {file:s})",
- "certmanager_conflicting_nginx_file": "Impossible de préparer le domaine pour le défi ACME : le fichier de configuration Nginx {filepath:s} est en conflit et doit être préalablement retiré",
+ "certmanager_conflicting_nginx_file": "Impossible de préparer le domaine pour le défi ACME : le fichier de configuration NGINX {filepath:s} est en conflit et doit être préalablement retiré",
"certmanager_hit_rate_limit": "Trop de certificats ont déjà été émis récemment pour ce même ensemble de domaines {domain:s}. Veuillez réessayer plus tard. Lisez https://letsencrypt.org/docs/rate-limits/ pour obtenir plus de détails sur les ratios et limitations",
- "ldap_init_failed_to_create_admin": "L’initialisation de l'annuaire LDAP n’a pas réussi à créer l’utilisateur admin",
- "ssowat_persistent_conf_read_error": "Erreur lors de la lecture de la configuration persistante de SSOwat : {error:s}. Modifiez le fichier /etc/ssowat/conf.json.persistent pour réparer la syntaxe JSON",
- "ssowat_persistent_conf_write_error": "Erreur lors de la sauvegarde de la configuration persistante de SSOwat : {error:s}. Modifiez le fichier /etc/ssowat/conf.json.persistent pour réparer la syntaxe JSON",
- "domain_cannot_remove_main": "Impossible de supprimer le domaine principal. Définissez d'abord un nouveau domaine principal",
+ "ldap_init_failed_to_create_admin": "L’initialisation de l’annuaire LDAP n’a pas réussi à créer l’utilisateur admin",
+ "domain_cannot_remove_main": "Vous ne pouvez pas supprimer '{domain:s}' car il s’agit du domaine principal. Vous devez d’abord définir un autre domaine comme domaine principal à l’aide de 'yunohost domain main-domain -n ', voici la liste des domaines candidats. : {other_domains:s}",
"certmanager_self_ca_conf_file_not_found": "Le fichier de configuration pour l’autorité du certificat auto-signé est introuvable (fichier : {file:s})",
"certmanager_unable_to_parse_self_CA_name": "Impossible d’analyser le nom de l’autorité du certificat auto-signé (fichier : {file:s})",
- "mailbox_used_space_dovecot_down": "Le service de courriel Dovecot doit être démarré, si vous souhaitez voir l’espace disque occupé par la messagerie",
+ "mailbox_used_space_dovecot_down": "Le service de courriel 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 sauvegarde (lien invalide vers {path:s})",
- "certmanager_acme_not_configured_for_domain": "Le certificat du domaine {domain:s} ne semble pas être correctement installé. Veuillez d'abord exécuter cert-install.",
- "certmanager_domain_not_resolved_locally": "Le domaine {domain:s} ne peut être résolu depuis votre serveur YunoHost. Cela peut se produire si vous avez récemment modifié votre enregistrement DNS. Si c'est le cas, merci d’attendre quelques heures qu’il se propage. Si le problème persiste, envisager d’ajouter {domain:s} au fichier /etc/hosts. (Si vous savez ce que vous faites, utilisez --no-checks pour désactiver ces vérifications.)",
- "certmanager_http_check_timeout": "Expiration du délai lorsque le serveur a essayé de se contacter lui-même via HTTP en utilisant l'adresse IP public {ip:s} du domaine {domain:s}. Vous rencontrez peut-être un problème d’hairpinning ou alors le pare-feu/routeur en amont de votre serveur est mal configuré.",
+ "certmanager_acme_not_configured_for_domain": "Le certificat du domaine {domain:s} ne semble pas être correctement installé. Veuillez d’abord exécuter cert-install.",
+ "certmanager_http_check_timeout": "Expiration du délai lorsque le serveur a essayé de se contacter lui-même via HTTP en utilisant l’adresse IP public {ip:s} du domaine {domain:s}. Vous rencontrez peut-être un problème d’hairpinning ou alors le pare-feu/routeur en amont de votre serveur est mal configuré.",
"certmanager_couldnt_fetch_intermediate_cert": "Expiration du délai lors de la tentative de récupération du certificat intermédiaire depuis Let’s Encrypt. L’installation ou le renouvellement du certificat a été annulé. Veuillez réessayer plus tard.",
- "appslist_retrieve_bad_format": "Le fichier récupéré pour la liste d’applications {appslist:s} n’est pas valide",
- "domain_hostname_failed": "Échec de l’utilisation d’un nouveau nom d’hôte. Cela pourrait causer des soucis plus tard (mais ce n’est pas sûr… peut-être que ça n’en causera pas).",
- "yunohost_ca_creation_success": "L’autorité de certification locale a été créée.",
- "appslist_name_already_tracked": "Il y a déjà une liste d’applications enregistrée avec le nom {name:s}.",
- "appslist_url_already_tracked": "Il y a déjà une liste d’applications enregistrée avec l’URL {url:s}.",
- "appslist_migrating": "Migration de la liste d’applications {appslist:s} …",
- "appslist_could_not_migrate": "Impossible de migrer la liste {appslist:s} ! Impossible d’exploiter l’URL. L’ancienne tâche programmée a été conservée dans {bkp_file:s}.",
- "appslist_corrupted_json": "Impossible de charger la liste d’applications. Il semble que {filename:s} soit corrompu.",
- "app_already_installed_cant_change_url": "Cette application est déjà installée. L’URL ne peut pas être changé simplement par cette fonction. Regardez si cela est disponible avec `app changeurl`.",
- "app_change_no_change_url_script": "L’application {app_name:s} ne prend pas encore en charge le changement d’URL, vous pourriez avoir besoin de la mettre à jour.",
+ "domain_hostname_failed": "Échec de l’utilisation d’un nouveau nom d’hôte. Cela pourrait causer des soucis plus tard (peut-être que ça n’en causera pas).",
+ "yunohost_ca_creation_success": "L’autorité de certification locale créée.",
+ "app_already_installed_cant_change_url": "Cette application est déjà installée. L’URL ne peut pas être changé simplement par cette fonction. Vérifiez si cela est disponible avec `app changeurl`.",
"app_change_url_failed_nginx_reload": "Le redémarrage de Nginx a échoué. Voici la sortie de 'nginx -t' :\n{nginx_errors:s}",
- "app_change_url_identical_domains": "L’ancien et le nouveau couple domaine/chemin_de_l'URL sont identiques pour ('{domain:s}{path:s}'), rien à faire.",
+ "app_change_url_identical_domains": "L’ancien et le nouveau couple domaine/chemin_de_l’URL sont identiques pour ('{domain:s}{path:s}'), rien à faire.",
"app_change_url_no_script": "L’application '{app_name:s}' ne prend pas encore en charge le changement d’URL. Vous devriez peut-être la mettre à jour.",
"app_change_url_success": "L’URL de l’application {app:s} a été changée en {domain:s}{path:s}",
"app_location_unavailable": "Cette URL n’est pas disponible ou est en conflit avec une application existante :\n{apps:s}",
"app_already_up_to_date": "{app:s} est déjà à jour",
- "invalid_url_format": "Format d’URL non valide",
- "global_settings_bad_choice_for_enum": "Valeur du paramètre {setting:s} incorrecte. Reçu : {received_type:s}, mais les valeurs possibles sont : {expected_type:s}",
+ "global_settings_bad_choice_for_enum": "Valeur du paramètre {setting:s} incorrecte. Reçu : {choice:s}, mais les valeurs possibles sont : {available_choices:s}",
"global_settings_bad_type_for_setting": "Le type du paramètre {setting:s} est incorrect. Reçu {received_type:s} alors que {expected_type:s} était attendu",
"global_settings_cant_open_settings": "Échec de l’ouverture du ficher de configurations car : {reason:s}",
- "global_settings_cant_serialize_setings": "Échec de sérialisation des données de configurations, cause : {reason:s}",
"global_settings_cant_write_settings": "Échec d’écriture du fichier de configurations car : {reason:s}",
"global_settings_key_doesnt_exists": "La clef '{settings_key:s}' n’existe pas dans les configurations générales, vous pouvez voir toutes les clefs disponibles en saisissant 'yunohost settings list'",
- "global_settings_reset_success": "Super ! Vos configurations précédentes ont été sauvegardées dans {path:s}",
+ "global_settings_reset_success": "Vos configurations précédentes ont été sauvegardées dans {path:s}",
"global_settings_setting_example_bool": "Exemple d’option booléenne",
"global_settings_setting_example_int": "Exemple d’option de type entier",
"global_settings_setting_example_string": "Exemple d’option de type chaîne",
"global_settings_setting_example_enum": "Exemple d’option de type énumération",
- "global_settings_unknown_type": "Situation inattendue : la configuration {setting:s} semble avoir le type {unknown_type:s} mais celui-ci n'est pas pris en charge par le système.",
+ "global_settings_unknown_type": "Situation inattendue : la configuration {setting:s} semble avoir le type {unknown_type:s} mais celui-ci n’est pas pris en charge par le système.",
"global_settings_unknown_setting_from_settings_file": "Clé inconnue dans les paramètres : '{setting_key:s}', rejet de cette clé et sauvegarde de celle-ci dans /etc/yunohost/unkown_settings.json",
- "service_conf_new_managed_file": "Le fichier de configuration « {conf} » est désormais géré par le service {service}.",
- "service_conf_file_kept_back": "Le fichier de configuration '{conf}' devait être supprimé par le service {service} mais a été conservé.",
- "backup_abstract_method": "Cette méthode de sauvegarde n’a pas encore été implémentée",
- "backup_applying_method_tar": "Création de l’archive tar de la sauvegarde …",
+ "backup_abstract_method": "Cette méthode de sauvegarde reste à implémenter",
+ "backup_applying_method_tar": "Création de l’archive TAR de la sauvegarde …",
"backup_applying_method_copy": "Copie de tous les fichiers à sauvegarder …",
- "backup_applying_method_borg": "Envoi de tous les fichiers à sauvegarder dans le répertoire borg-backup…",
+ "backup_applying_method_borg": "Envoi de tous les fichiers à sauvegarder dans le répertoire borg-backup …",
"backup_applying_method_custom": "Appel de la méthode de sauvegarde personnalisée '{method:s}' …",
"backup_archive_system_part_not_available": "La partie '{part:s}' du système n’est pas disponible dans cette sauvegarde",
- "backup_archive_mount_failed": "Le montage de l’archive de sauvegarde a échoué",
- "backup_archive_writing_error": "Impossible d'ajouter des fichiers '{source:s}' (nommés dans l'archive : '{dest:s}') à sauvegarder dans l'archive compressée '{archive:s}'",
- "backup_ask_for_copying_if_needed": "Certains fichiers n’ont pas pu être préparés pour être sauvegardés en utilisant la méthode qui évite temporairement de gaspiller de l’espace sur le système. Pour réaliser la sauvegarde, {size:s} Mo doivent être temporairement utilisés. Acceptez-vous ?",
+ "backup_archive_writing_error": "Impossible d’ajouter des fichiers '{source:s}' (nommés dans l’archive : '{dest:s}') à sauvegarder dans l’archive compressée '{archive:s}'",
+ "backup_ask_for_copying_if_needed": "Voulez-vous effectuer la sauvegarde en utilisant {size:s} temporairement ? (Cette méthode est utilisée car certains fichiers n’ont pas pu être préparés avec une méthode plus efficace.)",
"backup_borg_not_implemented": "La méthode de sauvegarde Borg n’est pas encore implémentée",
"backup_cant_mount_uncompress_archive": "Impossible de monter en lecture seule le dossier de l’archive décompressée",
"backup_copying_to_organize_the_archive": "Copie de {size:s} Mo pour organiser l’archive",
- "backup_csv_creation_failed": "Impossible de créer le fichier CSV nécessaire aux opérations futures de restauration",
+ "backup_csv_creation_failed": "Impossible de créer le fichier CSV nécessaire à la restauration",
"backup_csv_addition_failed": "Impossible d’ajouter des fichiers à sauvegarder dans le fichier CSV",
- "backup_custom_need_mount_error": "Échec de la méthode de sauvegarde personnalisée à l’étape 'need_mount'",
"backup_custom_backup_error": "Échec de la méthode de sauvegarde personnalisée à l’étape 'backup'",
"backup_custom_mount_error": "Échec de la méthode de sauvegarde personnalisée à l’étape 'mount'",
- "backup_no_uncompress_archive_dir": "Le dossier de l’archive décompressée n’existe pas",
- "backup_method_tar_finished": "L’archive tar de la sauvegarde a été créée",
+ "backup_no_uncompress_archive_dir": "Ce dossier d’archive décompressée n’existe pas",
+ "backup_method_tar_finished": "L’archive TAR de la sauvegarde a été créée",
"backup_method_copy_finished": "La copie de la sauvegarde est terminée",
"backup_method_borg_finished": "La sauvegarde dans Borg est terminée",
"backup_method_custom_finished": "La méthode de sauvegarde personnalisée '{method:s}' est terminée",
"backup_system_part_failed": "Impossible de sauvegarder la partie '{part:s}' du système",
- "backup_unable_to_organize_files": "Impossible d’organiser les fichiers dans l’archive avec la méthode rapide",
+ "backup_unable_to_organize_files": "Impossible d’utiliser la méthode rapide pour organiser les fichiers dans l’archive",
"backup_with_no_backup_script_for_app": "L’application {app:s} n’a pas de script de sauvegarde. Ignorer.",
- "backup_with_no_restore_script_for_app": "L’application {app:s} n’a pas de script de restauration, vous ne pourrez pas restaurer automatiquement la sauvegarde de cette application.",
+ "backup_with_no_restore_script_for_app": "L’application « {app:s} » n’a pas de script de restauration, vous ne pourrez pas restaurer automatiquement la sauvegarde de cette application.",
"global_settings_cant_serialize_settings": "Échec de la sérialisation des données de paramétrage car : {reason:s}",
"restore_removing_tmp_dir_failed": "Impossible de sauvegarder un ancien dossier temporaire",
"restore_extracting": "Extraction des fichiers nécessaires depuis l’archive …",
- "restore_mounting_archive": "Montage de l’archive dans '{path:s}'",
- "restore_may_be_not_enough_disk_space": "Votre système semble ne pas avoir suffisamment d’espace disponible (L'espace libre est de {free_space:d} octets. Le besoin d'espace nécessaire est de {needed_space:d} octets. En appliquant une marge de sécurité, la quantité d'espace nécessaire est de {margin:d} octets)",
- "restore_not_enough_disk_space": "Espace disponible insuffisant (L'espace libre est de {free_space:d} octets. Le besoin d'espace nécessaire est de {needed_space:d} octets. En appliquant une marge de sécurité, la quantité d'espace nécessaire est de {margin:d} octets)",
+ "restore_may_be_not_enough_disk_space": "Votre système semble ne pas avoir suffisamment d’espace disponible (L’espace libre est de {free_space:d} octets. Le besoin d’espace nécessaire est de {needed_space:d} octets. En appliquant une marge de sécurité, la quantité d’espace nécessaire est de {margin:d} octets)",
+ "restore_not_enough_disk_space": "Espace disponible insuffisant (L’espace libre est de {free_space:d} octets. Le besoin d’espace nécessaire est de {needed_space:d} octets. En appliquant une marge de sécurité, la quantité d’espace nécessaire est de {margin:d} octets)",
"restore_system_part_failed": "Impossible de restaurer la partie '{part:s}' du système",
"backup_couldnt_bind": "Impossible de lier {src:s} avec {dest:s}.",
"domain_dns_conf_is_just_a_recommendation": "Cette page montre la configuration *recommandée*. Elle ne configure *pas* le DNS pour vous. Il est de votre responsabilité que de configurer votre zone DNS chez votre fournisseur/registrar DNS avec cette recommandation.",
- "domain_dyndns_dynette_is_unreachable": "Impossible de contacter la dynette YunoHost. Soit YunoHost n’est pas correctement connecté à internet, soit le serveur de dynette est en panne. Erreur : {error}",
- "migrations_backward": "Migration en arrière.",
- "migrations_bad_value_for_target": "Nombre invalide pour le paramètre target, les numéros de migration sont 0 ou {}",
- "migrations_cant_reach_migration_file": "Impossible d’accéder aux fichiers de migrations avec le chemin %s",
- "migrations_current_target": "La cible de migration est {}",
- "migrations_error_failed_to_load_migration": "ERREUR : échec du chargement de migration {number} {name}",
- "migrations_forward": "Migration en avant",
- "migrations_loading_migration": "Chargement de la migration {number} {name} …",
- "migrations_migration_has_failed": "La migration {number} {name} a échoué avec l’exception {exception} : annulation",
+ "migrations_cant_reach_migration_file": "Impossible d’accéder aux fichiers de migration via le chemin '%s'",
+ "migrations_loading_migration": "Chargement de la migration {id} …",
+ "migrations_migration_has_failed": "La migration {id} a échoué avec l’exception {exception} : annulation",
"migrations_no_migrations_to_run": "Aucune migration à lancer",
- "migrations_show_currently_running_migration": "Application de la migration {number} {name} …",
- "migrations_show_last_migration": "La dernière migration appliquée est {}",
- "migrations_skip_migration": "Ignorer et passer la migration {number} {name}…",
- "server_shutdown": "Le serveur va éteindre",
+ "migrations_skip_migration": "Ignorer et passer la migration {id} …",
+ "server_shutdown": "Le serveur va s’éteindre",
"server_shutdown_confirm": "Le serveur va être éteint immédiatement, le voulez-vous vraiment ? [{answers:s}]",
"server_reboot": "Le serveur va redémarrer",
"server_reboot_confirm": "Le serveur va redémarrer immédiatement, le voulez-vous vraiment ? [{answers:s}]",
- "app_upgrade_some_app_failed": "Impossible de mettre à jour certaines applications",
- "ask_path": "Chemin",
+ "app_upgrade_some_app_failed": "Certaines applications n’ont pas été mises à jour",
"dyndns_could_not_check_provide": "Impossible de vérifier si {provider:s} peut fournir {domain:s}.",
"dyndns_domain_not_provided": "Le fournisseur DynDNS {provider:s} ne peut pas fournir le domaine {domain:s}.",
- "app_make_default_location_already_used": "Impossible de configurer l’application '{app}' par défaut pour le domaine {domain} car il est déjà utilisé par l'application '{other_app}'",
+ "app_make_default_location_already_used": "Impossible de configurer l’application '{app}' par défaut pour le domaine '{domain}' car il est déjà utilisé par l’application '{other_app}'",
"app_upgrade_app_name": "Mise à jour de l’application {app} …",
- "backup_output_symlink_dir_broken": "Vous avez un lien symbolique cassé à la place de votre dossier d’archives « {path:s} ». Vous pourriez avoir une configuration personnalisée pour sauvegarder vos données sur un autre système de fichiers, dans ce cas vous avez probablement oublié de monter ou de connecter votre disque dur ou votre clé USB.",
- "migrate_tsig_end": "La migration à hmac-sha512 est terminée",
+ "backup_output_symlink_dir_broken": "Votre répertoire d’archivage '{path:s}' est un lien symbolique brisé. Peut-être avez-vous oublié de re/monter ou de brancher le support de stockage sur lequel il pointe.",
+ "migrate_tsig_end": "La migration à HMAC-SHA-512 est terminée",
"migrate_tsig_failed": "La migration du domaine DynDNS {domain} à hmac-sha512 a échoué. Annulation des modifications. Erreur : {error_code} - {error}",
- "migrate_tsig_start": "L’algorithme de génération des clefs n’est pas suffisamment sécurisé pour la signature TSIG du domaine '{domain}', lancement de la migration vers hmac-sha512 qui est plus sécurisé",
- "migrate_tsig_wait": "Attendre 3 minutes pour que le serveur DynDNS prenne en compte la nouvelle clef …",
+ "migrate_tsig_start": "L’algorithme de génération des clefs n’est pas suffisamment sécurisé pour la signature TSIG du domaine '{domain}', lancement de la migration vers HMAC-SHA-512 qui est plus sécurisé",
+ "migrate_tsig_wait": "Attendre trois minutes pour que le serveur DynDNS prenne en compte la nouvelle clef …",
"migrate_tsig_wait_2": "2 minutes …",
"migrate_tsig_wait_3": "1 minute …",
- "migrate_tsig_wait_4": "30 secondes …",
- "migrate_tsig_not_needed": "Il ne semble pas que vous utilisez un domaine DynDNS, donc aucune migration n’est nécessaire !",
- "app_checkurl_is_deprecated": "Packagers /!\\ 'app checkurl' est obsolète ! Utilisez 'app register-url' en remplacement !",
+ "migrate_tsig_wait_4": "30 secondes …",
+ "migrate_tsig_not_needed": "Il ne semble pas que vous utilisez un domaine DynDNS, donc aucune migration n’est nécessaire.",
"migration_description_0001_change_cert_group_to_sslcert": "Changement des permissions de groupe des certificats de « metronome » à « ssl-cert »",
"migration_description_0002_migrate_to_tsig_sha256": "Amélioration de la sécurité de DynDNS TSIG en utilisant SHA512 au lieu de MD5",
"migration_description_0003_migrate_to_stretch": "Mise à niveau du système vers Debian Stretch et YunoHost 3.0",
- "migration_0003_backward_impossible": "La migration Stretch n’est pas réversible.",
"migration_0003_start": "Démarrage de la migration vers Stretch. Les journaux seront disponibles dans {logfile}.",
"migration_0003_patching_sources_list": "Modification du fichier sources.lists …",
"migration_0003_main_upgrade": "Démarrage de la mise à niveau principale …",
"migration_0003_fail2ban_upgrade": "Démarrage de la mise à niveau de fail2ban …",
- "migration_0003_restoring_origin_nginx_conf": "Votre fichier /etc/nginx/nginx.conf a été modifié d’une manière ou d’une autre. La migration va d’abords le réinitialiser à son état initial. Le fichier précédent sera disponible en tant que {backup_dest}.",
+ "migration_0003_restoring_origin_nginx_conf": "Votre fichier /etc/nginx/nginx.conf a été modifié d’une manière ou d’une autre. La migration va d’abord le réinitialiser à son état initial. Le fichier précédent sera disponible en tant que {backup_dest}.",
"migration_0003_yunohost_upgrade": "Démarrage de la mise à niveau du paquet YunoHost. La migration se terminera, mais la mise à jour réelle aura lieu immédiatement après. Une fois cette opération terminée, vous pourriez avoir à vous reconnecter à l’administration via le panel web.",
"migration_0003_not_jessie": "La distribution Debian actuelle n’est pas Jessie !",
- "migration_0003_system_not_fully_up_to_date": "Votre système n’est pas complètement à jour. Veuillez mener une mise à jour classique avant de lancer à migration à Stretch.",
+ "migration_0003_system_not_fully_up_to_date": "Votre système n’est pas complètement à jour. Veuillez mener une mise à jour classique avant de lancer la migration à Stretch.",
"migration_0003_still_on_jessie_after_main_upgrade": "Quelque chose s’est mal passé pendant la mise à niveau principale : le système est toujours sur Debian Jessie !? Pour investiguer sur le problème, veuillez regarder les journaux {log}:s …",
- "migration_0003_general_warning": "Veuillez noter que cette migration est une opération délicate. Si l’équipe YunoHost a fait de son mieux pour la relire et la tester, la migration pourrait tout de même casser des parties de votre système ou de vos applications.\n\nEn conséquence, nous vous recommandons :\n - de lancer une sauvegarde de vos données ou applications critiques. Plus d’informations sur https://yunohost.org/backup ;\n - d’être patient après avoir lancé la migration : selon votre connexion internet et matériel, cela pourrait prendre jusqu’à quelques heures pour que tout soit à niveau.\n\nEn outre, le port SMTP utilisé par les clients de messagerie externes comme (Thunderbird ou K9-Mail) a été changé de 465 (SSL/TLS) à 587 (STARTTLS). L’ancien port 465 sera automatiquement fermé et le nouveau port 587 sera ouvert dans le pare-feu. Vous et vos utilisateurs *devront* adapter la configuration de vos clients de messagerie en conséquence !",
- "migration_0003_problematic_apps_warning": "Veuillez noter que des applications possiblement problématiques ont été détectées. Il semble qu’elles n’aient pas été installées depuis une liste d’application ou qu’elles ne soit pas marquées comme « fonctionnelles ». En conséquence, nous ne pouvons pas garantir qu’elles fonctionneront après la mise à niveau : {problematic_apps}",
+ "migration_0003_general_warning": "Veuillez noter que cette migration est une opération délicate. Si l’équipe YunoHost a fait de son mieux pour la relire et la tester, la migration pourrait tout de même casser des parties de votre système ou de vos applications.\n\nEn conséquence, nous vous recommandons :\n - de lancer une sauvegarde de vos données ou applications critiques. Plus d’informations sur https://yunohost.org/backup ;\n - d’être patient après avoir lancé la migration : selon votre connexion internet et matériel, cela pourrait prendre jusqu’à quelques heures pour que tout soit à niveau.\n\nEn outre, le port SMTP utilisé par les clients de messagerie externes comme (Thunderbird ou K9-Mail) a été changé de 465 (SSL/TLS) à 587 (STARTTLS). L’ancien port 465 sera automatiquement fermé et le nouveau port 587 sera ouvert dans le pare-feu. Vous et vos utilisateurs *devront* adapter la configuration de vos clients de messagerie en conséquence.",
+ "migration_0003_problematic_apps_warning": "Veuillez noter que les applications installées potentiellement problématiques suivantes ont été détectées. Il semble que celles-ci n’ont pas été installées à partir d’un catalogue d’applications, ou ne sont pas marquées comme \"fonctionnelle\". Par conséquent, il ne peut pas être garanti qu’ils fonctionneront toujours après la mise à niveau: {problematic_apps}",
"migration_0003_modified_files": "Veuillez noter que les fichiers suivants ont été détectés comme modifiés manuellement et pourraient être écrasés à la fin de la mise à niveau : {manually_modified_files}",
"migrations_list_conflict_pending_done": "Vous ne pouvez pas utiliser --previous et --done simultanément.",
- "migrations_to_be_ran_manually": "La migration {number} {name} doit être lancée manuellement. Veuillez aller dans Outils > Migrations dans l’interface admin, ou lancer `yunohost tools migrations migrate`.",
- "migrations_need_to_accept_disclaimer": "Pour lancer la migration {number} {name}, vous devez accepter cette clause de non-responsabilité :\n---\n{disclaimer}\n---\nSi vous acceptez de lancer la migration, veuillez relancer la commande avec l’option --accept-disclaimer.",
- "service_description_avahi-daemon": "permet d’atteindre votre serveur via yunohost.local sur votre réseau local",
- "service_description_dnsmasq": "gère la résolution des noms de domaine (DNS)",
- "service_description_dovecot": "permet aux clients de messagerie d’accéder/récupérer les courriels (via IMAP et POP3)",
- "service_description_fail2ban": "protège contre les attaques brute-force et autres types d’attaques venant d’Internet",
- "service_description_glances": "surveille les informations système de votre serveur",
- "service_description_metronome": "gère les comptes de messagerie instantanée XMPP",
- "service_description_mysql": "stocke les données des applications (bases de données SQL)",
- "service_description_nginx": "sert ou permet l’accès à tous les sites web hébergés sur votre serveur",
- "service_description_nslcd": "gère la connexion en ligne de commande des utilisateurs YunoHost",
- "service_description_php5-fpm": "exécute des applications écrites en PHP avec nginx",
- "service_description_postfix": "utilisé pour envoyer et recevoir des courriels",
- "service_description_redis-server": "une base de données spécialisée utilisée pour l’accès rapide aux données, les files d’attentes et la communication entre les programmes",
- "service_description_rmilter": "vérifie divers paramètres dans les courriels",
- "service_description_rspamd": "filtre le pourriel, et d’autres fonctionnalités liées au courriel",
- "service_description_slapd": "stocke les utilisateurs, domaines et leurs informations liées",
- "service_description_ssh": "vous permet de vous connecter à distance à votre serveur via un terminal (protocole SSH)",
- "service_description_yunohost-api": "permet les interactions entre l’interface web de YunoHost et le système",
- "service_description_yunohost-firewall": "gère l'ouverture et la fermeture des ports de connexion aux services",
+ "migrations_to_be_ran_manually": "La migration {id} doit être lancée manuellement. Veuillez aller dans Outils > Migrations dans l’interface admin, ou lancer `yunohost tools migrations migrate`.",
+ "migrations_need_to_accept_disclaimer": "Pour lancer la migration {id}, vous devez accepter cette clause de non-responsabilité :\n---\n{disclaimer}\n---\nSi vous acceptez de lancer la migration, veuillez relancer la commande avec l’option --accept-disclaimer.",
+ "service_description_avahi-daemon": "Vous permet d’atteindre votre serveur en utilisant «yunohost.local» sur votre réseau local",
+ "service_description_dnsmasq": "Gère la résolution des noms de domaine (DNS)",
+ "service_description_dovecot": "Permet aux clients de messagerie d’accéder/récupérer les courriels (via IMAP et POP3)",
+ "service_description_fail2ban": "Protège contre les attaques brute-force et autres types d’attaques venant d’Internet",
+ "service_description_metronome": "Gère les comptes de messagerie instantanée XMPP",
+ "service_description_mysql": "Stocke les données des applications (bases de données SQL)",
+ "service_description_nginx": "Sert ou permet l’accès à tous les sites web hébergés sur votre serveur",
+ "service_description_nslcd": "Gère la connexion en ligne de commande des utilisateurs YunoHost",
+ "service_description_postfix": "Utilisé pour envoyer et recevoir des courriels",
+ "service_description_redis-server": "Une base de données spécialisée utilisée pour l’accès rapide aux données, les files d’attentes et la communication entre les programmes",
+ "service_description_rspamd": "Filtre le pourriel, et d’autres fonctionnalités liées au courriel",
+ "service_description_slapd": "Stocke les utilisateurs, domaines et leurs informations liées",
+ "service_description_ssh": "Vous permet de vous connecter à distance à votre serveur via un terminal (protocole SSH)",
+ "service_description_yunohost-api": "Permet les interactions entre l’interface web de YunoHost et le système",
+ "service_description_yunohost-firewall": "Gère l’ouverture et la fermeture des ports de connexion aux services",
"experimental_feature": "Attention : cette fonctionnalité est expérimentale et ne doit pas être considérée comme stable, vous ne devriez pas l’utiliser à moins que vous ne sachiez ce que vous faites.",
- "log_corrupted_md_file": "Le fichier yaml de métadonnées associé aux logs est corrompu : '{md_file}'\nErreur : {error}",
+ "log_corrupted_md_file": "Le fichier YAML de métadonnées associé aux logs est corrompu : '{md_file}'\nErreur : {error}",
"log_category_404": "Le journal de la catégorie '{category}' n’existe pas",
- "log_link_to_log": "Journal historisé complet de cette opération : ' {desc} '",
- "log_help_to_get_log": "Pour voir le journal historisé de cette opération '{desc}', utilisez la commande 'yunohost log display {name}'",
- "log_link_to_failed_log": "L’opération '{desc}' a échouée ! Pour avoir de l’aide, merci de fournir le journal historisé complet de l’opération en cliquant ici",
- "backup_php5_to_php7_migration_may_fail": "Impossible de convertir votre archive pour prendre en charge php7, vos applications php pourraient ne pas être restaurées (reason: {error:s})",
- "log_help_to_get_failed_log": "L’opération '{desc}' a échouée ! Pour avoir de l’aide, merci de partager le journal historisé de cette opération en utilisant la commande 'yunohost log display {name} --share'",
- "log_does_exists": "Il n’existe pas de journal historisé de l’opération ayant pour nom '{log}', utiliser 'yunohost log list pour voir tous les fichiers de journaux historisés disponibles'",
+ "log_link_to_log": "Journal complet de cette opération : ' {desc} '",
+ "log_help_to_get_log": "Pour voir le journal de cette opération '{desc}', utilisez la commande 'yunohost log display {name}'",
+ "log_link_to_failed_log": "L’opération '{desc}' a échoué ! Pour obtenir de l’aide, merci de partager le journal de l’opération en cliquant ici",
+ "backup_php5_to_php7_migration_may_fail": "Impossible de convertir votre archive pour prendre en charge PHP 7, vous pourriez ne plus pouvoir restaurer vos applications PHP (cause : {error:s})",
+ "log_help_to_get_failed_log": "L’opération '{desc}' a échoué ! Pour obtenir de l’aide, merci de partager le journal de l’opération en utilisant la commande 'yunohost log display {name} --share'",
+ "log_does_exists": "Il n’existe pas de journal de l’opération ayant pour nom '{log}', utiliser 'yunohost log list' pour voir tous les fichiers de journaux disponibles",
"log_operation_unit_unclosed_properly": "L’opération ne s’est pas terminée correctement",
- "log_app_addaccess": "Ajouter l’accès à '{}'",
- "log_app_removeaccess": "Enlever l’accès à '{}'",
- "log_app_clearaccess": "Retirer tous les accès à '{}'",
- "log_app_fetchlist": "Ajouter une liste d’application",
- "log_app_removelist": "Enlever une liste d’application",
"log_app_change_url": "Changer l’URL de l’application '{}'",
"log_app_install": "Installer l’application '{}'",
"log_app_remove": "Enlever l’application '{}'",
"log_app_upgrade": "Mettre à jour l’application '{}'",
"log_app_makedefault": "Faire de '{}' l’application par défaut",
- "log_available_on_yunopaste": "Le journal historisé est désormais disponible via {url}",
+ "log_available_on_yunopaste": "Le journal est désormais disponible via {url}",
"log_backup_restore_system": "Restaurer le système depuis une archive de sauvegarde",
"log_backup_restore_app": "Restaurer '{}' depuis une sauvegarde",
"log_remove_on_failed_restore": "Retirer '{}' après un échec de restauration depuis une archive de sauvegarde",
@@ -449,84 +323,78 @@
"log_letsencrypt_cert_install": "Installer le certificat Let’s Encrypt sur le domaine '{}'",
"log_selfsigned_cert_install": "Installer le certificat auto-signé sur le domaine '{}'",
"log_letsencrypt_cert_renew": "Renouveler le certificat Let’s Encrypt de '{}'",
- "log_service_enable": "Activer le service '{}'",
- "log_service_regen_conf": "Régénérer la configuration système de '{}'",
"log_user_create": "Ajouter l’utilisateur '{}'",
"log_user_delete": "Supprimer l’utilisateur '{}'",
"log_user_update": "Mettre à jour les informations de l’utilisateur '{}'",
- "log_tools_maindomain": "Faire de '{}' le domaine principal",
- "log_tools_migrations_migrate_forward": "Migrer vers",
- "log_tools_migrations_migrate_backward": "Revenir en arrière",
+ "log_domain_main_domain": "Faire de '{}' le domaine principal",
+ "log_tools_migrations_migrate_forward": "Éxecuter les migrations",
"log_tools_postinstall": "Faire la post-installation de votre serveur YunoHost",
"log_tools_upgrade": "Mettre à jour les paquets du système",
"log_tools_shutdown": "Éteindre votre serveur",
"log_tools_reboot": "Redémarrer votre serveur",
"mail_unavailable": "Cette adresse de courriel est réservée et doit être automatiquement attribuée au tout premier utilisateur",
- "migration_description_0004_php5_to_php7_pools": "Reconfiguration des groupes PHP pour utiliser PHP 7 au lieu de PHP 5",
+ "migration_description_0004_php5_to_php7_pools": "Reconfigurer les espaces utilisateurs PHP pour utiliser PHP 7 au lieu de PHP 5",
"migration_description_0005_postgresql_9p4_to_9p6": "Migration des bases de données de PostgreSQL 9.4 vers PostgreSQL 9.6",
"migration_0005_postgresql_94_not_installed": "PostgreSQL n’a pas été installé sur votre système. Rien à faire !",
"migration_0005_postgresql_96_not_installed": "PostgreSQL 9.4 a été trouvé et installé, mais pas PostgreSQL 9.6 !? Quelque chose d’étrange a dû arriver à votre système… :(",
- "migration_0005_not_enough_space": "Il n’y a pas assez d’espace libre de disponible sur {path} pour lancer maintenant la migration :(.",
- "recommend_to_add_first_user": "La post-installation est terminée mais YunoHost a besoin d’au moins un utilisateur pour fonctionner correctement. Vous devez en ajouter un en utilisant la commande 'yunohost user create $nomdutilisateur' ou bien via l’interface d’administration web.",
- "service_description_php7.0-fpm": "exécute des applications écrites en PHP avec Nginx",
+ "migration_0005_not_enough_space": "Laissez suffisamment d’espace disponible dans {path} pour exécuter la migration.",
+ "service_description_php7.0-fpm": "Exécute des applications écrites en PHP avec NGINX",
"users_available": "Liste des utilisateurs disponibles :",
"good_practices_about_admin_password": "Vous êtes maintenant sur le point de définir un nouveau mot de passe d’administration. Le mot de passe doit comporter au moins 8 caractères – bien qu’il soit recommandé d’utiliser un mot de passe plus long (c’est-à-dire une phrase secrète) et/ou d’utiliser différents types de caractères (majuscules, minuscules, chiffres et caractères spéciaux).",
"good_practices_about_user_password": "Vous êtes maintenant sur le point de définir un nouveau mot de passe utilisateur. Le mot de passe doit comporter au moins 8 caractères - bien qu’il soit recommandé d’utiliser un mot de passe plus long (c’est-à-dire une phrase secrète) et/ou d’utiliser différents types de caractères tels que : majuscules, minuscules, chiffres et caractères spéciaux.",
"migration_description_0006_sync_admin_and_root_passwords": "Synchroniser les mots de passe admin et root",
- "migration_0006_disclaimer": "YunoHost s’attendra désormais à ce que les mots de passe admin et root soient synchronisés. En exécutant cette migration, votre mot de passe root sera remplacé par le mot de passe administrateur.",
- "migration_0006_done": "Votre mot de passe root a été remplacé par celui de votre adminitrateur.",
- "password_listed": "Ce mot de passe est l'un des mots de passe les plus utilisés dans le monde. Veuillez choisir quelque chose d'un peu plus singulier.",
+ "migration_0006_disclaimer": "YunoHost s’attend maintenant à ce que les mots de passe administrateur et racine soient synchronisés. Cette migration remplace votre mot de passe root par le mot de passe administrateur.",
+ "password_listed": "Ce mot de passe est l’un des mots de passe les plus utilisés dans le monde. Veuillez choisir quelque chose d’un peu plus singulier.",
"password_too_simple_1": "Le mot de passe doit comporter au moins 8 caractères",
"password_too_simple_2": "Le mot de passe doit comporter au moins 8 caractères et contenir des chiffres, des majuscules et des minuscules",
"password_too_simple_3": "Le mot de passe doit comporter au moins 8 caractères et contenir des chiffres, des majuscules, des minuscules et des caractères spéciaux",
"password_too_simple_4": "Le mot de passe doit comporter au moins 12 caractères et contenir des chiffres, des majuscules, des minuscules et des caractères spéciaux",
"root_password_desynchronized": "Le mot de passe administrateur a été changé, mais YunoHost n’a pas pu le propager au mot de passe root !",
- "aborting": "Opération annulée.",
- "app_not_upgraded": "Les applications suivantes n'ont pas été mises à jour : {apps}",
- "app_start_install": "Installation de l'application {app} …",
- "app_start_remove": "Suppression de l'application {app} …",
- "app_start_backup": "Collecte des fichiers devant être sauvegardés pour {app} …",
- "app_start_restore": "Restauration de l'application {app} …",
+ "aborting": "Annulation.",
+ "app_not_upgraded": "L’application {failed_app} n’a pas été mise à jour et par conséquence les applications suivantes n’ont pas été mises à jour : {apps}",
+ "app_start_install": "Installation de l’application {app} …",
+ "app_start_remove": "Suppression de l’application {app} …",
+ "app_start_backup": "Collecte des fichiers devant être sauvegardés pour l’application {app} …",
+ "app_start_restore": "Restauration de l’application {app} …",
"app_upgrade_several_apps": "Les applications suivantes seront mises à jour : {apps}",
"ask_new_domain": "Nouveau domaine",
"ask_new_path": "Nouveau chemin",
- "backup_actually_backuping": "Création d'une archive de sauvegarde à partir des fichiers collectés …",
- "backup_mount_archive_for_restore": "Préparation de l'archive pour restauration …",
- "confirm_app_install_warning": "Avertissement : cette application peut fonctionner mais n'est pas bien intégrée dans YunoHost. Certaines fonctionnalités telles que l'authentification unique et la sauvegarde/restauration peuvent ne pas être disponibles. L'installer quand même ? [{answers:s}] ",
- "confirm_app_install_danger": "AVERTISSEMENT ! Cette application est encore expérimentale (explicitement, elle ne fonctionne pas) et risque de casser votre système ! Vous ne devriez probablement PAS l'installer sans savoir ce que vous faites. Êtes-vous prêt à prendre ce risque ? [{answers:s}] ",
- "confirm_app_install_thirdparty": "AVERTISSEMENT ! L'installation d'applications tierces peut compromettre l'intégrité et la sécurité de votre système. Vous ne devriez probablement PAS l'installer si vous ne savez pas ce que vous faites. Êtes-vous prêt à prendre ce risque ? [{answers:s}] ",
+ "backup_actually_backuping": "Création d’une archive de sauvegarde à partir des fichiers collectés …",
+ "backup_mount_archive_for_restore": "Préparation de l’archive pour restauration …",
+ "confirm_app_install_warning": "Avertissement : cette application peut fonctionner mais n’est pas bien intégrée dans YunoHost. Certaines fonctionnalités telles que l’authentification unique et la sauvegarde/restauration peuvent ne pas être disponibles. L’installer quand même ? [{answers:s}] ",
+ "confirm_app_install_danger": "DANGER ! Cette application est connue pour être encore expérimentale (si elle ne fonctionne pas explicitement) ! Vous ne devriez probablement PAS l’installer à moins de savoir ce que vous faites. AUCUN SUPPORT ne sera fourni si cette application ne fonctionne pas ou casse votre système … Si vous êtes prêt à prendre ce risque de toute façon, tapez '{answers:s}'",
+ "confirm_app_install_thirdparty": "DANGER ! Cette application ne fait pas partie du catalogue d’applications de YunoHost. L’installation d’applications tierces peut compromettre l’intégrité et la sécurité de votre système. Vous ne devriez probablement PAS l’installer à moins de savoir ce que vous faites. AUCUN SUPPORT ne sera fourni si cette application ne fonctionne pas ou casse votre système … Si vous êtes prêt à prendre ce risque de toute façon, tapez '{answers:s}'",
"dpkg_is_broken": "Vous ne pouvez pas faire ça maintenant car dpkg/apt (le gestionnaire de paquets du système) semble avoir laissé des choses non configurées. Vous pouvez essayer de résoudre ce problème en vous connectant via SSH et en exécutant `sudo dpkg --configure -a'.",
"dyndns_could_not_check_available": "Impossible de vérifier si {domain:s} est disponible chez {provider:s}.",
- "file_does_not_exist": "Le fichier dont le chemin est {path:s} n'existe pas.",
+ "file_does_not_exist": "Le fichier dont le chemin est {path:s} n’existe pas.",
"global_settings_setting_security_password_admin_strength": "Qualité du mot de passe administrateur",
- "global_settings_setting_security_password_user_strength": "Qualité du mot de passe de l'utilisateur",
- "global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "Autoriser l'utilisation de la clé hôte DSA (obsolète) pour la configuration du service SSH",
+ "global_settings_setting_security_password_user_strength": "Qualité du mot de passe de l’utilisateur",
+ "global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "Autoriser l’utilisation de la clé hôte DSA (obsolète) pour la configuration du service SSH",
"hook_json_return_error": "Échec de la lecture au retour du script {path:s}. Erreur : {msg:s}. Contenu brut : {raw_content}",
"migration_description_0007_ssh_conf_managed_by_yunohost_step1": "La configuration SSH sera gérée par YunoHost (étape 1, automatique)",
"migration_description_0008_ssh_conf_managed_by_yunohost_step2": "La configuration SSH sera gérée par YunoHost (étape 2, manuelle)",
- "migration_0007_cancelled": "YunoHost n'a pas réussi à améliorer la façon dont est gérée votre configuration SSH.",
- "migration_0007_cannot_restart": "SSH ne peut pas être redémarré après avoir essayé d'annuler la migration numéro 6.",
+ "migration_0007_cancelled": "Impossible d’améliorer la gestion de votre configuration SSH.",
+ "migration_0007_cannot_restart": "SSH ne peut pas être redémarré après avoir essayé d’annuler la migration numéro 6.",
"migration_0008_general_disclaimer": "Pour améliorer la sécurité de votre serveur, il est recommandé de laisser YunoHost gérer la configuration SSH. Votre configuration SSH actuelle diffère de la configuration recommandée. Si vous laissez YunoHost la reconfigurer, la façon dont vous vous connectez à votre serveur via SSH changera comme suit :",
- "migration_0008_port": " - vous devrez vous connecter en utilisant le port 22 au lieu de votre actuel port SSH personnalisé. N'hésitez pas à le reconfigurer ;",
- "migration_0008_root": " - vous ne pourrez pas vous connecter en tant que root via SSH. Au lieu de cela, vous devrez utiliser l'utilisateur admin ;",
- "migration_0008_dsa": " - la clé DSA sera désactivée. Par conséquent, il se peut que vous ayez besoin d'invalider un avertissement effrayant de votre client SSH afin de revérifier l'empreinte de votre serveur ;",
- "migration_0008_warning": "Si vous comprenez ces avertissements et que vous acceptez de laisser YunoHost remplacer votre configuration actuelle, exécutez la migration. Sinon, vous pouvez également passer la migration, bien que cela ne soit pas recommandé.",
- "migration_0008_no_warning": "Aucun risque majeur n'a été identifié concernant l'écrasement de votre configuration SSH - mais nous ne pouvons pas en être absolument sûrs ;) ! Si vous acceptez de laisser YunoHost remplacer votre configuration actuelle, exécutez la migration. Sinon, vous pouvez également passer la migration, bien que cela ne soit pas recommandé.",
+ "migration_0008_port": "- Vous devrez vous connecter en utilisant le port 22 au lieu de votre actuel port SSH personnalisé. N’hésitez pas à le reconfigurer ;",
+ "migration_0008_root": "- Vous ne pourrez pas vous connecter en tant que root via SSH. Au lieu de cela, vous devrez utiliser l’utilisateur admin ;",
+ "migration_0008_dsa": "- La clé DSA sera désactivée. Par conséquent, il se peut que vous ayez besoin d’invalider un avertissement effrayant de votre client SSH afin de revérifier l’empreinte de votre serveur ;",
+ "migration_0008_warning": "Si vous comprenez ces avertissements et souhaitez que YunoHost écrase votre configuration actuelle, exécutez la migration. Sinon, vous pouvez également ignorer la migration, bien que cela ne soit pas recommandé.",
+ "migration_0008_no_warning": "Remplacer votre configuration SSH devrait être sûr, bien que cela ne puisse être promis ! Exécutez la migration pour la remplacer. Sinon, vous pouvez également ignorer la migration, bien que cela ne soit pas recommandé.",
"migrations_success": "Migration {number} {name} réussie !",
- "pattern_password_app": "Désolé, les mots de passe ne doivent pas contenir les caractères suivants : {forbidden_chars}",
+ "pattern_password_app": "Désolé, les mots de passe ne peuvent pas contenir les caractères suivants : {forbidden_chars}",
"root_password_replaced_by_admin_password": "Votre mot de passe root a été remplacé par votre mot de passe administrateur.",
- "service_conf_now_managed_by_yunohost": "Le fichier de configuration '{conf}' est maintenant géré par YunoHost.",
"service_reload_failed": "Impossible de recharger le service '{service:s}'.\n\nJournaux historisés récents de ce service : {logs:s}",
- "service_reloaded": "Le service '{service:s}' a été rechargé",
+ "service_reloaded": "Le service « {service:s} » a été rechargé",
"service_restart_failed": "Impossible de redémarrer le service '{service:s}'\n\nJournaux historisés récents de ce service : {logs:s}",
- "service_restarted": "Le service '{service:s}' a été redémarré",
+ "service_restarted": "Le service « {service:s} » a été redémarré",
"service_reload_or_restart_failed": "Impossible de recharger ou de redémarrer le service '{service:s}'\n\nJournaux historisés récents de ce service : {logs:s}",
- "service_reloaded_or_restarted": "Le service '{service:s}' a été rechargé ou redémarré",
+ "service_reloaded_or_restarted": "Le service « {service:s} » a été rechargé ou redémarré",
"this_action_broke_dpkg": "Cette action a laissé des paquets non configurés par dpkg/apt (les gestionnaires de paquets système). Vous pouvez essayer de résoudre ce problème en vous connectant via SSH et en exécutant `sudo dpkg --configure -a`.",
- "app_action_cannot_be_ran_because_required_services_down": "Cette application requiert certains services qui sont actuellement arrêtés. Avant de continuer, vous devriez essayer de redémarrer les services suivants (et éventuellement rechercher pourquoi ils sont arrêtés) : {services}",
- "admin_password_too_long": "Choisissez un mot de passe plus court que 127 caractères",
+ "app_action_cannot_be_ran_because_required_services_down": "Ces services requis doivent être en cours d’exécution pour exécuter cette action: {services}. Essayez de les redémarrer pour continuer (et éventuellement rechercher pourquoi ils sont en panne).",
+ "admin_password_too_long": "Veuillez choisir un mot de passe de moins de 127 caractères",
"log_regen_conf": "Régénérer les configurations du système '{}'",
- "migration_0009_not_needed": "Cette migration semble avoir déjà été jouée ? On l'ignore.",
+ "migration_0009_not_needed": "Cette migration semble avoir déjà été jouée ? On l’ignore.",
"regenconf_file_backed_up": "Le fichier de configuration '{conf}' a été sauvegardé sous '{backup}'",
"regenconf_file_copy_failed": "Impossible de copier le nouveau fichier de configuration '{new}' vers '{conf}'",
"regenconf_file_manually_modified": "Le fichier de configuration '{conf}' a été modifié manuellement et ne sera pas mis à jour",
@@ -536,57 +404,209 @@
"regenconf_file_updated": "Le fichier de configuration '{conf}' a été mis à jour",
"regenconf_now_managed_by_yunohost": "Le fichier de configuration '{conf}' est maintenant géré par YunoHost (catégorie {category}).",
"regenconf_up_to_date": "La configuration est déjà à jour pour la catégorie '{category}'",
- "already_up_to_date": "Il n'y a rien à faire ! Tout est déjà à jour !",
- "global_settings_setting_security_nginx_compatibility": "Compatibilité versus compromis sécuritaire pour le serveur web nginx. Affecte les cryptogrammes (et d'autres aspects liés à la sécurité)",
- "global_settings_setting_security_ssh_compatibility": "Compatibilité versus compromis sécuritaire pour le serveur SSH. Affecte les cryptogrammes (et d'autres aspects liés à la sécurité)",
- "global_settings_setting_security_postfix_compatibility": "Compatibilité versus compromis sécuritaire pour le serveur Postfix. Affecte les cryptogrammes (et d'autres aspects liés à la sécurité)",
+ "already_up_to_date": "Il n’y a rien à faire ! Tout est déjà à jour !",
+ "global_settings_setting_security_nginx_compatibility": "Compatibilité versus compromis sécuritaire pour le serveur web nginx. Affecte les cryptogrammes (et d’autres aspects liés à la sécurité)",
+ "global_settings_setting_security_ssh_compatibility": "Compatibilité versus compromis sécuritaire pour le serveur SSH. Affecte les cryptogrammes (et d’autres aspects liés à la sécurité)",
+ "global_settings_setting_security_postfix_compatibility": "Compatibilité versus compromis sécuritaire pour le serveur Postfix. Affecte les cryptogrammes (et d’autres aspects liés à la sécurité)",
"migration_description_0009_decouple_regenconf_from_services": "Dissocier le mécanisme « regen-conf » des services",
- "migration_description_0010_migrate_to_apps_json": "Supprimer les listes d'applications obsolètes et utiliser la nouvelle liste unifiée 'apps.json' à la place",
+ "migration_description_0010_migrate_to_apps_json": "Supprimer les catalogues d’applications obsolètes afin d’utiliser la nouvelle liste unifiée 'apps.json' à la place (les anciens catalogues seront remplacés durant la migration 13)",
"regenconf_file_kept_back": "Le fichier de configuration '{conf}' devait être supprimé par « regen-conf » (catégorie {category}) mais a été conservé.",
- "regenconf_updated": "La configuration a été mise à jour pour la catégorie '{category}'",
+ "regenconf_updated": "La configuration a été mise à jour pour '{category}'",
"regenconf_would_be_updated": "La configuration aurait dû être mise à jour pour la catégorie '{category}'",
"regenconf_dry_pending_applying": "Vérification de la configuration en attente qui aurait été appliquée pour la catégorie '{category}' …",
"regenconf_failed": "Impossible de régénérer la configuration pour la ou les catégorie(s) : '{categories}'",
"regenconf_pending_applying": "Applique la configuration en attente pour la catégorie '{category}' …",
"service_regen_conf_is_deprecated": "'yunohost service regen-conf' est obsolète ! Veuillez plutôt utiliser 'yunohost tools regen-conf' à la place.",
- "tools_upgrade_at_least_one": "Veuillez spécifier --apps OU --system",
+ "tools_upgrade_at_least_one": "Veuillez spécifier '--apps' OU '--system'",
"tools_upgrade_cant_both": "Impossible de mettre à niveau le système et les applications en même temps",
- "tools_upgrade_cant_hold_critical_packages": "Impossibilité de maintenir les paquets critiques...",
- "tools_upgrade_regular_packages": "Mise à jour des paquets du système (non liés a YunoHost) ...",
+ "tools_upgrade_cant_hold_critical_packages": "Impossibilité de maintenir les paquets critiques…",
+ "tools_upgrade_regular_packages": "Mise à jour des paquets du système (non liés a YunoHost) …",
"tools_upgrade_regular_packages_failed": "Impossible de mettre à jour les paquets suivants : {packages_list}",
- "tools_upgrade_special_packages": "Mise à jour des paquets 'spécifiques' (liés a YunoHost) ...",
- "tools_upgrade_special_packages_completed": "La mise à jour des paquets de YunoHost est finie!\nPressez [Entrée] pour revenir à la ligne de commande",
- "updating_app_lists": "Récupération des mises à jour des applications disponibles…",
- "dpkg_lock_not_available": "Cette commande ne peut être lancée maintenant car il semblerai qu'un autre programme utilise déjà le verrou dpkg du gestionnaire de paquets du système",
- "tools_upgrade_cant_unhold_critical_packages": "Impossible de dé-marquer les paquets critiques ...",
- "tools_upgrade_special_packages_explanation": "Cette opération prendra fin mais la mise à jour spécifique continuera en arrière-plan. Veuillez ne pas lancer d'autre action sur votre serveur dans les 10 prochaines minutes (en fonction de la vitesse de votre matériel). Une fois que c'est fait, vous devrez peut-être vous reconnecter sur le panel d'administration web. Le journal de la mise à jour sera disponible dans Outils > Log (dans le panel d'administration web) ou dans la liste des journaux YunoHost (en ligne de commande).",
+ "tools_upgrade_special_packages": "Mise à jour des paquets 'spécifiques' (liés a YunoHost) …",
+ "tools_upgrade_special_packages_completed": "La mise à jour des paquets de YunoHost est finie !\nPressez [Entrée] pour revenir à la ligne de commande",
+ "dpkg_lock_not_available": "Cette commande ne peut être exécutée actuellement car un autre programme semble utiliser le verrou de dpkg (gestionnaire de paquets)",
+ "tools_upgrade_cant_unhold_critical_packages": "Impossible de conserver les paquets critiques…",
+ "tools_upgrade_special_packages_explanation": "La mise à jour spéciale va continuer en arrière-plan. Veuillez ne pas lancer d’autres actions sur votre serveur pendant environ 10 minutes (en fonction de la vitesse du matériel). Après cela, il vous faudra peut-être vous reconnecter à la webadmin. Le journal de mise à niveau sera disponible dans Outils → Journal (dans la webadmin) ou via \"yunohost log list\" (en ligne de commande).",
"update_apt_cache_failed": "Impossible de mettre à jour le cache APT (gestionnaire de paquets Debian). Voici un extrait du fichier sources.list qui pourrait vous aider à identifier les lignes problématiques :\n{sourceslist}",
"update_apt_cache_warning": "Des erreurs se sont produites lors de la mise à jour du cache APT (gestionnaire de paquets Debian). Voici un extrait des lignes du fichier sources.list qui pourrait vous aider à identifier les lignes problématiques :\n{sourceslist}",
- "apps_permission_not_found": "Aucune permission trouvée pour les applications installées",
- "apps_permission_restoration_failed": "L'autorisation '{permission:s}' pour la restauration de l'application {app:s} a échoué",
- "backup_permission": "Autorisation de sauvegarde pour l'application {app:s}",
- "edit_group_not_allowed": "Vous n'êtes pas autorisé à modifier le groupe {group:s}",
- "error_when_removing_sftpuser_group": "Erreur en essayant de supprimer le groupe sftpusers",
- "group_created": "Le groupe '{group}' a créé avec succès",
- "group_deleted": "Le groupe '{group}' a été supprimé",
- "group_deletion_not_allowed": "Le groupe {group:s} ne peut pas être supprimé manuellement.",
- "group_info_failed": "L'information sur le groupe a échoué",
+ "backup_permission": "Permission de sauvegarde pour l’application {app:s}",
+ "group_created": "Le groupe '{group}' a été créé",
+ "group_deleted": "Suppression du groupe '{group}'",
"group_unknown": "Le groupe {group:s} est inconnu",
"group_updated": "Le groupe '{group}' a été mis à jour",
- "group_update_failed": "La mise à jour du groupe '{group}' a échoué",
- "group_already_allowed": "Le groupe '{group:s}' a déjà la permission '{permission:s}' activée pour l'application '{app:s}'",
- "group_already_disallowed": "Le groupe '{group:s}' a déjà la permission '{permission:s}' désactivée pour l'application '{app:s}'",
- "group_name_already_exist": "Le groupe {name:s} existe déjà",
- "group_creation_failed": "Échec de la création du groupe '{group}'",
- "group_deletion_failed": "Échec de la suppression du groupe '{group}'",
- "edit_permission_with_group_all_users_not_allowed": "Vous n'êtes pas autorisé à modifier les permissions pour le groupe 'all_users', utilisez 'yunohost user permission clear APP' ou 'yunohost user permission add APP -u USER' à la place.",
- "log_permission_add": "Ajouter l'autorisation '{}' pour l'application '{}'",
- "log_permission_remove": "Supprimer l'autorisation '{}'",
- "log_permission_update": "Mise à jour de l'autorisation '{}' pour l'application '{}'",
- "log_user_group_add": "Ajouter '{}' au groupe",
+ "group_update_failed": "La mise à jour du groupe '{group}' a échoué : {error}",
+ "group_creation_failed": "Échec de la création du groupe '{group}': {error}",
+ "group_deletion_failed": "Échec de la suppression du groupe '{group}': {error}",
"log_user_group_delete": "Supprimer le groupe '{}'",
"log_user_group_update": "Mettre à jour '{}' pour le groupe",
- "log_user_permission_add": "Mettre à jour l'autorisation pour '{}'",
- "log_user_permission_remove": "Mettre à jour l'autorisation pour '{}'",
- "mailbox_disabled": "La boîte aux lettres est désactivée pour l'utilisateur {user:s}"
+ "mailbox_disabled": "La boîte aux lettres est désactivée pour l’utilisateur {user:s}",
+ "app_action_broke_system": "Cette action semble avoir cassé des services importants : {services}",
+ "apps_already_up_to_date": "Toutes les applications sont déjà à jour",
+ "migration_0011_create_group": "Création d’un groupe pour chaque utilisateur…",
+ "migration_0011_done": "Migration terminée. Vous êtes maintenant en mesure de gérer des groupes d’utilisateurs.",
+ "migrations_must_provide_explicit_targets": "Vous devez fournir des cibles explicites lorsque vous utilisez '--skip' ou '--force-rerun'",
+ "migrations_no_such_migration": "Il n’y a pas de migration appelée '{id}'",
+ "migrations_pending_cant_rerun": "Ces migrations étant toujours en attente, vous ne pouvez pas les exécuter à nouveau : {ids}",
+ "migration_description_0012_postgresql_password_to_md5_authentication": "Forcer l’authentification PostgreSQL à utiliser MD5 pour les connexions locales",
+ "migrations_exclusive_options": "'auto', '--skip' et '--force-rerun' sont des options mutuellement exclusives.",
+ "migrations_not_pending_cant_skip": "Ces migrations ne sont pas en attente et ne peuvent donc pas être ignorées: {ids}",
+ "migration_0011_can_not_backup_before_migration": "La sauvegarde du système n’a pas pu être terminée avant l’échec de la migration. Erreur: {error:s}",
+ "migration_0011_migrate_permission": "Migration des autorisations des paramètres des applications vers LDAP …",
+ "migration_0011_migration_failed_trying_to_rollback": "La migration a échoué… Tentative de restauration du système.",
+ "migration_0011_rollback_success": "Système restauré.",
+ "migration_0011_update_LDAP_database": "Mise à jour de la base de données LDAP…",
+ "migration_0011_backup_before_migration": "Création d’une sauvegarde des paramètres de la base de données LDAP et des applications avant la migration.",
+ "permission_not_found": "Autorisation '{permission:s}' introuvable",
+ "permission_update_failed": "Impossible de mettre à jour la permission '{permission}' : {error}",
+ "permission_updated": "Permission '{permission:s}' mise à jour",
+ "permission_update_nothing_to_do": "Aucune autorisation pour mettre à jour",
+ "dyndns_provider_unreachable": "Impossible d’atteindre le fournisseur DynDNS {provider}: votre YunoHost n’est pas correctement connecté à Internet ou le serveur Dynette est en panne.",
+ "migration_0011_update_LDAP_schema": "Mise à jour du schéma LDAP …",
+ "migrations_already_ran": "Ces migrations sont déjà effectuées : {ids}",
+ "migrations_dependencies_not_satisfied": "Exécutez ces migrations : '{dependencies_id}', avant migration {id}.",
+ "migrations_failed_to_load_migration": "Impossible de charger la migration {id}: {error}",
+ "migrations_running_forward": "Exécution de la migration {id} …",
+ "migrations_success_forward": "Migration {id} terminée",
+ "operation_interrupted": "L’opération a été interrompue manuellement ?",
+ "permission_already_exist": "L’autorisation '{permission}' existe déjà",
+ "permission_created": "Permission '{permission:s}' créée",
+ "permission_creation_failed": "Impossible de créer l’autorisation '{permission}' : {error}",
+ "permission_deleted": "Permission '{permission:s}' supprimée",
+ "permission_deletion_failed": "Impossible de supprimer la permission '{permission}' : {error}",
+ "migration_description_0011_setup_group_permission": "Initialiser les groupes d’utilisateurs et autorisations pour les applications et les services",
+ "migration_0011_LDAP_update_failed": "Impossible de mettre à jour LDAP. Erreur: {error:s}",
+ "group_already_exist": "Le groupe {group} existe déjà",
+ "group_already_exist_on_system": "Le groupe {group} existe déjà dans les groupes système",
+ "group_cannot_be_deleted": "Le groupe {group} ne peut pas être supprimé manuellement.",
+ "group_user_already_in_group": "L’utilisateur {user} est déjà dans le groupe {group}",
+ "group_user_not_in_group": "L’utilisateur {user} n’est pas dans le groupe {group}",
+ "log_permission_create": "Créer permission '{}'",
+ "log_permission_delete": "Supprimer permission '{}'",
+ "log_user_group_create": "Créer '{}' groupe",
+ "log_user_permission_update": "Mise à jour des accès pour la permission '{}'",
+ "log_user_permission_reset": "Réinitialiser la permission '{}'",
+ "migration_0011_failed_to_remove_stale_object": "Impossible de supprimer un objet périmé {dn}: {error}",
+ "permission_already_allowed": "Le groupe '{group}' a déjà l'autorisation '{permission}' activée",
+ "permission_already_disallowed": "Le groupe '{group}' a déjà l'autorisation '{permission}' désactivé",
+ "permission_cannot_remove_main": "Supprimer une autorisation principale n'est pas autorisé",
+ "user_already_exists": "L'utilisateur '{user}' existe déjà",
+ "app_full_domain_unavailable": "Désolé, cette application doit être installée sur un domaine qui lui est propre, mais d'autres applications sont déjà installées sur le domaine '{domain}'. Vous pouvez utiliser un sous-domaine dédié à cette application à la place.",
+ "group_cannot_edit_all_users": "Le groupe 'all_users' ne peut pas être édité manuellement. C'est un groupe spécial destiné à contenir tous les utilisateurs enregistrés dans YunoHost",
+ "group_cannot_edit_visitors": "Le groupe 'visiteurs' ne peut pas être édité manuellement. C'est un groupe spécial représentant les visiteurs anonymes",
+ "group_cannot_edit_primary_group": "Le groupe '{group}' ne peut pas être édité manuellement. C'est le groupe principal destiné à ne contenir qu'un utilisateur spécifique.",
+ "log_permission_url": "Mise à jour de l'URL associée à l'autorisation '{}'",
+ "migration_0011_slapd_config_will_be_overwritten": "Il semble que vous ayez modifié manuellement la configuration de slapd. Pour cette migration critique, YunoHost doit forcer la mise à jour de la configuration de slapd. Les fichiers originaux seront sauvegardés dans {conf_backup_folder}.",
+ "permission_already_up_to_date": "L’autorisation n’a pas été mise à jour car les demandes d’ajout/suppression correspondent déjà à l’état actuel.",
+ "permission_currently_allowed_for_all_users": "Cette autorisation est actuellement accordée à tous les utilisateurs en plus des autres groupes. Vous voudrez probablement soit supprimer l’autorisation 'all_users', soit supprimer les autres groupes auxquels il est actuellement autorisé.",
+ "app_install_failed": "Impossible d’installer {app}: {error}",
+ "app_install_script_failed": "Une erreur est survenue dans le script d’installation de l’application",
+ "permission_require_account": "Permission {permission} n’a de sens que pour les utilisateurs ayant un compte et ne peut donc pas être activé pour les visiteurs.",
+ "app_remove_after_failed_install": "Supprimer l’application après l’échec de l’installation …",
+ "diagnosis_display_tip_web": "Vous pouvez aller à la section Diagnostic (dans l’écran d’accueil) pour voir les problèmes rencontrés.",
+ "diagnosis_cant_run_because_of_dep": "Impossible d’exécuter le diagnostic pour {category} alors qu’il existe des problèmes importants liés à {dep}.",
+ "diagnosis_found_errors": "Trouvé {errors} problème(s) significatif(s) lié(s) à {category} !",
+ "diagnosis_found_errors_and_warnings": "Trouvé {errors} problème(s) significatif(s) (et {warnings} (avertissement(s)) en relation avec {category} !",
+ "diagnosis_ip_not_connected_at_all": "Le serveur ne semble pas du tout connecté à Internet !?",
+ "diagnosis_ip_weird_resolvconf": "La résolution DNS semble fonctionner, mais soyez prudent en utilisant un fichier /etc/resolv.conf personnalisé.",
+ "diagnosis_ip_weird_resolvconf_details": "Au lieu de cela, ce fichier devrait être un lien symbolique vers /etc/resolvconf/run/resolv.conf lui-même pointant vers 127.0.0.1 (dnsmasq). Les résolveurs réels doivent être configurés dans /etc/resolv.dnsmasq.conf.",
+ "diagnosis_dns_missing_record": "Selon la configuration DNS recommandée, vous devez ajouter un enregistrement DNS\nType: {type}\nNom: {name}\nValeur {value}",
+ "diagnosis_diskusage_ok": "Le stockage {mountpoint} (sur le périphérique {device}) a encore {free} ({free_percent}%) d’espace libre !",
+ "diagnosis_ram_ok": "Le système dispose encore de {available} ({available_percent}%) de RAM sur {total}.",
+ "diagnosis_regenconf_allgood": "Tous les fichiers de configuration sont conformes à la configuration recommandée !",
+ "diagnosis_security_vulnerable_to_meltdown": "Vous semblez vulnérable à la vulnérabilité de sécurité critique de Meltdown",
+ "diagnosis_basesystem_host": "Le serveur utilise Debian {debian_version}",
+ "diagnosis_basesystem_kernel": "Le serveur utilise le noyau Linux {kernel_version}",
+ "diagnosis_basesystem_ynh_single_version": "{package} version: {version} ({repo})",
+ "diagnosis_basesystem_ynh_main_version": "Le serveur utilise YunoHost {main_version} ({repo})",
+ "diagnosis_basesystem_ynh_inconsistent_versions": "Vous exécutez des versions incohérentes des packages YunoHost … probablement à cause d’une mise à niveau partielle ou échouée.",
+ "diagnosis_display_tip_cli": "Vous pouvez exécuter 'yunohost diagnosis show --issues' pour afficher les problèmes détectés.",
+ "diagnosis_failed_for_category": "Échec du diagnostic pour la catégorie '{category}': {error}",
+ "diagnosis_cache_still_valid": "(Le cache est encore valide pour le diagnostic {category}. Il ne sera pas re-diagnostiqué pour le moment!)",
+ "diagnosis_ignored_issues": "(+ {nb_ignored} questions ignorée(s))",
+ "diagnosis_found_warnings": "Trouvé {warnings} objet(s) pouvant être amélioré(s) pour {category}.",
+ "diagnosis_everything_ok": "Tout semble bien pour {category} !",
+ "diagnosis_failed": "Impossible d’extraire le résultat du diagnostic pour la catégorie '{category}': {error}",
+ "diagnosis_ip_connected_ipv4": "Le serveur est connecté à Internet en IPv4 !",
+ "diagnosis_ip_no_ipv4": "Le serveur ne dispose pas d’une adresse IPv4.",
+ "diagnosis_ip_connected_ipv6": "Le serveur est connecté à Internet en IPv6 !",
+ "diagnosis_ip_no_ipv6": "Le serveur ne dispose pas d’une adresse IPv6.",
+ "diagnosis_ip_dnsresolution_working": "La résolution de nom de domaine fonctionne !",
+ "diagnosis_ip_broken_dnsresolution": "La résolution du nom de domaine semble interrompue pour une raison quelconque … Un pare-feu bloque-t-il les requêtes DNS ?",
+ "diagnosis_ip_broken_resolvconf": "La résolution du nom de domaine semble cassée sur votre serveur, ce qui semble lié au fait que /etc/resolv.conf ne pointe pas vers 127.0.0.1.",
+ "diagnosis_dns_good_conf": "Bonne configuration DNS pour le domaine {domain} (catégorie {category})",
+ "diagnosis_dns_bad_conf": "Configuration DNS incorrecte ou manquante pour le domaine {domain} (catégorie {category})",
+ "diagnosis_dns_discrepancy": "L’enregistrement DNS de type {0} et nom {1} ne correspond pas à la configuration recommandée. Valeur actuelle: {2}. Valeur exceptée: {3}. Vous pouvez consulter https://yunohost.org/dns_config pour plus d’informations.",
+ "diagnosis_services_bad_status": "Le service {service} est {status} :-(",
+ "diagnosis_diskusage_verylow": "Le stockage {mountpoint} (sur le périphérique {device}) ne dispose que de {free_abs_GB} Go ({free_percent}%). Vous devriez vraiment envisager de nettoyer un peu d’espace.",
+ "diagnosis_diskusage_low": "Le stockage {mountpoint} (sur le périphérique {device}) ne dispose que de {free_abs_GB} Go ({free_percent}%). Faites attention.",
+ "diagnosis_ram_verylow": "Le système ne dispose plus que de {available_abs_MB} MB ({available_percent}%) ! (sur {total_abs_MB} Mo)",
+ "diagnosis_ram_low": "Le système n’a plus de {available_abs_MB} MB ({available_percent}%) RAM sur {total_abs_MB} MB. Faites attention.",
+ "diagnosis_swap_none": "Le système n’a aucun échange. Vous devez envisager d’ajouter au moins 256 Mo de swap pour éviter les situations où le système manque de mémoire.",
+ "diagnosis_swap_notsomuch": "Le système ne dispose que de {total_MB} Mo de swap. Vous devez envisager d’avoir au moins 256 Mo pour éviter les situations où le système manque de mémoire.",
+ "diagnosis_swap_ok": "Le système dispose de {total_MB} Mo de swap !",
+ "diagnosis_dns_discrepancy": "L’enregistrement DNS de type {type} et nom {name} ne correspond pas à la configuration recommandée.\nValeur actuelle: {current}\nValeur attendue: {value}",
+ "diagnosis_services_bad_status": "Le service {service} est {status} :-(",
+ "diagnosis_diskusage_verylow": "Le stockage {mountpoint} (sur le périphérique {device}) ne dispose que de {free} ({free_percent}%). Vous devriez vraiment envisager de nettoyer un peu d’espace.",
+ "diagnosis_diskusage_low": "Le stockage {mountpoint} (sur le périphérique {device}) ne dispose que de {free} ({free_percent}%). Faites attention.",
+ "diagnosis_ram_verylow": "Le système ne dispose plus que de {available} ({available_percent}%)! (sur {total})",
+ "diagnosis_ram_low": "Le système n’a plus de {available} ({available_percent}%) RAM sur {total}. Faites attention.",
+ "diagnosis_swap_none": "Le système n’a aucun espace de swap. Vous devriez envisager d’ajouter au moins {recommended} de swap pour éviter les situations où le système manque de mémoire.",
+ "diagnosis_swap_notsomuch": "Le système ne dispose que de {total} de swap. Vous devez envisager d’avoir au moins {recommended} pour éviter les situations où le système manque de mémoire.",
+ "diagnosis_swap_ok": "Le système dispose de {total} de swap !",
+ "diagnosis_regenconf_manually_modified": "Le fichier de configuration {file} a été modifié manuellement.",
+ "diagnosis_regenconf_manually_modified_debian": "Le fichier de configuration {file} a été modifié manuellement par rapport à celui par défaut de Debian.",
+ "diagnosis_regenconf_manually_modified_details": "C’est probablement OK tant que vous savez ce que vous faites ;) !",
+ "diagnosis_regenconf_manually_modified_debian_details": "Cela peut probablement être OK, mais il faut garder un œil dessus …",
+ "diagnosis_security_all_good": "Aucune vulnérabilité de sécurité critique n’a été trouvée.",
+ "apps_catalog_init_success": "Système de catalogue d’applications initialisé !",
+ "apps_catalog_failed_to_download": "Impossible de télécharger le catalogue des applications {apps_catalog}:{error}",
+ "diagnosis_mail_outgoing_port_25_blocked": "Le port sortant 25 semble être bloqué. Vous devriez essayer de le débloquer dans le panneau de configuration de votre fournisseur de services Internet (ou hébergeur). En attendant, le serveur ne pourra pas envoyer de courrier électronique à d’autres serveurs.",
+ "domain_cannot_remove_main_add_new_one": "Vous ne pouvez pas supprimer '{domain:s}' car il s’agit du domaine principal et de votre seul domaine. Vous devez d’abord ajouter un autre domaine à l’aide de 'yunohost domain add ', puis définir comme domaine principal à l’aide de 'yunohost domain main-domain -n ' et vous pouvez ensuite supprimer le domaine '{domain:s}' à l’aide de 'yunohost domain remove {domain:s}'.",
+ "diagnosis_security_vulnerable_to_meltdown_details": "Pour résoudre ce problème, vous devez mettre à niveau votre système et redémarrer pour charger le nouveau noyau Linux (ou contacter votre fournisseur de serveur si cela ne fonctionne pas). Voir https://meltdownattack.com/ pour plus d’informations.",
+ "diagnosis_description_basesystem": "Système de base",
+ "diagnosis_description_ip": "Connectivité Internet",
+ "diagnosis_description_dnsrecords": "Enregistrements DNS",
+ "diagnosis_description_services": "État des services",
+ "diagnosis_description_systemresources": "Ressources système",
+ "diagnosis_description_ports": "Exposition des ports",
+ "diagnosis_description_regenconf": "Configurations système",
+ "diagnosis_description_security": "Contrôles de sécurité",
+ "diagnosis_ports_could_not_diagnose": "Impossible de diagnostiquer si les ports sont accessibles de l'extérieur.",
+ "diagnosis_ports_could_not_diagnose_details": "Erreur: {error}",
+ "apps_catalog_updating": "Mise à jour du catalogue d'applications…",
+ "apps_catalog_obsolete_cache": "Le cache du catalogue d'applications est vide ou obsolète.",
+ "apps_catalog_update_success": "Le catalogue des applications a été mis à jour !",
+ "diagnosis_mail_ougoing_port_25_ok": "Le port sortant 25 n’est pas bloqué et le courrier électronique peut être envoyé à d’autres serveurs.",
+ "diagnosis_description_mail": "Email",
+ "diagnosis_ports_unreachable": "Le port {port} n’est pas accessible de l’extérieur.",
+ "diagnosis_ports_ok": "Le port {port} est accessible de l’extérieur.",
+ "diagnosis_http_could_not_diagnose": "Impossible de diagnostiquer si le domaine est accessible de l’extérieur.",
+ "diagnosis_http_could_not_diagnose_details": "Erreur: {error}",
+ "diagnosis_http_ok": "Le domaine {domain} est accessible en HTTP depuis l’extérieur.",
+ "diagnosis_http_unreachable": "Le domaine {domain} est inaccessible en HTTP depuis l’extérieur.",
+ "diagnosis_unknown_categories": "Les catégories suivantes sont inconnues: {categories}",
+ "migration_description_0013_futureproof_apps_catalog_system": "Migrer vers le nouveau système de catalogue d’applications à l’épreuve du temps",
+ "app_upgrade_script_failed": "Une erreur s’est produite durant l’exécution du script de mise à niveau de l’application",
+ "migration_description_0014_remove_app_status_json": "Supprimer les anciens fichiers d’application status.json",
+ "diagnosis_services_running": "Le service {service} est en cours de fonctionnement !",
+ "diagnosis_services_conf_broken": "La configuration est cassée pour le service {service} !",
+ "diagnosis_ports_needed_by": "Rendre ce port accessible est nécessaire pour les fonctionnalités de type {category} (service {service})",
+ "diagnosis_ports_forwarding_tip": "Pour résoudre ce problème, vous devez probablement configurer la redirection de port sur votre routeur Internet comme décrit sur https://yunohost.org/isp_box_config",
+ "diagnosis_http_connection_error": "Erreur de connexion : impossible de se connecter au domaine demandé, il est probablement injoignable.",
+ "diagnosis_no_cache": "Pas encore de cache de diagnostique pour la catégorie « {category} »",
+ "yunohost_postinstall_end_tip": "La post-installation terminée! Pour finaliser votre configuration, il est recommendé de :\n - ajouter un premier utilisateur depuis la section \"Utilisateurs\" de l’interface web (ou \"yunohost user create \" en ligne de commande);\n - diagnostiquer les potentiels problèmes dans la section \"Diagnostic\" de l'interface web (ou \"yunohost diagnosis run\" en ligne de commande);\n - lire les parties \"Finalisation de votre configuration\" et \"Découverte de Yunohost\" dans le guide de l’administrateur: https://yunohost.org/admindoc.",
+ "diagnosis_services_bad_status_tip": "Vous pouvez essayer de redémarrer le service. Si cela ne fonctionne pas, consultez les journaux de service à l’aide de 'yunohost service log {service}' ou de la section 'Services' dans la webadmin.",
+ "diagnosis_http_bad_status_code": "Le système de diagnostique n’a pas réussi à contacter votre serveur. Il se peut qu’une autre machine réponde à la place de votre serveur. Vérifiez que le port 80 est correctement redirigé, que votre configuration nginx est à jour et qu’un reverse-proxy n’interfère pas.",
+ "diagnosis_http_timeout": "Expiration du délai en essayant de contacter votre serveur de l’extérieur. Il semble être inaccessible. Vérifiez que vous transférez correctement le port 80, que nginx est en cours d’exécution et qu’un pare-feu n’interfère pas.",
+ "global_settings_setting_pop3_enabled": "Activer le protocole POP3 pour le serveur de messagerie",
+ "log_app_action_run": "Lancer l’action de l’application '{}'",
+ "log_app_config_show_panel": "Montrer le panneau de configuration de l’application '{}'",
+ "log_app_config_apply": "Appliquer la configuration à l’application '{}'",
+ "diagnosis_never_ran_yet": "Il apparaît que le serveur a été installé récemment et qu’il n’y a pas encore eu de diagnostic. Vous devriez en lancer un depuis le webmin ou en utilisant 'yunohost diagnosis run' depuis la ligne de commande.",
+ "diagnosis_description_web": "Web",
+ "diagnosis_basesystem_hardware_board": "Le modèle de carte du serveur est {model}",
+ "diagnosis_basesystem_hardware": "L’architecture du serveur est {virt} {arch}",
+ "group_already_exist_on_system_but_removing_it": "Le groupe {group} est déjà présent dans les groupes du système, mais YunoHost va le supprimer…",
+ "certmanager_warning_subdomain_dns_record": "Le sous-domaine '{subdomain:s}' ne résout pas vers la même adresse IP que '{domain:s}'. Certaines fonctionnalités seront indisponibles tant que vous n’aurez pas corrigé cela et regénéré le certificat.",
+ "domain_cannot_add_xmpp_upload": "Vous ne pouvez pas ajouter de domaine commençant par 'xmpp-upload.'. Ce type de nom est réservé à la fonctionnalité d’upload XMPP intégrée dans YunoHost."
}
diff --git a/locales/hi.json b/locales/hi.json
index 015fd4e5e..609464c8f 100644
--- a/locales/hi.json
+++ b/locales/hi.json
@@ -9,73 +9,48 @@
"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": "आप को अपनी कस्टम एप्लीकेशन के लिए नाम देने की आवश्यकता है",
- "diagnosis_debian_version_error": "डेबियन वर्जन प्राप्त करने में असफलता {error}",
- "diagnosis_kernel_version_error": "कर्नेल वर्जन प्राप्त नहीं की जा पा रही : {error}",
- "diagnosis_monitor_disk_error": "डिस्क की मॉनिटरिंग नहीं की जा पा रही: {error}",
- "diagnosis_monitor_network_error": "नेटवर्क की मॉनिटरिंग नहीं की जा पा रही: {error}",
- "diagnosis_monitor_system_error": "सिस्टम की मॉनिटरिंग नहीं की जा पा रही: {error}",
- "diagnosis_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 के साथ इनवैलिड डोमिन इस्तेमाल किया गया"
-}
+ "password_too_simple_1": "पासवर्ड को कम से कम 8 वर्ण लंबा होना चाहिए"
+}
\ No newline at end of file
diff --git a/locales/hu.json b/locales/hu.json
index a6df4d680..3ba14286f 100644
--- a/locales/hu.json
+++ b/locales/hu.json
@@ -9,5 +9,6 @@
"app_already_up_to_date": "{app:s} napra kész",
"app_argument_choice_invalid": "{name:s} érvénytelen választás, csak egyike lehet {choices:s} közül",
"app_argument_invalid": "'{name:s}' hibás paraméter érték :{error:s}",
- "app_argument_required": "Parameter '{name:s}' kötelező"
-}
+ "app_argument_required": "Parameter '{name:s}' kötelező",
+ "password_too_simple_1": "A jelszónak legalább 8 karakter hosszúnak kell lennie"
+}
\ No newline at end of file
diff --git a/locales/it.json b/locales/it.json
index 2c194d5a6..79204320b 100644
--- a/locales/it.json
+++ b/locales/it.json
@@ -9,15 +9,12 @@
"backup_created": "Backup completo",
"backup_invalid_archive": "Archivio di backup non valido",
"backup_output_directory_not_empty": "La directory di output non è vuota",
- "backup_running_app_script": "Esecuzione del script di backup dell'applicazione '{app:s}'...",
"domain_created": "Il dominio è stato creato",
- "domain_dyndns_invalid": "Il dominio non è valido per essere usato con DynDNS",
"domain_exists": "Il dominio è già esistente",
"ldap_initialized": "LDAP è stato inizializzato",
"pattern_email": "L'indirizzo email deve essere valido (es. someone@domain.org)",
"pattern_mailbox_quota": "La dimensione deve avere un suffisso b/k/M/G/T o 0 per disattivare la quota",
"port_already_opened": "La porta {port:d} è già aperta per {ip_version:s} connessioni",
- "port_unavailable": "La porta {port:d} non è disponibile",
"service_add_failed": "Impossibile aggiungere il servizio '{service:s}'",
"service_cmd_exec_failed": "Impossibile eseguire il comando '{command:s}'",
"service_disabled": "Il servizio '{service:s}' è stato disattivato",
@@ -31,12 +28,8 @@
"admin_password": "Password dell'amministrazione",
"admin_password_change_failed": "Impossibile cambiare la password",
"admin_password_changed": "La password dell'amministrazione è stata cambiata",
- "app_incompatible": "L'applicazione {app} è incompatibile con la tua versione YunoHost",
"app_install_files_invalid": "Non sono validi i file di installazione",
- "app_location_already_used": "L'applicazione '{app}' è già installata in questo percorso ({path})",
- "app_location_install_failed": "Impossibile installare l'applicazione in questo percorso perchè andrebbe in conflitto con l'applicazione '{other_app}' già installata in '{other_path}'",
"app_manifest_invalid": "Manifesto dell'applicazione non valido: {error}",
- "app_no_upgrade": "Nessun applicazione da aggiornare",
"app_not_correctly_installed": "{app:s} sembra di non essere installata correttamente",
"app_not_properly_removed": "{app:s} non è stata correttamente rimossa",
"action_invalid": "L'azione '{action:s}' non è valida",
@@ -44,20 +37,12 @@
"app_sources_fetch_failed": "Impossibile riportare i file sorgenti",
"app_upgrade_failed": "Impossibile aggiornare {app:s}",
"app_upgraded": "{app:s} è stata aggiornata",
- "appslist_fetched": "La lista delle applicazioni {appslist:s} è stata recuperata",
- "appslist_removed": "La lista delle applicazioni {appslist:s} è stata rimossa",
- "app_package_need_update": "Il pacchetto dell'applicazione {app} deve essere aggiornato per seguire i cambiamenti di YunoHost",
"app_requirements_checking": "Controllo i pacchetti richiesti per {app}…",
- "app_requirements_failed": "Impossibile soddisfare i requisiti per {app}: {error}",
"app_requirements_unmeet": "Requisiti non soddisfatti per {app}, il pacchetto {pkgname} ({version}) deve essere {spec}",
- "appslist_unknown": "Lista di applicazioni {appslist:s} sconosciuta.",
- "ask_current_admin_password": "Password attuale dell'amministrazione",
"ask_firstname": "Nome",
"ask_lastname": "Cognome",
- "ask_list_to_remove": "Lista da rimuovere",
"ask_main_domain": "Dominio principale",
"ask_new_admin_password": "Nuova password dell'amministrazione",
- "backup_action_required": "Devi specificare qualcosa da salvare",
"backup_app_failed": "Non è possibile fare il backup dell'applicazione '{app:s}'",
"backup_archive_app_not_found": "L'applicazione '{app:s}' non è stata trovata nel archivio di backup",
"app_argument_choice_invalid": "Scelta non valida per l'argomento '{name:s}', deve essere uno di {choices:s}",
@@ -65,32 +50,19 @@
"app_argument_required": "L'argomento '{name:s}' è requisito",
"app_id_invalid": "Identificativo dell'applicazione non valido",
"app_unsupported_remote_type": "Il tipo remoto usato per l'applicazione non è supportato",
- "appslist_retrieve_error": "Impossibile recuperare la lista di applicazioni remote {appslist:s}: {error:s}",
- "appslist_retrieve_bad_format": "Il file recuperato per la lista di applicazioni {appslist:s} non è valido",
"backup_archive_broken_link": "Non è possibile accedere al archivio di backup (link rotto verso {path:s})",
- "backup_archive_hook_not_exec": "Il hook '{hook:s}' non è stato eseguito in questo backup",
"backup_archive_name_unknown": "Archivio di backup locale chiamato '{name:s}' sconosciuto",
"backup_archive_open_failed": "Non è possibile aprire l'archivio di backup",
"backup_cleaning_failed": "Non è possibile pulire la directory temporanea di backup",
- "backup_creating_archive": "Creazione del archivio di backup…",
"backup_creation_failed": "La creazione del backup è fallita",
"backup_delete_error": "Impossibile cancellare '{path:s}'",
"backup_deleted": "Il backup è stato cancellato",
- "backup_extracting_archive": "Estrazione del archivio di backup…",
"backup_hook_unknown": "Hook di backup '{hook:s}' sconosciuto",
"backup_nothings_done": "Non c'è niente da salvare",
"backup_output_directory_forbidden": "Directory di output vietata. I backup non possono esser creati nelle sotto-cartelle /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var o /home/yunohost.backup/archives",
"backup_output_directory_required": "Devi fornire una directory di output per il backup",
"backup_running_hooks": "Esecuzione degli hook di backup…",
"custom_app_url_required": "Devi fornire un URL per essere in grado di aggiornare l'applicazione personalizzata {app:s}",
- "custom_appslist_name_required": "Devi fornire un nome per la lista di applicazioni personalizzata",
- "diagnosis_debian_version_error": "Impossibile riportare la versione di Debian: {error}",
- "diagnosis_kernel_version_error": "Impossibile riportare la versione del kernel: {error}",
- "diagnosis_monitor_disk_error": "Impossibile controllare i dischi: {error}",
- "diagnosis_monitor_network_error": "Impossibile controllare la rete: {error}",
- "diagnosis_monitor_system_error": "Impossibile controllare il sistema: {error}",
- "diagnosis_no_apps": "Nessuna applicazione installata",
- "dnsmasq_isnt_installed": "dnsmasq non sembra installato, impartisci il comando 'apt-get remove bind9 && apt-get install dnsmasq'",
"domain_creation_failed": "Impossibile creare un dominio",
"domain_deleted": "Il dominio è stato cancellato",
"domain_deletion_failed": "Impossibile cancellare il dominio",
@@ -99,8 +71,6 @@
"domain_hostname_failed": "La definizione del nuovo hostname è fallita",
"domain_uninstall_app_first": "Una o più applicazioni sono installate su questo dominio. Disinstalla loro prima di procedere alla cancellazione di un dominio",
"domain_unknown": "Dominio sconosciuto",
- "domain_zone_exists": "Il file di zona DNS è già esistente",
- "domain_zone_not_found": "Il file di zona DNS non è stato trovato per il dominio {:s}",
"done": "Terminato",
"domains_available": "Domini disponibili:",
"downloading": "Scaricamento…",
@@ -122,7 +92,6 @@
"firewall_reload_failed": "Impossibile ricaricare il firewall",
"firewall_reloaded": "Il firewall è stato ricaricato",
"firewall_rules_cmd_failed": "Alcune regole del firewall sono fallite. Per ulteriori informazioni, vedi il registro.",
- "format_datetime_short": "%m/%d/%Y %I:%M %p",
"hook_exec_failed": "L'esecuzione dello script è fallita: {path:s}",
"hook_exec_not_terminated": "L'esecuzione dello script non è stata terminata: {path:s}",
"hook_name_unknown": "Nome di hook '{name:s}' sconosciuto",
@@ -131,49 +100,25 @@
"ip6tables_unavailable": "Non puoi giocare con ip6tables qui. O sei in un container o il tuo kernel non lo supporta",
"iptables_unavailable": "Non puoi giocare con iptables qui. O sei in un container o il tuo kernel non lo supporta",
"ldap_init_failed_to_create_admin": "L'inizializzazione LDAP non è riuscita a creare un utente admin",
- "license_undefined": "Indeterminato",
"mail_alias_remove_failed": "Impossibile rimuovere l'alias mail '{mail:s}'",
"mail_domain_unknown": "Dominio d'indirizzo mail '{domain:s}' sconosciuto",
"mail_forward_remove_failed": "Impossibile rimuovere la mail inoltrata '{mail:s}'",
"mailbox_used_space_dovecot_down": "Il servizio di posta elettronica Dovecot deve essere attivato se vuoi riportare lo spazio usato dalla posta elettronica",
- "maindomain_change_failed": "Impossibile cambiare il dominio principale",
- "maindomain_changed": "Il dominio principale è stato cambiato",
- "monitor_disabled": "Il monitoraggio del sistema è stato disattivato",
- "monitor_enabled": "Il monitoraggio del sistema è stato attivato",
- "monitor_glances_con_failed": "Impossibile collegarsi al server Glances",
- "monitor_not_enabled": "Il monitoraggio del server non è attivato",
- "monitor_period_invalid": "Periodo di tempo non valido",
- "monitor_stats_file_not_found": "I file statistici non sono stati trovati",
- "monitor_stats_no_update": "Nessuna statistica di monitoraggio da aggiornare",
- "monitor_stats_period_unavailable": "Nessuna statistica disponibile per il periodo",
- "mountpoint_unknown": "Punto di mount sconosciuto",
- "mysql_db_creation_failed": "La creazione del database MySQL è fallita",
- "mysql_db_init_failed": "L'inizializzazione del database MySQL è fallita",
- "mysql_db_initialized": "Il database MySQL è stato inizializzato",
- "new_domain_required": "Devi fornire il nuovo dominio principale",
- "no_appslist_found": "Nessuna lista di applicazioni trovata",
+ "main_domain_change_failed": "Impossibile cambiare il dominio principale",
+ "main_domain_changed": "Il dominio principale è stato cambiato",
"no_internet_connection": "Il server non è collegato a Internet",
- "no_ipv6_connectivity": "La connessione IPv6 non è disponibile",
"not_enough_disk_space": "Non c'è abbastanza spazio libero in '{path:s}'",
- "package_not_installed": "Il pacchetto '{pkgname}' non è installato",
"package_unknown": "Pacchetto '{pkgname}' sconosciuto",
- "packages_no_upgrade": "Nessuno pacchetto da aggiornare",
- "packages_upgrade_critical_later": "I pacchetti critici {packages:s} verranno aggiornati più tardi",
"packages_upgrade_failed": "Impossibile aggiornare tutti i pacchetti",
- "path_removal_failed": "Impossibile rimuovere il percorso {:s}",
"pattern_backup_archive_name": "Deve essere un nome di file valido con caratteri alfanumerici e -_. soli",
"pattern_domain": "Deve essere un nome di dominio valido (es. il-mio-dominio.org)",
"pattern_firstname": "Deve essere un nome valido",
"pattern_lastname": "Deve essere un cognome valido",
- "pattern_listname": "Caratteri alfanumerici e trattini bassi soli",
"pattern_password": "Deve contenere almeno 3 caratteri",
- "pattern_port": "Deve essere un numero di porta valido (es. 0-65535)",
"pattern_port_or_range": "Deve essere un numero di porta valido (es. 0-65535) o una fascia di porte valida (es. 100:200)",
"pattern_positive_number": "Deve essere un numero positivo",
"pattern_username": "Caratteri minuscoli alfanumerici o trattini bassi soli",
"port_already_closed": "La porta {port:d} è già chiusa per le connessioni {ip_version:s}",
- "port_available": "La porta {port:d} è disponibile",
- "restore_action_required": "Devi specificare qualcosa da ripristinare",
"restore_already_installed_app": "Un'applicazione è già installata con l'identificativo '{app:s}'",
"restore_app_failed": "Impossibile ripristinare l'applicazione '{app:s}'",
"restore_cleaning_failed": "Impossibile pulire la directory temporanea di ripristino",
@@ -181,10 +126,6 @@
"restore_confirm_yunohost_installed": "Sei sicuro di volere ripristinare un sistema già installato? {answers:s}",
"restore_failed": "Impossibile ripristinare il sistema",
"user_update_failed": "Impossibile aggiornare l'utente",
- "network_check_smtp_ko": "La posta in uscita (SMTP porta 25) sembra bloccata dalla tua rete",
- "network_check_smtp_ok": "La posta in uscita (SMTP porta 25) non è bloccata",
- "no_restore_script": "Nessuno script di ripristino trovato per l'applicazone '{app:s}'",
- "package_unexpected_error": "Un'errore inaspettata si è verificata durante il trattamento del pacchetto '{pkgname}'",
"restore_hook_unavailable": "Lo script di ripristino per '{part:s}' non è disponibile per il tuo sistema e non è nemmeno nell'archivio",
"restore_nothings_done": "Non è stato ripristinato nulla",
"restore_running_app_script": "Esecuzione dello script di ripristino dell'applicazione '{app:s}'…",
@@ -192,39 +133,19 @@
"service_added": "Il servizio '{service:s}' è stato aggiunto",
"service_already_started": "Il servizio '{service:s}' è già stato avviato",
"service_already_stopped": "Il servizio '{service:s}' è già stato fermato",
- "service_conf_file_backed_up": "Il file di configurazione '{conf}' è stato salvato in '{backup}'",
- "service_conf_file_copy_failed": "Impossibile copiare il nuovo file di configurazione '{new}' in '{conf}'",
- "service_conf_file_manually_modified": "Il file di configurazione '{conf}' è stato modificato manualmente e non verrà aggiornato",
- "service_conf_file_manually_removed": "Il file di configurazione '{conf}' è stato rimosso manualmente e non verrà creato",
- "service_conf_file_not_managed": "Il file di configurazione '{conf}' non è ancora amministrato e non verrà aggiornato",
- "service_conf_file_remove_failed": "Impossibile rimuovere il file di configurazione '{conf}'",
- "service_conf_file_removed": "Il file di configurazione '{conf}' è stato rimosso",
- "service_conf_file_updated": "Il file di configurazione '{conf}' è stato aggiornato",
- "service_conf_up_to_date": "La configurazione è già aggiornata per il servizio '{service}'",
- "service_conf_updated": "La configurazione è stata aggiornata per il servizio '{service}'",
- "service_conf_would_be_updated": "La configurazione sarebbe stata aggiornata per il servizio '{service}'",
"service_disable_failed": "Impossibile disabilitare il servizio '{service:s}'\n\nRegistri di servizio recenti:{logs:s}",
"service_enable_failed": "Impossibile abilitare il servizio '{service:s}'\n\nRegistri di servizio recenti:{logs:s}",
"service_enabled": "Il servizio '{service:s}' è stato attivato",
- "service_no_log": "Nessuno registro da visualizzare per il servizio '{service:s}'",
- "service_regenconf_dry_pending_applying": "Verifica della configurazione in sospeso che sarebbe stata applicata per il servizio '{service}'…",
- "service_regenconf_failed": "Impossibile rigenerare la configurazione per il/i servizio/i: {services}",
- "service_regenconf_pending_applying": "Applicazione della configurazione in sospeso per il servizio '{service}'…",
"service_start_failed": "Impossibile eseguire il servizio '{service:s}'\n\nRegistri di servizio recenti:{logs:s}",
"service_started": "Il servizio '{service:s}' è stato avviato",
- "service_status_failed": "Impossibile determinare lo stato del servizio '{service:s}'",
"service_stopped": "Il servizio '{service:s}' è stato fermato",
"service_unknown": "Servizio '{service:s}' sconosciuto",
"ssowat_conf_generated": "La configurazione SSOwat è stata generata",
"ssowat_conf_updated": "La configurazione SSOwat è stata aggiornata",
- "ssowat_persistent_conf_read_error": "Un'errore si è verificata durante la lettura della configurazione persistente SSOwat: {error:s}. Modifica il file persistente /etc/ssowat/conf.json per correggere la sintassi JSON",
- "ssowat_persistent_conf_write_error": "Un'errore si è verificata durante la registrazione della configurazione persistente SSOwat: {error:s}. Modifica il file persistente /etc/ssowat/conf.json per correggere la sintassi JSON",
"system_upgraded": "Il sistema è stato aggiornato",
"unbackup_app": "L'applicazione '{app:s}' non verrà salvata",
"unexpected_error": "Un'errore inaspettata si è verificata",
- "unit_unknown": "Unità '{unit:s}' sconosciuta",
"unlimit": "Nessuna quota",
- "update_cache_failed": "Impossibile aggiornare la cache APT",
"updating_apt_cache": "Recupero degli aggiornamenti disponibili per i pacchetti di sistema…",
"upgrade_complete": "Aggiornamento completo",
"upnp_dev_not_found": "Nessuno supporto UPnP trovato",
@@ -235,7 +156,6 @@
"user_creation_failed": "Impossibile creare l'utente",
"user_deletion_failed": "Impossibile cancellare l'utente",
"user_home_creation_failed": "Impossibile creare la home directory del utente",
- "user_info_failed": "Impossibile riportare le informazioni del utente",
"user_unknown": "Utente sconosciuto: {user:s}",
"user_updated": "L'utente è stato aggiornato",
"yunohost_already_installed": "YunoHost è già installato",
@@ -253,7 +173,6 @@
"certmanager_domain_http_not_working": "Sembra che non sia possibile accedere al dominio {domain:s} attraverso HTTP. Verifica la configurazione del DNS e di nginx",
"app_already_installed_cant_change_url": "Questa applicazione è già installata. L'URL non può essere cambiato solo da questa funzione. Guarda se `app changeurl` è disponibile.",
"app_already_up_to_date": "{app:s} è già aggiornata",
- "app_change_no_change_url_script": "L'applicazione {app_name:s} non supporta ancora il cambio del proprio URL, potrebbe essere necessario aggiornarla.",
"app_change_url_failed_nginx_reload": "Riavvio di nginx fallito. Questo è il risultato di 'nginx -t':\n{nginx_errors:s}",
"app_change_url_identical_domains": "Il vecchio ed il nuovo dominio/percorso_url sono identici ('{domain:s}{path:s}'), nessuna operazione necessaria.",
"app_change_url_no_script": "L'applicazione '{app_name:s}' non supporta ancora la modifica dell'URL. Forse dovresti aggiornare l'applicazione.",
@@ -262,18 +181,11 @@
"app_location_unavailable": "Questo URL non è disponibile o va in conflitto con la/le applicazione/i già installata/e:\n{apps:s}",
"app_upgrade_app_name": "Aggiornando l'applicazione {app}…",
"app_upgrade_some_app_failed": "Impossibile aggiornare alcune applicazioni",
- "appslist_corrupted_json": "Caricamento della lista delle applicazioni non riuscita. Sembra che {filename:s} sia corrotto.",
- "appslist_could_not_migrate": "Migrazione della lista delle applicazioni {appslist:s} non riuscita! Impossibile analizzare l'URL... La vecchia operazione pianificata è stata tenuta in {bkp_file:s}.",
- "appslist_migrating": "Migrando la lista di applicazioni {appslist:s}…",
- "appslist_name_already_tracked": "C'è già una lista di applicazioni registrata con il nome {name:s}.",
- "appslist_url_already_tracked": "C'è già una lista di applicazioni registrata con URL {url:s}.",
- "ask_path": "Percorso",
"backup_abstract_method": "Questo metodo di backup non è ancora stato implementato",
"backup_applying_method_borg": "Inviando tutti i file da salvare nel backup nel deposito borg-backup…",
"backup_applying_method_copy": "Copiando tutti i files nel backup…",
"backup_applying_method_custom": "Chiamando il metodo di backup personalizzato '{method:s}'…",
"backup_applying_method_tar": "Creando l'archivio tar del backup…",
- "backup_archive_mount_failed": "Montaggio dell'archivio del backup non riuscito",
"backup_archive_system_part_not_available": "La parte di sistema '{part:s}' non è disponibile in questo backup",
"backup_archive_writing_error": "Impossibile aggiungere i file al backup nell'archivio compresso",
"backup_ask_for_copying_if_needed": "Alcuni files non possono essere preparati al backup utilizzando il metodo che consente di evitare il consumo temporaneo di spazio nel sistema. Per eseguire il backup, {size:s}MB dovranno essere utilizzati temporaneamente. Sei d'accordo?",
@@ -285,7 +197,6 @@
"backup_csv_creation_failed": "Impossibile creare il file CVS richiesto per le future operazioni di ripristino",
"backup_custom_backup_error": "Il metodo di backup personalizzato è fallito allo step 'backup'",
"backup_custom_mount_error": "Il metodo di backup personalizzato è fallito allo step 'mount'",
- "backup_custom_need_mount_error": "Il metodo di backup personalizzato è fallito allo step 'need_mount'",
"backup_method_borg_finished": "Backup in borg terminato",
"backup_method_copy_finished": "Copia di backup terminata",
"backup_method_custom_finished": "Metodo di backup personalizzato '{method:s}' terminato",
@@ -316,7 +227,7 @@
"certmanager_cert_signing_failed": "Firma del nuovo certificato fallita",
"good_practices_about_user_password": "Ora stai per impostare una nuova password utente. La password dovrebbe essere di almeno 8 caratteri - anche se è buona pratica utilizzare password più lunghe (es. una sequenza di parole) e/o utilizzare vari tipi di caratteri (maiuscole, minuscole, numeri e simboli).",
"password_listed": "Questa password è una tra le più utilizzate al mondo. Per favore scegline una più unica.",
- "password_too_simple_1": "La password deve essere lunga almeno 8 caratteri",
+ "password_too_simple_1": "La password deve contenere almeno 8 caratteri",
"password_too_simple_2": "La password deve essere lunga almeno 8 caratteri e contenere numeri, maiuscole e minuscole",
"password_too_simple_3": "La password deve essere lunga almeno 8 caratteri e contenere numeri, maiuscole e minuscole e simboli",
"password_too_simple_4": "La password deve essere lunga almeno 12 caratteri e contenere numeri, maiuscole e minuscole",
@@ -327,7 +238,6 @@
"certmanager_conflicting_nginx_file": "Impossibile preparare il dominio per il controllo ACME: il file di configurazione nginx {filepath:s} è in conflitto e dovrebbe essere prima rimosso",
"certmanager_couldnt_fetch_intermediate_cert": "Tempo scaduto durante il tentativo di recupero di un certificato intermedio da Let's Encrypt. Installazione/rinnovo non riuscito - per favore riprova più tardi.",
"certmanager_domain_dns_ip_differs_from_public_ip": "Il valore DNS 'A' per il dominio {domain:s} è diverso dall'IP di questo server. Se hai modificato recentemente il tuo valore A, attendi che si propaghi (esistono online alcuni siti per il controllo della propagazione DNS). (Se sai cosa stai facendo, usa --no-checks per disabilitare quei controlli.)",
- "certmanager_domain_not_resolved_locally": "Il dominio {domain:s} non può essere risolto in locale dal server Yunohost. Questo può accadere se hai modificato recentemente il tuo valore DNS. Se così fosse, per favore aspetta qualche ora per far si che si propaghi. Se il problema persiste, prova ad aggiungere {domain:s} in /etc/hosts. (Se sai cosa stai facendo, usa --no-checks per disabilitare quei controlli.)",
"certmanager_error_no_A_record": "Nessun valore DNS 'A' trovato per {domain:s}. Devi far puntare il tuo nome di dominio verso la tua macchina per essere in grado di installare un certificato Let's Encrypt! (Se sai cosa stai facendo, usa --no-checks per disabilitare quei controlli.)",
"certmanager_hit_rate_limit": "Troppi certificati già rilasciati per l'esatta serie di dominii {domain:s} recentemente. Per favore riprova più tardi. Guarda https://letsencrypt.org/docs/rate-limits/ per maggiori dettagli",
"certmanager_http_check_timeout": "Tempo scaduto durante il tentativo di contatto del tuo server a se stesso attraverso HTTP utilizzando l'indirizzo IP pubblico (dominio {domain:s} con ip {ip:s}). Potresti avere un problema di hairpinning o il firewall/router davanti al tuo server non è correttamente configurato.",
@@ -340,7 +250,6 @@
"dpkg_is_broken": "Non puoi eseguire questo ora perchè dpkg/apt (i gestori di pacchetti del sistema) sembrano essere in stato danneggiato... Puoi provare a risolvere il problema connettendoti via SSH ed eseguire `sudo dpkg --configure -a`.",
"domain_cannot_remove_main": "Non è possibile rimuovere il dominio principale ora. Prima imposta un nuovo dominio principale",
"domain_dns_conf_is_just_a_recommendation": "Questo comando ti mostra qual è la configurazione *raccomandata*. Non ti imposta la configurazione DNS al tuo posto. È tua responsabilità configurare la tua zona DNS nel tuo registrar in accordo con queste raccomandazioni.",
- "domain_dyndns_dynette_is_unreachable": "Impossibile raggiungere la dynette YunoHost, o il tuo YunHost non è correttamente connesso a internet o il server dynette non è attivo. Errore: {error}",
"dyndns_could_not_check_provide": "Impossibile controllare se {provider:s} possano fornire {domain:s}.",
"dyndns_could_not_check_available": "Impossibile controllare se {domain:s} è disponibile su {provider:s}.",
"dyndns_domain_not_provided": "Il fornitore Dyndns {provider:s} non può fornire il dominio {domain:s}.",
@@ -366,7 +275,6 @@
"global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "Consenti l'uso del (deprecato) hostkey DSA per la configurazione del demone SSH",
"global_settings_unknown_type": "Situazione inaspettata, l'impostazione {setting:s} sembra essere di tipo {unknown_type:s} ma non è un tipo supportato dal sistema.",
"good_practices_about_admin_password": "Stai per definire una nuova password di amministratore. La password deve essere almeno di 8 caratteri - anche se è buona pratica utilizzare password più lunghe (es. una frase, una serie di parole) e/o utilizzare vari tipi di caratteri (maiuscole, minuscole, numeri e simboli).",
- "invalid_url_format": "Formato URL non valido",
"log_corrupted_md_file": "Il file dei metadati yaml associato con i registri è corrotto: '{md_file}'",
"log_category_404": "La categoria di registrazione '{category}' non esiste",
"log_link_to_log": "Registro completo di questa operazione: '{desc}'",
@@ -375,11 +283,6 @@
"log_link_to_failed_log": "L'operazione '{desc}' è fallita! Per ottenere aiuto, per favore fornisci il registro completo dell'operazione cliccando qui",
"log_help_to_get_failed_log": "L'operazione '{desc}' è fallita! Per ottenere aiuto, per favore condividi il registro completo dell'operazione utilizzando il comando 'yunohost log display {name} --share'",
"log_does_exists": "Non esiste nessun registro delle operazioni chiamato '{log}', usa 'yunohost log list' per vedere tutti i registri delle operazioni disponibili",
- "log_app_addaccess": "Aggiungi accesso a '{}'",
- "log_app_removeaccess": "Rimuovi accesso a '{}'",
- "log_app_clearaccess": "Rimuovi tutti gli accessi a '{}'",
- "log_app_fetchlist": "Aggiungi un elenco di applicazioni",
- "log_app_removelist": "Rimuovi un elenco di applicazioni",
"log_app_change_url": "Cambia l'url dell'applicazione '{}'",
"log_app_install": "Installa l'applicazione '{}'",
"log_app_remove": "Rimuovi l'applicazione '{}'",
@@ -397,14 +300,12 @@
"log_letsencrypt_cert_install": "Installa un certificato Let's encrypt sul dominio '{}'",
"log_selfsigned_cert_install": "Installa un certificato autofirmato sul dominio '{}'",
"log_letsencrypt_cert_renew": "Rinnova il certificato Let's encrypt sul dominio '{}'",
- "log_service_enable": "Abilita il servizio '{}'",
"log_regen_conf": "Rigenera configurazioni di sistema '{}'",
"log_user_create": "Aggiungi l'utente '{}'",
"log_user_delete": "Elimina l'utente '{}'",
"log_user_update": "Aggiornate le informazioni dell'utente '{}'",
- "log_tools_maindomain": "Rendi '{}' dominio principale",
+ "log_domain_main_domain": "Rendi '{}' dominio principale",
"log_tools_migrations_migrate_forward": "Migra avanti",
- "log_tools_migrations_migrate_backward": "Migra indietro",
"log_tools_postinstall": "Postinstallazione del tuo server YunoHost",
"log_tools_upgrade": "Aggiornamento dei pacchetti di sistema",
"log_tools_shutdown": "Spegni il tuo server",
@@ -425,7 +326,6 @@
"migration_description_0005_postgresql_9p4_to_9p6": "Migra i database da postgresql 9.4 a 9.6",
"migration_description_0006_sync_admin_and_root_passwords": "Sincronizza password di amministratore e root",
"migration_description_0010_migrate_to_apps_json": "Rimuovi gli elenchi di app deprecati ed usa invece il nuovo elenco unificato 'apps.json'",
- "migration_0003_backward_impossible": "La migrazione a Stretch non può essere annullata.",
"migration_0003_start": "Migrazione a Stretch iniziata. I registri saranno disponibili in {logfile}.",
"migration_0003_patching_sources_list": "Sistemando il file sources.lists…",
"migration_0003_main_upgrade": "Iniziando l'aggiornamento principale…",
@@ -435,5 +335,5 @@
"migration_0003_not_jessie": "La distribuzione attuale non è Jessie!",
"migration_0003_system_not_fully_up_to_date": "Il tuo sistema non è completamente aggiornato. Per favore prima esegui un aggiornamento normale prima di migrare a stretch.",
"this_action_broke_dpkg": "Questa azione ha danneggiato dpkg/apt (i gestori di pacchetti del sistema)… Puoi provare a risolvere questo problema connettendoti via SSH ed eseguendo `sudo dpkg --configure -a`.",
- "updating_app_lists": "Recupero degli aggiornamenti disponibili per le applicazioni…"
-}
+ "app_action_broke_system": "Questa azione sembra avere roto servizi importanti: {services}"
+}
\ No newline at end of file
diff --git a/locales/nb_NO.json b/locales/nb_NO.json
index 0967ef424..07695ec3d 100644
--- a/locales/nb_NO.json
+++ b/locales/nb_NO.json
@@ -1 +1,139 @@
-{}
+{
+ "aborting": "Avbryter…",
+ "admin_password": "Administrasjonspassord",
+ "admin_password_change_failed": "Kan ikke endre passord",
+ "admin_password_changed": "Administrasjonspassord endret",
+ "admin_password_too_long": "Velg et passord kortere enn 127 tegn",
+ "app_already_installed": "{app:s} er allerede installert",
+ "app_already_up_to_date": "{app:s} er allerede oppdatert",
+ "app_argument_invalid": "Velg en gydlig verdi for argumentet '{name:s}': {error:s}",
+ "app_argument_required": "Argumentet '{name:s}' er påkrevd",
+ "app_id_invalid": "Ugyldig program-ID",
+ "dyndns_cron_remove_failed": "Kunne ikke fjerne cron-jobb for DynDNS: {error}",
+ "dyndns_key_not_found": "Fant ikke DNS-nøkkel for domenet",
+ "app_not_correctly_installed": "{app:s} ser ikke ut til å ha blitt installert på riktig måte",
+ "dyndns_provider_unreachable": "Kunne ikke nå DynDNS-tilbyder {provider}: Enten har du ikke satt opp din YunoHost rett, dynette-tjeneren er nede, eller du mangler nett.",
+ "app_not_properly_removed": "{app:s} har ikke blitt fjernet på riktig måte",
+ "app_removed": "{app:s} fjernet",
+ "app_requirements_checking": "Sjekker påkrevde pakker for {app:s}…",
+ "app_start_install": "Installerer programmet '{app}'…",
+ "action_invalid": "Ugyldig handling '{action:s}'",
+ "app_start_restore": "Gjenoppretter programmet '{app}'…",
+ "backup_created": "Sikkerhetskopi opprettet",
+ "backup_archive_name_exists": "En sikkerhetskopi med dette navnet finnes allerede.",
+ "backup_archive_name_unknown": "Ukjent lokalt sikkerhetskopiarkiv ved navn '{name:s}'",
+ "already_up_to_date": "Ingenting å gjøre. Alt er oppdatert.",
+ "backup_method_copy_finished": "Sikkerhetskopi fullført",
+ "backup_method_tar_finished": "TAR-sikkerhetskopiarkiv opprettet",
+ "app_action_cannot_be_ran_because_required_services_down": "Dette programmet krever noen tjenester som ikke kjører. Før du fortsetter, du bør prøve å starte følgende tjenester på ny (og antagelig undersøke hvorfor de er nede): {services}",
+ "app_already_installed_cant_change_url": "Dette programmet er allerede installert. Nettadressen kan ikke endres kun med denne funksjonen. Ta en titt på `app changeurl` hvis den er tilgjengelig.",
+ "domain_exists": "Domenet finnes allerede",
+ "app_change_url_failed_nginx_reload": "Kunne ikke gjeninnlaste NGINX. Her har du utdataen for 'nginx -t'\n{nginx_errors:s}",
+ "domains_available": "Tilgjengelige domener:",
+ "done": "Ferdig",
+ "downloading": "Laster ned…",
+ "dyndns_could_not_check_provide": "Kunne ikke sjekke om {provider:s} kan tilby {domain:s}.",
+ "dyndns_could_not_check_available": "Kunne ikke sjekke om {domain:s} er tilgjengelig på {provider:s}.",
+ "mail_domain_unknown": "Ukjent e-postadresse for domenet '{domain:s}'",
+ "migrate_tsig_wait_2": "2 min…",
+ "log_remove_on_failed_restore": "Fjern '{}' etter mislykket gjenoppretting fra sikkerhetskopiarkiv",
+ "log_letsencrypt_cert_install": "Installer et Let's Encrypt-sertifikat på '{}'-domenet",
+ "log_letsencrypt_cert_renew": "Forny '{}'-Let's Encrypt-sertifikat",
+ "log_user_update": "Oppdater brukerinfo for '{}'",
+ "mail_alias_remove_failed": "Kunne ikke fjerne e-postaliaset '{mail:s}'",
+ "app_action_broke_system": "Denne handlingen ser ut til å ha knekt disse viktige tjenestene: {services}",
+ "app_argument_choice_invalid": "Bruk én av disse valgene '{choices:s}' for argumentet '{name:s}'",
+ "app_extraction_failed": "Kunne ikke pakke ut installasjonsfilene",
+ "app_install_files_invalid": "Disse filene kan ikke installeres",
+ "backup_abstract_method": "Denne sikkerhetskopimetoden er ikke implementert enda",
+ "backup_actually_backuping": "Oppretter sikkerhetskopiarkiv fra innsamlede filer…",
+ "backup_app_failed": "Kunne ikke sikkerhetskopiere programmet '{app:s}'",
+ "backup_applying_method_tar": "Lager TAR-sikkerhetskopiarkiv…",
+ "backup_archive_app_not_found": "Fant ikke programmet '{app:s}' i sikkerhetskopiarkivet",
+ "backup_archive_open_failed": "Kunne ikke åpne sikkerhetskopiarkivet",
+ "app_start_remove": "Fjerner programmet '{app}'…",
+ "app_start_backup": "Samler inn filer for sikkerhetskopiering for {app}…",
+ "backup_applying_method_copy": "Kopier alle filer til sikkerhetskopi…",
+ "backup_borg_not_implemented": "Borg-sikkerhetskopimetoden er ikke implementert enda",
+ "backup_creation_failed": "Kunne ikke opprette sikkerhetskopiarkiv",
+ "backup_couldnt_bind": "Kunne ikke binde {src:s} til {dest:s}.",
+ "backup_csv_addition_failed": "Kunne ikke legge til filer for sikkerhetskopi inn i CSV-filen",
+ "backup_deleted": "Sikkerhetskopi slettet",
+ "backup_no_uncompress_archive_dir": "Det finnes ingen slik utpakket arkivmappe",
+ "backup_delete_error": "Kunne ikke slette '{path:s}'",
+ "certmanager_domain_unknown": "Ukjent domene '{domain:s}'",
+ "certmanager_cert_signing_failed": "Kunne ikke signere det nye sertifikatet",
+ "executing_command": "Kjører kommendoen '{command:s}'…",
+ "executing_script": "Kjører skriptet '{script:s}'…",
+ "extracting": "Pakker ut…",
+ "log_domain_add": "Legg til '{}'-domenet i systemoppsett",
+ "log_domain_remove": "Fjern '{}'-domenet fra systemoppsett",
+ "log_dyndns_subscribe": "Abonner på YunoHost-underdomenet '{}'",
+ "log_dyndns_update": "Oppdater IP-adressen tilknyttet ditt YunoHost-underdomene '{}'",
+ "migrate_tsig_wait_3": "1 min…",
+ "migrate_tsig_wait_4": "30 sekunder…",
+ "backup_invalid_archive": "Dette er ikke et sikkerhetskopiarkiv",
+ "backup_nothings_done": "Ingenting å lagre",
+ "backup_method_borg_finished": "Sikkerhetskopi inn i Borg fullført",
+ "field_invalid": "Ugyldig felt '{:s}'",
+ "firewall_reloaded": "Brannmur gjeninnlastet",
+ "log_app_change_url": "Endre nettadresse for '{}'-programmet",
+ "log_app_install": "Installer '{}'-programmet",
+ "log_app_remove": "Fjern '{}'-programmet",
+ "log_app_upgrade": "Oppgrader '{}'-programmet",
+ "log_app_makedefault": "Gjør '{}' til forvalgt program",
+ "log_available_on_yunopaste": "Denne loggen er nå tilgjengelig via {url}",
+ "log_tools_shutdown": "Slå av tjeneren din",
+ "log_tools_reboot": "Utfør omstart av tjeneren din",
+ "apps_already_up_to_date": "Alle programmer allerede oppdatert",
+ "backup_mount_archive_for_restore": "Forbereder arkiv for gjenopprettelse…",
+ "backup_copying_to_organize_the_archive": "Kopierer {size:s} MB for å organisere arkivet",
+ "domain_cannot_remove_main": "Kan ikke fjerne hoveddomene. Sett et først",
+ "domain_cert_gen_failed": "Kunne ikke opprette sertifikat",
+ "domain_created": "Domene opprettet",
+ "domain_creation_failed": "Kunne ikke opprette domene",
+ "domain_dyndns_root_unknown": "Ukjent DynDNS-rotdomene",
+ "domain_unknown": "Ukjent domene",
+ "dyndns_cron_installed": "Opprettet cron-jobb for DynDNS",
+ "dyndns_cron_removed": "Fjernet cron-jobb for DynDNS",
+ "dyndns_ip_update_failed": "Kunne ikke oppdatere IP-adresse til DynDNS",
+ "dyndns_ip_updated": "Oppdaterte din IP på DynDNS",
+ "dyndns_key_generating": "Oppretter DNS-nøkkel… Dette kan ta en stund.",
+ "dyndns_no_domain_registered": "Inget domene registrert med DynDNS",
+ "dyndns_registered": "DynDNS-domene registrert",
+ "global_settings_setting_security_password_admin_strength": "Admin-passordets styrke",
+ "dyndns_registration_failed": "Kunne ikke registrere DynDNS-domene: {error:s}",
+ "global_settings_setting_security_password_user_strength": "Brukerpassordets styrke",
+ "log_backup_restore_app": "Gjenopprett '{}' fra sikkerhetskopiarkiv",
+ "log_remove_on_failed_install": "Fjern '{}' etter mislykket installasjon",
+ "log_selfsigned_cert_install": "Installer selvsignert sertifikat på '{}'-domenet",
+ "log_user_delete": "Slett '{}' bruker",
+ "log_user_group_delete": "Slett '{}' gruppe",
+ "log_user_group_update": "Oppdater '{}' gruppe",
+ "ldap_init_failed_to_create_admin": "LDAP-igangsettelse kunne ikke opprette admin-bruker",
+ "ldap_initialized": "LDAP-igangsatt",
+ "migration_description_0003_migrate_to_stretch": "Oppgrader systemet til Debian Stretch og YunoHost 3.0",
+ "app_unknown": "Ukjent program",
+ "app_upgrade_app_name": "Oppgraderer {app}…",
+ "app_upgrade_failed": "Kunne ikke oppgradere {app:s}",
+ "app_upgrade_some_app_failed": "Noen programmer kunne ikke oppgraderes",
+ "app_upgraded": "{app:s} oppgradert",
+ "ask_email": "E-postadresse",
+ "ask_firstname": "Fornavn",
+ "ask_lastname": "Etternavn",
+ "ask_main_domain": "Hoveddomene",
+ "ask_new_admin_password": "Nytt administrasjonspassord",
+ "app_upgrade_several_apps": "Følgende programmer vil oppgraderes: {apps}",
+ "ask_new_domain": "Nytt domene",
+ "ask_new_path": "Ny sti",
+ "ask_password": "Passord",
+ "domain_deleted": "Domene slettet",
+ "domain_deletion_failed": "Kunne ikke slette domene",
+ "domain_dyndns_already_subscribed": "Du har allerede abonnement på et DynDNS-domene",
+ "log_category_404": "Loggkategorien '{category}' finnes ikke",
+ "log_link_to_log": "Full logg for denne operasjonen: '{desc}'",
+ "log_help_to_get_log": "For å vise loggen for operasjonen '{desc}', bruk kommandoen 'yunohost log display {name}'",
+ "log_user_create": "Legg til '{}' bruker",
+ "app_change_url_success": "{app:s} nettadressen er nå {domain:s}{path:s}",
+ "app_install_failed": "Kunne ikke installere {app}: {error}"
+}
\ No newline at end of file
diff --git a/locales/ne.json b/locales/ne.json
new file mode 100644
index 000000000..9e26dfeeb
--- /dev/null
+++ b/locales/ne.json
@@ -0,0 +1 @@
+{}
\ No newline at end of file
diff --git a/locales/nl.json b/locales/nl.json
index 166df89ff..dfee556b2 100644
--- a/locales/nl.json
+++ b/locales/nl.json
@@ -1,28 +1,20 @@
{
"action_invalid": "Ongeldige actie '{action:s}'",
"admin_password": "Administrator wachtwoord",
- "admin_password_changed": "Het administratie wachtwoord is gewijzigd",
+ "admin_password_changed": "Het administratie wachtwoord werd gewijzigd",
"app_already_installed": "{app:s} is al geïnstalleerd",
"app_argument_invalid": "'{name:s}' bevat ongeldige waarde: {error:s}",
"app_argument_required": "Het '{name:s}' moet ingevuld worden",
"app_extraction_failed": "Kan installatiebestanden niet uitpakken",
"app_id_invalid": "Ongeldige app-id",
"app_install_files_invalid": "Ongeldige installatiebestanden",
- "app_location_already_used": "Er is al een app geïnstalleerd op deze locatie",
- "app_location_install_failed": "Kan app niet installeren op deze locatie",
"app_manifest_invalid": "Ongeldig app-manifest",
- "app_no_upgrade": "Geen apps op te upgraden",
"app_not_installed": "{app:s} is niet geïnstalleerd",
- "app_recent_version_required": "{:s} vereist een nieuwere versie van moulinette",
"app_removed": "{app:s} succesvol verwijderd",
"app_sources_fetch_failed": "Kan bronbestanden niet ophalen",
"app_unknown": "Onbekende app",
"app_upgrade_failed": "Kan app {app:s} niet updaten",
"app_upgraded": "{app:s} succesvol geüpgraded",
- "appslist_fetched": "App-lijst {appslist:s} succesvol opgehaald",
- "appslist_removed": "App-lijst {appslist:s} succesvol verwijderd",
- "appslist_unknown": "App-lijst {appslist:s} is onbekend.",
- "ask_current_admin_password": "Huidig administratorwachtwoord",
"ask_email": "Email-adres",
"ask_firstname": "Voornaam",
"ask_lastname": "Achternaam",
@@ -30,26 +22,19 @@
"ask_password": "Wachtwoord",
"backup_archive_name_exists": "Een backuparchief met dezelfde naam bestaat al",
"backup_cleaning_failed": "Kan tijdelijke backup map niet leeg maken",
- "backup_creating_archive": "Backup wordt gestart...",
"backup_invalid_archive": "Ongeldig backup archief",
"backup_output_directory_not_empty": "Doelmap is niet leeg",
- "backup_running_app_script": "Backup script voor app '{app:s}' is gestart...",
"custom_app_url_required": "U moet een URL opgeven om uw aangepaste app {app:s} bij te werken",
- "custom_appslist_name_required": "U moet een naam opgeven voor uw aangepaste app-lijst",
- "dnsmasq_isnt_installed": "dnsmasq lijkt niet geïnstalleerd te zijn, voer alstublieft het volgende commando uit: 'apt-get remove bind9 && apt-get install dnsmasq'",
"domain_cert_gen_failed": "Kan certificaat niet genereren",
"domain_created": "Domein succesvol aangemaakt",
"domain_creation_failed": "Kan domein niet aanmaken",
"domain_deleted": "Domein succesvol verwijderd",
"domain_deletion_failed": "Kan domein niet verwijderen",
"domain_dyndns_already_subscribed": "U heeft reeds een domein bij DynDNS geregistreerd",
- "domain_dyndns_invalid": "Het domein is ongeldig voor DynDNS",
"domain_dyndns_root_unknown": "Onbekend DynDNS root domein",
"domain_exists": "Domein bestaat al",
"domain_uninstall_app_first": "Een of meerdere apps zijn geïnstalleerd op dit domein, verwijder deze voordat u het domein verwijdert",
"domain_unknown": "Onbekend domein",
- "domain_zone_exists": "DNS zone bestand bestaat al",
- "domain_zone_not_found": "DNS zone bestand niet gevonden voor domein: {:s}",
"done": "Voltooid",
"downloading": "Downloaden...",
"dyndns_cron_remove_failed": "De cron-job voor DynDNS kon niet worden verwijderd",
@@ -62,27 +47,15 @@
"installation_complete": "Installatie voltooid",
"installation_failed": "Installatie gefaald",
"ldap_initialized": "LDAP is klaar voor gebruik",
- "license_undefined": "Niet gedefinieerd",
"mail_alias_remove_failed": "Kan mail-alias '{mail:s}' niet verwijderen",
- "monitor_stats_no_update": "Er zijn geen recente monitoringstatistieken bij te werken",
- "mysql_db_creation_failed": "Aanmaken MySQL database gefaald",
- "mysql_db_init_failed": "Initialiseren MySQL database gefaald",
- "mysql_db_initialized": "MySQL database is succesvol geïnitialiseerd",
- "network_check_smtp_ko": "Uitgaande mail (SMPT port 25) wordt blijkbaar geblokkeerd door uw het netwerk",
- "no_appslist_found": "Geen app-lijst gevonden",
"no_internet_connection": "Server is niet verbonden met het internet",
- "no_ipv6_connectivity": "IPv6-stack is onbeschikbaar",
- "path_removal_failed": "Kan pad niet verwijderen {:s}",
"pattern_email": "Moet een geldig emailadres bevatten (bv. abc@example.org)",
- "pattern_listname": "Slechts cijfers, letters en '_' zijn toegelaten",
"pattern_mailbox_quota": "Mailbox quota moet een waarde bevatten met b/k/M/G/T erachter of 0 om geen quota in te stellen",
"pattern_password": "Wachtwoord moet tenminste 3 karakters lang zijn",
"port_already_closed": "Poort {port:d} is al gesloten voor {ip_version:s} verbindingen",
"port_already_opened": "Poort {port:d} is al open voor {ip_version:s} verbindingen",
- "port_available": "Poort {port:d} is beschikbaar",
- "port_unavailable": "Poort {port:d} is niet beschikbaar",
"restore_app_failed": "De app '{app:s}' kon niet worden terug gezet",
- "restore_hook_unavailable": "De herstel-hook '{hook:s}' is niet beschikbaar op dit systeem",
+ "restore_hook_unavailable": "De herstel-hook '{part:s}' is niet beschikbaar op dit systeem",
"service_add_failed": "Kan service '{service:s}' niet toevoegen",
"service_already_started": "Service '{service:s}' draait al",
"service_cmd_exec_failed": "Kan '{command:s}' niet uitvoeren",
@@ -91,7 +64,6 @@
"service_removed": "Service werd verwijderd",
"service_stop_failed": "Kan service '{service:s}' niet stoppen",
"service_unknown": "De service '{service:s}' bestaat niet",
- "show_diff": "Let op de volgende verschillen zijn:\n{diff:s}",
"unexpected_error": "Er is een onbekende fout opgetreden",
"unrestore_app": "App '{app:s}' wordt niet teruggezet",
"updating_apt_cache": "Lijst van beschikbare pakketten wordt bijgewerkt...",
@@ -108,35 +80,26 @@
"yunohost_configured": "YunoHost configuratie is OK",
"admin_password_change_failed": "Wachtwoord kan niet veranderd worden",
"app_argument_choice_invalid": "Ongeldige keuze voor argument '{name:s}'. Het moet een van de volgende keuzes zijn {choices:s}",
- "app_incompatible": "Deze applicatie is incompatibel met uw YunoHost versie",
"app_not_correctly_installed": "{app:s} schijnt niet juist geïnstalleerd te zijn",
"app_not_properly_removed": "{app:s} werd niet volledig verwijderd",
- "app_package_need_update": "Het is noodzakelijk om het app pakket te updaten, in navolging van veranderingen aan YunoHost",
"app_requirements_checking": "Controleer noodzakelijke pakketten...",
- "app_requirements_failed": "Er wordt niet aan de aanvorderingen voldaan: {error}",
"app_requirements_unmeet": "Er wordt niet aan de aanvorderingen voldaan, het pakket {pkgname} ({version}) moet {spec} zijn",
"app_unsupported_remote_type": "Niet ondersteund besturings type voor de app",
- "appslist_retrieve_error": "Niet mogelijk om de externe applicatie lijst op te halen {appslist:s}: {error:s}",
- "appslist_retrieve_bad_format": "Opgehaald bestand voor applicatie lijst {appslist:s} is geen geldige applicatie lijst",
- "appslist_name_already_tracked": "Er is reeds een geregistreerde applicatie lijst met de naam {name:s}.",
- "appslist_url_already_tracked": "Er is reeds een geregistreerde applicatie lijst met de url {url:s}.",
- "appslist_migrating": "Migreer applicatielijst {appslist:s} ...",
- "appslist_could_not_migrate": "Kon applicatielijst {appslist:s} niet migreren! Niet in staat om de url te verwerken... De oude cron job is opgeslagen onder {bkp_file:s}.",
- "appslist_corrupted_json": "Kon de applicatielijst niet laden. Het schijnt, dat {filename:s} beschadigd is.",
- "ask_list_to_remove": "Te verwijderen lijst",
"ask_main_domain": "Hoofd-domein",
- "backup_action_required": "U moet iets om op te slaan uitkiezen",
"backup_app_failed": "Kon geen backup voor app '{app:s}' aanmaken",
"backup_archive_app_not_found": "App '{app:s}' kon niet in het backup archief gevonden worden",
"backup_archive_broken_link": "Het backup archief kon niet geopend worden (Ongeldig verwijs naar {path:s})",
- "backup_archive_hook_not_exec": "Hook '{hook:s}' kon voor deze backup niet uitgevoerd worden",
"backup_archive_name_unknown": "Onbekend lokaal backup archief namens '{name:s}' gevonden",
"backup_archive_open_failed": "Kan het backup archief niet openen",
"backup_created": "Backup aangemaakt",
"backup_creation_failed": "Aanmaken van backup mislukt",
"backup_delete_error": "Kon pad '{path:s}' niet verwijderen",
"backup_deleted": "Backup werd verwijderd",
- "backup_extracting_archive": "Backup archief uitpakken...",
"backup_hook_unknown": "backup hook '{hook:s}' onbekend",
- "backup_nothings_done": "Niets om op te slaan"
-}
+ "backup_nothings_done": "Niets om op te slaan",
+ "password_too_simple_1": "Het wachtwoord moet minimaal 8 tekens lang zijn",
+ "already_up_to_date": "Er is niets te doen, alles is al up-to-date.",
+ "admin_password_too_long": "Gelieve een wachtwoord te kiezen met minder dan 127 karakters",
+ "app_action_cannot_be_ran_because_required_services_down": "De volgende diensten moeten actief zijn om deze actie uit te voeren: {services}. Probeer om deze te herstarten om verder te gaan (en om eventueel te onderzoeken waarom ze niet werken).",
+ "aborting": "Annulatie."
+}
\ No newline at end of file
diff --git a/locales/oc.json b/locales/oc.json
index 320a18341..95f581851 100644
--- a/locales/oc.json
+++ b/locales/oc.json
@@ -5,89 +5,64 @@
"app_already_installed": "{app:s} es ja installat",
"app_already_up_to_date": "{app:s} es ja a jorn",
"installation_complete": "Installacion acabada",
- "app_id_invalid": "Id d’aplicacion incorrècte",
- "app_install_files_invalid": "Fichièrs d’installacion incorrèctes",
- "app_no_upgrade": "Pas cap d’aplicacion d’actualizar",
+ "app_id_invalid": "ID d’aplicacion incorrècte",
+ "app_install_files_invalid": "Installacion impossibla d’aquestes fichièrs",
"app_not_correctly_installed": "{app:s} sembla pas ben installat",
- "app_not_installed": "L’aplicacion {app:s} es pas installat. Vaquí la lista de las aplicacions installadas : {all_apps}",
+ "app_not_installed": "Impossible de trobar l’aplicacion {app:s}. Vaquí la lista de las aplicacions installadas : {all_apps}",
"app_not_properly_removed": "{app:s} es pas estat corrèctament suprimit",
- "app_removed": "{app:s} es estat suprimit",
+ "app_removed": "{app:s} es estada suprimida",
"app_unknown": "Aplicacion desconeguda",
"app_upgrade_app_name": "Actualizacion de l’aplicacion {app}…",
- "app_upgrade_failed": "Impossible d’actualizar {app:s}",
+ "app_upgrade_failed": "Impossible d’actualizar {app:s} : {error}",
"app_upgrade_some_app_failed": "D’aplicacions se pòdon pas actualizar",
"app_upgraded": "{app:s} es estada actualizada",
- "appslist_fetched": "Recuperacion de la lista d’aplicacions {appslist:s} corrèctament realizada",
- "appslist_migrating": "Migracion de la lista d’aplicacion{appslist:s}…",
- "appslist_name_already_tracked": "I a ja una lista d’aplicacion enregistrada amb lo nom {name:s}.",
- "appslist_removed": "Supression de la lista d’aplicacions {appslist:s} corrèctament realizada",
- "appslist_retrieve_bad_format": "Lo fichièr recuperat per la lista d’aplicacions {appslist:s} es pas valid",
- "appslist_unknown": "La lista d’aplicacions {appslist:s} es desconeguda.",
- "appslist_url_already_tracked": "I a ja una lista d’aplicacions enregistrada amb l’URL {url:s}.",
- "ask_current_admin_password": "Senhal administrator actual",
"ask_email": "Adreça de corrièl",
"ask_firstname": "Prenom",
"ask_lastname": "Nom",
- "ask_list_to_remove": "Lista de suprimir",
"ask_main_domain": "Domeni màger",
"ask_new_admin_password": "Nòu senhal administrator",
"ask_password": "Senhal",
- "ask_path": "Camin",
- "backup_action_required": "Devètz precisar çò que cal salvagardar",
"backup_app_failed": "Impossible de salvagardar l’aplicacion « {app:s} »",
"backup_applying_method_copy": "Còpia de totes los fichièrs dins la salvagarda…",
- "backup_applying_method_tar": "Creacion de l’archiu tar de la salvagarda…",
- "backup_archive_name_exists": "Un archiu de salvagarda amb aquesta nom existís ja",
+ "backup_applying_method_tar": "Creacion de l’archiu TAR de la salvagarda…",
+ "backup_archive_name_exists": "Un archiu de salvagarda amb aquesta nom existís ja.",
"backup_archive_name_unknown": "L’archiu local de salvagarda apelat « {name:s} » es desconegut",
"action_invalid": "Accion « {action:s} » incorrècta",
- "app_argument_choice_invalid": "Causida invalida pel paramètre « {name:s} », cal que siá un de {choices:s}",
- "app_argument_invalid": "Valor invalida pel paramètre « {name:s} » : {error:s}",
+ "app_argument_choice_invalid": "Utilizatz una de las opcions « {choices:s} » per l’argument « {name:s} »",
+ "app_argument_invalid": "Causissètz una valor invalida pel paramètre « {name:s} » : {error:s}",
"app_argument_required": "Lo paramètre « {name:s} » es requesit",
- "app_change_url_failed_nginx_reload": "La reaviada de nginx a fracassat. Vaquí la sortida de « nginx -t » :\n{nginx_errors:s}",
+ "app_change_url_failed_nginx_reload": "Reaviada de NGINX impossibla. Vaquí la sortida de « nginx -t » :\n{nginx_errors:s}",
"app_change_url_identical_domains": "L’ancian e lo novèl coble domeni/camin son identics per {domain:s}{path:s}, pas res a far.",
- "app_change_url_success": "L’URL de l’aplicacion {app:s} a cambiat per {domain:s}{path:s}",
- "app_checkurl_is_deprecated": "Packagers /!\\ ’app checkurl’ es obsolèt ! Utilizatz ’app register-url’ a la plaça !",
+ "app_change_url_success": "L’URL de l’aplicacion {app:s} es ara {domain:s}{path:s}",
"app_extraction_failed": "Extraccion dels fichièrs d’installacion impossibla",
- "app_incompatible": "L’aplicacion {app} es pas compatibla amb vòstra version de YunoHost",
- "app_location_already_used": "L’aplicacion « {app} » es ja installada a aqueste emplaçament ({path})",
- "app_manifest_invalid": "Manifest d’aplicacion incorrècte : {error}",
- "app_package_need_update": "Lo paquet de l’aplicacion {app} deu èsser actualizat per poder seguir los cambiaments de YunoHost",
+ "app_manifest_invalid": "I a quicòm que truca amb lo manifest de l’aplicacion : {error}",
"app_requirements_checking": "Verificacion dels paquets requesits per {app}…",
"app_sources_fetch_failed": "Recuperacion dels fichièrs fonts impossibla, l’URL es corrècta ?",
"app_unsupported_remote_type": "Lo tipe alonhat utilizat per l’aplicacion es pas suportat",
- "appslist_retrieve_error": "Impossible de recuperar la lista d’aplicacions alonhadas {appslist:s} : {error:s}",
"backup_archive_app_not_found": "L’aplicacion « {app:s} » es pas estada trobada dins l’archiu de la salvagarda",
- "backup_archive_broken_link": "Impossible d‘accedir a l’archiu de salvagarda (ligam invalid cap a {path:s})",
- "backup_archive_mount_failed": "Lo montatge de l’archiu de salvagarda a fracassat",
+ "backup_archive_broken_link": "Impossible d’accedir a l’archiu de salvagarda (ligam invalid cap a {path:s})",
"backup_archive_open_failed": "Impossible de dobrir l’archiu de salvagarda",
"backup_archive_system_part_not_available": "La part « {part:s} » del sistèma es pas disponibla dins aquesta salvagarda",
"backup_cleaning_failed": "Impossible de netejar lo repertòri temporari de salvagarda",
"backup_copying_to_organize_the_archive": "Còpia de {size:s} Mio per organizar l’archiu",
"backup_created": "Salvagarda acabada",
- "backup_creating_archive": "Creacion de l’archiu de salvagarda…",
- "backup_creation_failed": "Impossible de crear la salvagarda",
+ "backup_creation_failed": "Creacion impossibla de l’archiu de salvagarda",
"app_already_installed_cant_change_url": "Aquesta aplicacion es ja installada. Aquesta foncion pòt pas simplament cambiar l’URL. Agachatz « app changeurl » s’es disponible.",
- "app_change_no_change_url_script": "L’aplicacion {app_name:s} pren pas en compte lo cambiament d’URL, poiretz aver de l’actualizar.",
"app_change_url_no_script": "L’aplicacion {app_name:s} pren pas en compte lo cambiament d’URL, benlèu que vos cal l’actualizar.",
"app_make_default_location_already_used": "Impossible de configurar l’aplicacion « {app} » per defaut pel domeni {domain} perque es ja utilizat per l’aplicacion {other_app}",
- "app_location_install_failed": "Impossible d’installar l’aplicacion a aqueste emplaçament per causa de conflicte amb l’aplicacion {other_app} qu’es ja installada sus {other_path}",
"app_location_unavailable": "Aquesta URL es pas disponibla o en conflicte amb una aplicacion existenta :\n{apps:s}",
- "appslist_corrupted_json": "Cargament impossible de la lista d’aplicacion. Sembla que {filename:s} siá gastat.",
- "backup_delete_error": "Impossible de suprimir « {path:s} »",
+ "backup_delete_error": "Supression impossibla de « {path:s} »",
"backup_deleted": "La salvagarda es estada suprimida",
"backup_hook_unknown": "Script de salvagarda « {hook:s} » desconegut",
- "backup_invalid_archive": "Archiu de salvagarda incorrècte",
+ "backup_invalid_archive": "Aquò es pas un archiu de salvagarda",
"backup_method_borg_finished": "La salvagarda dins Borg es acabada",
"backup_method_copy_finished": "La còpia de salvagarda es acabada",
- "backup_method_tar_finished": "L’archiu tar de la salvagarda es estat creat",
- "backup_output_directory_not_empty": "Lo dorsièr de sortida es pas void",
+ "backup_method_tar_finished": "L’archiu TAR de la salvagarda es estat creat",
+ "backup_output_directory_not_empty": "Devètz causir un dorsièr de sortida void",
"backup_output_directory_required": "Vos cal especificar un dorsièr de sortida per la salvagarda",
- "backup_running_app_script": "Lançament de l’escript de salvagarda de l’aplicacion « {app:s} »...",
"backup_running_hooks": "Execucion dels scripts de salvagarda…",
"backup_system_part_failed": "Impossible de salvagardar la part « {part:s} » del sistèma",
- "app_requirements_failed": "Impossible de complir las condicions requesidas per {app} : {error}",
"app_requirements_unmeet": "Las condicions requesidas per {app} son pas complidas, lo paquet {pkgname} ({version}) deu èsser {spec}",
- "appslist_could_not_migrate": "Migracion de la lista impossibla {appslist:s} ! Impossible d’analizar l’URL… L’anciana tasca cron es estada servada dins {bkp_file:s}.",
"backup_abstract_method": "Aqueste metòde de salvagarda es pas encara implementat",
"backup_applying_method_custom": "Crida lo metòde de salvagarda personalizat « {method:s} »…",
"backup_borg_not_implemented": "Lo metòde de salvagarda Bord es pas encara implementat",
@@ -95,15 +70,12 @@
"backup_csv_addition_failed": "Impossible d’ajustar de fichièrs a la salvagarda dins lo fichièr CSV",
"backup_custom_backup_error": "Fracàs del metòde de salvagarda personalizat a l’etapa « backup »",
"backup_custom_mount_error": "Fracàs del metòde de salvagarda personalizat a l’etapa « mount »",
- "backup_custom_need_mount_error": "Fracàs del metòde de salvagarda personalizat a l’etapa « need_mount »",
"backup_method_custom_finished": "Lo metòde de salvagarda personalizat « {method:s} » es acabat",
"backup_nothings_done": "I a pas res de salvagardar",
"backup_unable_to_organize_files": "Impossible d’organizar los fichièrs dins l’archiu amb lo metòde rapid",
- "service_status_failed": "Impossible de determinar l’estat del servici « {service:s} »",
"service_stopped": "Lo servici « {service:s} » es estat arrestat",
"service_unknown": "Servici « {service:s} » desconegut",
"unbackup_app": "L’aplicacion « {app:s} » serà pas salvagardada",
- "unit_unknown": "Unitat « {unit:s} » desconeguda",
"unlimit": "Cap de quòta",
"unrestore_app": "L’aplicacion « {app:s} » serà pas restaurada",
"upnp_dev_not_found": "Cap de periferic compatible UPnP pas trobat",
@@ -115,170 +87,131 @@
"yunohost_installing": "Installacion de YunoHost…",
"backup_applying_method_borg": "Mandadís de totes los fichièrs a la salvagarda dins lo repertòri borg-backup…",
"backup_csv_creation_failed": "Creacion impossibla del fichièr CSV necessari a las operacions futuras de restauracion",
- "backup_extracting_archive": "Extraccion de l’archiu de salvagarda…",
- "backup_output_symlink_dir_broken": "Avètz un ligam simbolic copat allòc de vòstre repertòri d’archiu « {path:s} ». Poiriatz aver una configuracion personalizada per salvagardar vòstras donadas sus un autre sistèma de fichièrs, en aquel cas, saique oblidèretz de montar o de connectar lo disc o la clau USB.",
+ "backup_output_symlink_dir_broken": "Vòstre repertòri d’archiu « {path:s} » es un ligam simbolic copat. Saique oblidèretz de re/montar o de connectar supòrt.",
"backup_with_no_backup_script_for_app": "L’aplicacion {app:s} a pas cap de script de salvagarda. I fasèm pas cas.",
"backup_with_no_restore_script_for_app": "L’aplicacion {app:s} a pas cap de script de restauracion, poiretz pas restaurar automaticament la salvagarda d’aquesta aplicacion.",
- "certmanager_acme_not_configured_for_domain": "Lo certificat del domeni {domain:s} sembla pas corrèctament installat. Mercés de lançar d’en primièr cert-install per aqueste domeni.",
+ "certmanager_acme_not_configured_for_domain": "Lo certificat pel domeni {domain:s} sembla pas corrèctament installat. Mercés de lançar d’en primièr « cert-install » per aqueste domeni.",
"certmanager_attempt_to_renew_nonLE_cert": "Lo certificat pel domeni {domain:s} es pas provesit per Let’s Encrypt. Impossible de lo renovar automaticament !",
"certmanager_attempt_to_renew_valid_cert": "Lo certificat pel domeni {domain:s} es a man d’expirar ! (Podètz utilizar --force se sabètz çò que fasètz)",
"certmanager_cannot_read_cert": "Quicòm a trucat en ensajar de dobrir lo certificat actual pel domeni {domain:s} (fichièr : {file:s}), rason : {reason:s}",
- "certmanager_cert_install_success": "Installacion capitada del certificat Let’s Encrypt pel domeni {domain:s} !",
- "certmanager_cert_install_success_selfsigned": "Installacion capitada del certificat auto-signat pel domeni {domain:s} !",
- "certmanager_cert_signing_failed": "Fracàs de la signatura del nòu certificat",
- "certmanager_domain_cert_not_selfsigned": "Lo certificat del domeni {domain:s} es pas auto-signat. Volètz vertadièrament lo remplaçar ? (Utiliatz --force)",
- "certmanager_domain_dns_ip_differs_from_public_ip": "L’enregistrament DNS « A » del domeni {domain:s} es diferent de l’adreça IP d’aqueste servidor. Se fa pauc qu’avètz modificat l’enregistrament « A », mercés d’esperar l’espandiment (qualques verificadors d’espandiment son disponibles en linha). (Se sabètz çò que fasèm, utilizatz --no-checks per desactivar aqueles contraròtles)",
- "certmanager_domain_http_not_working": "Sembla que lo domeni {domain:s} es pas accessible via HTTP. Mercés de verificar que las configuracions DNS e nginx son corrèctas",
- "certmanager_domain_unknown": "Domeni desconegut {domain:s}",
+ "certmanager_cert_install_success": "Lo certificat Let’s Encrypt es ara installat pel domeni « {domain:s} »",
+ "certmanager_cert_install_success_selfsigned": "Lo certificat auto-signat es ara installat pel domeni « {domain:s} »",
+ "certmanager_cert_signing_failed": "Signatura impossibla del nòu certificat",
+ "certmanager_domain_cert_not_selfsigned": "Lo certificat pel domeni {domain:s} es pas auto-signat. Volètz vertadièrament lo remplaçar ? (Utilizatz « --force » per o far)",
+ "certmanager_domain_dns_ip_differs_from_public_ip": "L’enregistrament DNS « A » pel domeni {domain:s} es diferent de l’adreça IP d’aqueste servidor. Se fa pauc qu’avètz modificat l’enregistrament « A », mercés d’esperar l’espandiment (qualques verificadors d’espandiment son disponibles en linha). (Se sabètz çò que fasèm, utilizatz --no-checks per desactivar aqueles contraròtles)",
+ "certmanager_domain_http_not_working": "Sembla que lo domeni {domain:s} es pas accessible via HTTP. Mercés de verificar que las configuracions DNS e NGINK son corrèctas",
+ "certmanager_domain_unknown": "Domeni desconegut « {domain:s} »",
"certmanager_no_cert_file": "Lectura impossibla del fichièr del certificat pel domeni {domain:s} (fichièr : {file:s})",
- "certmanager_self_ca_conf_file_not_found": "Lo fichièr de configuracion per l’autoritat del certificat auto-signat es introbabla (fichièr : {file:s})",
- "certmanager_unable_to_parse_self_CA_name": "Analisi impossible lo nom de l’autoritat del certificat auto-signat (fichièr : {file:s})",
+ "certmanager_self_ca_conf_file_not_found": "Impossible de trobar lo fichièr de configuracion per l’autoritat del certificat auto-signat (fichièr : {file:s})",
+ "certmanager_unable_to_parse_self_CA_name": "Analisi impossibla del nom de l’autoritat del certificat auto-signat (fichièr : {file:s})",
"custom_app_url_required": "Cal que donetz una URL per actualizar vòstra aplicacion personalizada {app:s}",
- "custom_appslist_name_required": "Cal que nomenetz vòstra lista d’aplicacions personalizadas",
- "diagnosis_debian_version_error": "Impossible de determinar la version de Debian : {error}",
- "diagnosis_kernel_version_error": "Impossible de recuperar la version del nuclèu : {error}",
- "diagnosis_no_apps": "Pas cap d’aplicacion installada",
- "dnsmasq_isnt_installed": "dnsmasq sembla pas èsser installat, mercés de lançar « apt-get remove bind9 && apt-get install dnsmasq »",
"domain_cannot_remove_main": "Impossible de levar lo domeni màger. Definissètz un novèl domeni màger d’en primièr",
"domain_cert_gen_failed": "Generacion del certificat impossibla",
- "domain_created": "Lo domeni es creat",
- "domain_creation_failed": "Creacion del certificat impossibla",
- "domain_deleted": "Lo domeni es suprimit",
- "domain_deletion_failed": "Supression impossibla del domeni",
- "domain_dyndns_invalid": "Domeni incorrècte per una utilizacion amb DynDNS",
+ "domain_created": "Domeni creat",
+ "domain_creation_failed": "Creacion del domeni {domain}: impossibla",
+ "domain_deleted": "Domeni suprimit",
+ "domain_deletion_failed": "Supression impossibla del domeni {domain}: {error}",
"domain_dyndns_root_unknown": "Domeni DynDNS màger desconegut",
"domain_exists": "Lo domeni existís ja",
"domain_hostname_failed": "Fracàs de la creacion d’un nòu nom d’òst. Aquò poirà provocar de problèmas mai tard (mas es pas segur… benlèu que coparà pas res).",
"domain_unknown": "Domeni desconegut",
- "domain_zone_exists": "Lo fichièr zòna DNS existís ja",
- "domain_zone_not_found": "Fichèr de zòna DNS introbable pel domeni {:s}",
"domains_available": "Domenis disponibles :",
"done": "Acabat",
"downloading": "Telecargament…",
"dyndns_could_not_check_provide": "Impossible de verificar se {provider:s} pòt provesir {domain:s}.",
- "dyndns_cron_installed": "La tasca cron pel domeni DynDNS es installada",
- "dyndns_cron_remove_failed": "Impossible de levar la tasca cron pel domeni DynDNS a causa de {error}",
- "dyndns_cron_removed": "La tasca cron pel domeni DynDNS es levada",
+ "dyndns_cron_installed": "Tasca cron pel domeni DynDNS creada",
+ "dyndns_cron_remove_failed": "Impossible de levar la tasca cron pel domeni DynDNS : {error}",
+ "dyndns_cron_removed": "Tasca cron pel domeni DynDNS levada",
"dyndns_ip_update_failed": "Impossible d’actualizar l’adreça IP sul domeni DynDNS",
- "dyndns_ip_updated": "Vòstra adreça IP es estada actualizada pel domeni DynDNS",
- "dyndns_key_generating": "La clau DNS es a se generar, pòt trigar una estona…",
+ "dyndns_ip_updated": "Vòstra adreça IP actualizada pel domeni DynDNS",
+ "dyndns_key_generating": "La clau DNS es a se generar… pòt trigar una estona.",
"dyndns_key_not_found": "Clau DNS introbabla pel domeni",
"dyndns_no_domain_registered": "Cap de domeni pas enregistrat amb DynDNS",
- "dyndns_registered": "Lo domeni DynDNS es enregistrat",
- "dyndns_registration_failed": "Enregistrament del domeni DynDNS impossibla : {error:s}",
+ "dyndns_registered": "Domeni DynDNS enregistrat",
+ "dyndns_registration_failed": "Enregistrament del domeni DynDNS impossible : {error:s}",
"dyndns_domain_not_provided": "Lo provesidor DynDNS {provider:s} pòt pas fornir lo domeni {domain:s}.",
"dyndns_unavailable": "Lo domeni {domain:s} es pas disponible.",
"extracting": "Extraccion…",
"field_invalid": "Camp incorrècte : « {:s} »",
- "format_datetime_short": "%d/%m/%Y %H:%M",
"global_settings_cant_open_settings": "Fracàs de la dobertura del fichièr de configuracion, rason : {reason:s}",
- "global_settings_key_doesnt_exists": "La clau « {settings_key:s} » existís pas dins las configuracions globalas, podètz veire totas las claus disponiblas en picant « yunohost settings list »",
- "global_settings_reset_success": "Capitada ! Vòstra configuracion precedenta es estada salvagarda dins {path:s}",
+ "global_settings_key_doesnt_exists": "La clau « {settings_key:s} » existís pas dins las configuracions globalas, podètz veire totas las claus disponiblas en executant « yunohost settings list »",
+ "global_settings_reset_success": "Configuracion precedenta ara salvagarda dins {path:s}",
"global_settings_setting_example_bool": "Exemple d’opcion booleana",
"global_settings_unknown_setting_from_settings_file": "Clau desconeguda dins los paramètres : {setting_key:s}, apartada e salvagardada dins /etc/yunohost/settings-unknown.json",
- "installation_failed": "Fracàs de l’installacion",
- "invalid_url_format": "Format d’URL pas valid",
+ "installation_failed": "Quicòm a trucat e l’installacion a pas reüssit",
"ldap_initialized": "L’annuari LDAP es inicializat",
- "license_undefined": "indefinida",
- "maindomain_change_failed": "Modificacion impossibla del domeni màger",
- "maindomain_changed": "Lo domeni màger es estat modificat",
- "migrate_tsig_end": "La migracion cap a hmac-sha512 es acabada",
+ "main_domain_change_failed": "Modificacion impossibla del domeni màger",
+ "main_domain_changed": "Lo domeni màger es estat modificat",
+ "migrate_tsig_end": "La migracion cap a HMAC-SHA512 es acabada",
"migrate_tsig_wait_2": "2 minutas…",
"migrate_tsig_wait_3": "1 minuta…",
"migrate_tsig_wait_4": "30 segondas…",
"migration_description_0002_migrate_to_tsig_sha256": "Melhora la seguretat de DynDNS TSIG en utilizar SHA512 allòc de MD5",
"migration_description_0003_migrate_to_stretch": "Mesa a nivèl del sistèma cap a Debian Stretch e YunoHost 3.0",
- "migration_0003_backward_impossible": "La migracion Stretch es pas reversibla.",
"migration_0003_start": "Aviada de la migracion cap a Stretech. Los jornals seràn disponibles dins {logfile}.",
"migration_0003_patching_sources_list": "Petaçatge de sources.lists…",
"migration_0003_main_upgrade": "Aviada de la mesa a nivèl màger…",
- "migration_0003_fail2ban_upgrade": "Aviada de la mesa a nivèl de fail2ban…",
+ "migration_0003_fail2ban_upgrade": "Aviada de la mesa a nivèl de Fail2Ban…",
"migration_0003_not_jessie": "La distribucion Debian actuala es pas Jessie !",
"migrations_cant_reach_migration_file": "Impossible d’accedir als fichièrs de migracion amb lo camin %s",
- "migrations_current_target": "La cibla de migracion est {}",
- "migrations_error_failed_to_load_migration": "ERROR : fracàs del cargament de la migracion {number} {name}",
"migrations_list_conflict_pending_done": "Podètz pas utilizar --previous e --done a l’encòp.",
"migrations_loading_migration": "Cargament de la migracion {id}…",
"migrations_no_migrations_to_run": "Cap de migracion de lançar",
- "migrations_show_currently_running_migration": "Realizacion de la migracion {number} {name}…",
- "migrations_show_last_migration": "La darrièra migracion realizada es {}",
- "monitor_glances_con_failed": "Connexion impossibla al servidor Glances",
- "monitor_not_enabled": "Lo seguiment de l’estat del servidor es pas activat",
- "monitor_stats_no_update": "Cap de donadas d’estat del servidor d’actualizar",
- "mountpoint_unknown": "Ponch de montatge desconegut",
- "mysql_db_creation_failed": "Creacion de la basa de donadas MySQL impossibla",
- "no_appslist_found": "Cap de lista d’aplicacions pas trobada",
"no_internet_connection": "Lo servidor es pas connectat a Internet",
- "package_not_installed": "Lo paquet « {pkgname} » es pas installat",
"package_unknown": "Paquet « {pkgname} » desconegut",
- "packages_no_upgrade": "I a pas cap de paquet d’actualizar",
"packages_upgrade_failed": "Actualizacion de totes los paquets impossibla",
- "path_removal_failed": "Impossible de suprimir lo camin {:s}",
"pattern_domain": "Deu èsser un nom de domeni valid (ex : mon-domeni.org)",
"pattern_email": "Deu èsser una adreça electronica valida (ex : escais@domeni.org)",
"pattern_firstname": "Deu èsser un pichon nom valid",
"pattern_lastname": "Deu èsser un nom valid",
"pattern_password": "Deu conténer almens 3 caractèrs",
- "pattern_port": "Deu èsser un numèro de pòrt valid (ex : 0-65535)",
"pattern_port_or_range": "Deu èsser un numèro de pòrt valid (ex : 0-65535) o un interval de pòrt (ex : 100:200)",
"pattern_positive_number": "Deu èsser un nombre positiu",
"port_already_closed": "Lo pòrt {port:d} es ja tampat per las connexions {ip_version:s}",
"port_already_opened": "Lo pòrt {port:d} es ja dubèrt per las connexions {ip_version:s}",
- "port_available": "Lo pòrt {port:d} es disponible",
- "port_unavailable": "Lo pòrt {port:d} es pas disponible",
"restore_already_installed_app": "Una aplicacion es ja installada amb l’id « {app:s} »",
"restore_app_failed": "Impossible de restaurar l’aplicacion « {app:s} »",
- "backup_ask_for_copying_if_needed": "D’unes fichièrs an pas pogut èsser preparatz per la salvagarda en utilizar lo metòde qu’evita de gastar d’espaci sul sistèma de manièra temporària. Per lançar la salvagarda, cal utilizar temporàriament {size:s} Mo. Acceptatz ?",
+ "backup_ask_for_copying_if_needed": "Volètz far una salvagarda en utilizant {size:s} Mo temporàriament ? (Aqueste biais de far es emplegat perque unes fichièrs an pas pogut èsser preparats amb un metòde mai eficaç.)",
"yunohost_not_installed": "YunoHost es pas installat o corrèctament installat. Mercés d’executar « yunohost tools postinstall »",
- "backup_output_directory_forbidden": "Repertòri de destinacion defendut. Las salvagardas pòdon pas se realizar dins los repertòris bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var ou /home/yunohost.backup/archives",
+ "backup_output_directory_forbidden": "Causissètz un repertòri de destinacion deferent. Las salvagardas pòdon pas se realizar dins los repertòris bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var ou /home/yunohost.backup/archives",
"certmanager_attempt_to_replace_valid_cert": "Sètz a remplaçar un certificat corrècte e valid pel domeni {domain:s} ! (Utilizatz --force per cortcircuitar)",
- "certmanager_cert_renew_success": "Renovèlament capitat d’un certificat Let’s Encrypt pel domeni {domain:s} !",
- "certmanager_certificate_fetching_or_enabling_failed": "Sembla d’aver fracassat l’activacion d’un nòu certificat per {domain:s}…",
- "certmanager_conflicting_nginx_file": "Impossible de preparar lo domeni pel desfís ACME : lo fichièr de configuracion nginx {filepath:s} es en conflicte e deu èsser levat d’en primièr",
+ "certmanager_cert_renew_success": "Renovèlament capitat d’un certificat Let’s Encrypt pel domeni « {domain:s} »",
+ "certmanager_certificate_fetching_or_enabling_failed": "Sembla qu’utilizar lo nòu certificat per {domain:s} fonciona pas…",
+ "certmanager_conflicting_nginx_file": "Impossible de preparar lo domeni pel desfís ACME : lo fichièr de configuracion NGINX {filepath:s} es en conflicte e deu èsser levat d’en primièr",
"certmanager_couldnt_fetch_intermediate_cert": "Expiracion del relambi pendent l’ensag de recuperacion del certificat intermediari dins de Let’s Encrypt. L’installacion / lo renovèlament es estat interromput - tornatz ensajar mai tard.",
- "certmanager_domain_not_resolved_locally": "Lo domeni {domain:s} pòt pas èsser determinat dins de vòstre servidor YunoHost. Pòt arribar s’avètz recentament modificat vòstre enregistrament DNS. Dins aqueste cas, mercés d’esperar unas oras per l’espandiment. Se lo problèma dura, consideratz ajustar {domain:s} a /etc/hosts. (Se sabètz çò que fasètz, utilizatz --no-checks per desactivar las verificacions.)",
"certmanager_error_no_A_record": "Cap d’enregistrament DNS « A » pas trobat per {domain:s}. Vos cal indicar que lo nom de domeni mene a vòstra maquina per poder installar un certificat Let’S Encrypt ! (Se sabètz çò que fasètz, utilizatz --no-checks per desactivar las verificacions.)",
"certmanager_hit_rate_limit": "Tròp de certificats son ja estats demandats recentament per aqueste ensem de domeni {domain:s}. Mercés de tornar ensajar mai tard. Legissètz https://letsencrypt.org/docs/rate-limits/ per mai detalhs",
"certmanager_http_check_timeout": "Expiracion del relambi d’ensag del servidor de se contactar via HTTP amb son adreça IP publica {domain:s} amb l’adreça {ip:s}. Coneissètz benlèu de problèmas d’hairpinning o lo parafuòc/router amont de vòstre servidor es mal configurat.",
"domain_dns_conf_is_just_a_recommendation": "Aqueste pagina mòstra la configuracion *recomandada*. Non configura *pas* lo DNS per vos. Sètz responsable de la configuracion de vòstra zòna DNS en çò de vòstre registrar DNS amb aquesta recomandacion.",
"domain_dyndns_already_subscribed": "Avètz ja soscrich a un domeni DynDNS",
- "domain_dyndns_dynette_is_unreachable": "Impossible de contactar la dynette YunoHost, siá YunoHost pas es pas corrèctament connectat a Internet, siá lo servidor de la dynett es arrestat. Error : {error}",
"domain_uninstall_app_first": "Una o mantuna aplicacions son installadas sus aqueste domeni. Mercés de las desinstallar d’en primièr abans de suprimir aqueste domeni",
"firewall_reload_failed": "Impossible de recargar lo parafuòc",
- "firewall_reloaded": "Lo parafuòc es estat recargat",
+ "firewall_reloaded": "Parafuòc recargat",
"firewall_rules_cmd_failed": "Unas règlas del parafuòc an fracassat. Per mai informacions, consultatz lo jornal.",
- "global_settings_bad_choice_for_enum": "La valor del paramètre {setting:s} es incorrècta. Recebut : {received_type:s}, mas las opcions esperadas son : {expected_type:s}",
- "global_settings_bad_type_for_setting": "Lo tipe del paramètre {setting:s} es incorrècte. Recebut : {received_type:s}, esperat {expected_type:s}",
+ "global_settings_bad_choice_for_enum": "La valor del paramètre {setting:s} es incorrècta. Recebut : {choice:s}, mas las opcions esperadas son : {available_choices:s}",
+ "global_settings_bad_type_for_setting": "Lo tipe del paramètre {setting:s} es incorrècte, recebut : {received_type:s}, esperat {expected_type:s}",
"global_settings_cant_write_settings": "Fracàs de l’escritura del fichièr de configuracion, rason : {reason:s}",
"global_settings_setting_example_enum": "Exemple d’opcion de tipe enumeracion",
"global_settings_setting_example_int": "Exemple d’opcion de tipe entièr",
"global_settings_setting_example_string": "Exemple d’opcion de tipe cadena",
"global_settings_unknown_type": "Situacion inesperada, la configuracion {setting:s} sembla d’aver lo tipe {unknown_type:s} mas es pas un tipe pres en carga pel sistèma.",
- "hook_exec_failed": "Fracàs de l’execucion del script « {path:s} »",
- "hook_exec_not_terminated": "L’execucion del escript « {path:s} » es pas acabada",
+ "hook_exec_failed": "Fracàs de l’execucion del script : « {path:s} »",
+ "hook_exec_not_terminated": "Lo escript « {path:s} » a pas acabat corrèctament",
"hook_list_by_invalid": "La proprietat de tria de las accions es invalida",
"hook_name_unknown": "Nom de script « {name:s} » desconegut",
"ldap_init_failed_to_create_admin": "L’inicializacion de LDAP a pas pogut crear l’utilizaire admin",
"mail_domain_unknown": "Lo domeni de corrièl « {domain:s} » es desconegut",
"mailbox_used_space_dovecot_down": "Lo servici corrièl Dovecot deu èsser aviat, se volètz conéisser l’espaci ocupat per la messatjariá",
- "migrate_tsig_failed": "La migracion del domeni dyndns {domain} cap a hmac-sha512 a pas capitat, anullacion de las modificacions. Error : {error_code} - {error}",
- "migrate_tsig_wait": "Esperem 3 minutas que lo servidor dyndns prenga en compte la novèla clau…",
- "migrate_tsig_not_needed": "Sembla qu’utilizatz pas un domeni dyndns, donc cap de migracion es pas necessària !",
+ "migrate_tsig_failed": "La migracion del domeni DynDNS {domain} cap a HMAC-SHA512 a pas capitat, anullacion de las modificacions. Error : {error_code} - {error}",
+ "migrate_tsig_wait": "Esperem 3 minutas que lo servidor DynDNS prenga en compte la novèla clau…",
+ "migrate_tsig_not_needed": "Sembla qu’utilizatz pas un domeni DynDNS, donc cap de migracion es pas necessària.",
"migration_0003_yunohost_upgrade": "Aviada de la mesa a nivèl del paquet YunoHost… La migracion acabarà, mas l’actualizacion reala se realizarà tot bèl aprèp. Un còp acabada, poiretz vos reconnectar a l’administracion web.",
"migration_0003_system_not_fully_up_to_date": "Lo sistèma es pas complètament a jorn. Mercés de lançar una mesa a jorn classica abans de començar la migracion per Stretch.",
"migration_0003_modified_files": "Mercés de notar que los fichièrs seguents son estats detectats coma modificats manualament e poiràn èsser escafats a la fin de la mesa a nivèl : {manually_modified_files}",
- "monitor_period_invalid": "Lo periòde de temps es incorrècte",
- "monitor_stats_file_not_found": "Lo fichièr d’estatisticas es introbable",
- "monitor_stats_period_unavailable": "Cap d’estatisticas son pas disponiblas pel periòde",
- "mysql_db_init_failed": "Impossible d’inicializar la basa de donadas MySQL",
"service_disable_failed": "Impossible de desactivar lo servici « {service:s} »↵\n↵\nJornals recents : {logs:s}",
"service_disabled": "Lo servici « {service:s} » es desactivat",
"service_enable_failed": "Impossible d’activar lo servici « {service:s} »↵\n↵\nJornals recents : {logs:s}",
"service_enabled": "Lo servici « {service:s} » es activat",
- "service_no_log": "Cap de jornal de far veire pel servici « {service:s} »",
- "service_regenconf_dry_pending_applying": "Verificacion de las configuracions en espèra que poirián èsser aplicadas pel servici « {service} »…",
- "service_regenconf_failed": "Regeneracion impossibla de la configuracion pels servicis : {services}",
- "service_regenconf_pending_applying": "Aplicacion de las configuracions en espèra pel servici « {service} »…",
"service_remove_failed": "Impossible de levar lo servici « {service:s} »",
"service_removed": "Lo servici « {service:s} » es estat levat",
"service_start_failed": "Impossible d’aviar lo servici « {service:s} »↵\n↵\nJornals recents : {logs:s}",
@@ -296,26 +229,14 @@
"user_deleted": "L’utilizaire es suprimit",
"user_deletion_failed": "Supression impossibla de l’utilizaire",
"user_home_creation_failed": "Creacion impossibla del repertòri personal a l’utilizaire",
- "user_info_failed": "Recuperacion impossibla de las informacions tocant l’utilizaire",
"user_unknown": "Utilizaire « {user:s} » desconegut",
"user_update_failed": "Modificacion impossibla de l’utilizaire",
"user_updated": "L’utilizaire es estat modificat",
"yunohost_ca_creation_failed": "Creacion impossibla de l’autoritat de certificacion",
"yunohost_ca_creation_success": "L’autoritat de certificacion locala es creada.",
- "service_conf_file_kept_back": "Lo fichièr de configuracion « {conf} » deuriá èsser suprimit pel servici {service} mas es estat servat.",
- "service_conf_file_manually_modified": "Lo fichièr de configuracion « {conf} » es estat modificat manualament e serà pas actualizat",
- "service_conf_file_manually_removed": "Lo fichièr de configuracion « {conf} » es suprimit manualament e serà pas creat",
- "service_conf_file_remove_failed": "Supression impossibla del fichièr de configuracion « {conf} »",
- "service_conf_file_removed": "Lo fichièr de configuracion « {conf} » es suprimit",
- "service_conf_file_updated": "Lo fichièr de configuracion « {conf} » es actualizat",
- "service_conf_new_managed_file": "Lo servici {service} gerís ara lo fichièr de configuracion « {conf} ».",
- "service_conf_up_to_date": "La configuracion del servici « {service} » es ja actualizada",
- "service_conf_would_be_updated": "La configuracion del servici « {service} » seriá estada actualizada",
"service_description_avahi-daemon": "permet d’aténher vòstre servidor via yunohost.local sus vòstre ret local",
"service_description_dnsmasq": "gerís la resolucion dels noms de domeni (DNS)",
"updating_apt_cache": "Actualizacion de la lista dels paquets disponibles…",
- "service_conf_file_backed_up": "Lo fichièr de configuracion « {conf} » es salvagardat dins « {backup} »",
- "service_conf_file_copy_failed": "Còpia impossibla del nòu fichièr de configuracion « {new} » cap a « {conf} »",
"server_reboot_confirm": "Lo servidor es per reaviar sul pic, o volètz vertadièrament ? {answers:s}",
"service_add_failed": "Apondon impossible del servici « {service:s} »",
"service_added": "Lo servici « {service:s} » es ajustat",
@@ -328,7 +249,6 @@
"restore_failed": "Impossible de restaurar lo sistèma",
"restore_hook_unavailable": "Lo script de restauracion « {part:s} » es pas disponible sus vòstre sistèma e es pas tanpauc dins l’archiu",
"restore_may_be_not_enough_disk_space": "Lo sistèma sembla d’aver pas pro d’espaci disponible (liure : {free_space:d} octets, necessari : {needed_space:d} octets, marge de seguretat : {margin:d} octets)",
- "restore_mounting_archive": "Montatge de l’archiu dins « {path:s} »",
"restore_not_enough_disk_space": "Espaci disponible insufisent (liure : {free_space:d} octets, necessari : {needed_space:d} octets, marge de seguretat : {margin:d} octets)",
"restore_nothings_done": "Res es pas estat restaurat",
"restore_removing_tmp_dir_failed": "Impossible de levar u ancian repertòri temporari",
@@ -338,90 +258,58 @@
"server_shutdown": "Lo servidor serà atudat",
"server_shutdown_confirm": "Lo servidor es per s’atudar sul pic, o volètz vertadièrament ? {answers:s}",
"server_reboot": "Lo servidor es per reaviar",
- "network_check_mx_ko": "L’enregistrament DNS MX es pas especificat",
- "new_domain_required": "Vos cal especificar lo domeni màger",
- "no_ipv6_connectivity": "La connectivitat IPv6 es pas disponibla",
"not_enough_disk_space": "Espaci disc insufisent sus « {path:s} »",
- "package_unexpected_error": "Una error inesperada es apareguda amb lo paquet « {pkgname} »",
- "packages_upgrade_critical_later": "Los paquets critics {packages:s} seràn actualizats mai tard",
- "restore_action_required": "Devètz precisar çò que cal restaurar",
"service_cmd_exec_failed": "Impossible d’executar la comanda « {command:s} »",
- "service_conf_updated": "La configuracion es estada actualizada pel servici « {service} »",
"service_description_mysql": "garda las donadas de las aplicacions (base de donadas SQL)",
- "service_description_php5-fpm": "executa d’aplicacions escrichas en PHP amb nginx",
"service_description_postfix": "emplegat per enviar e recebre de corrièls",
- "service_description_rmilter": "verifica mantun paramètres dels corrièls",
"service_description_slapd": "garda los utilizaires, domenis e lors informacions ligadas",
"service_description_ssh": "vos permet de vos connectar a distància a vòstre servidor via un teminal (protocòl SSH)",
"service_description_yunohost-api": "permet las interaccions entre l’interfàcia web de YunoHost e le sistèma",
"service_description_yunohost-firewall": "gerís los pòrts de connexion dobèrts e tampats als servicis",
- "ssowat_persistent_conf_read_error": "Error en legir la configuracion duradissa de SSOwat : {error:s}. Modificatz lo fichièr /etc/ssowat/conf.json.persistent per reparar la sintaxi JSON",
- "ssowat_persistent_conf_write_error": "Error en salvagardar la configuracion duradissa de SSOwat : {error:s}. Modificatz lo fichièr /etc/ssowat/conf.json.persistent per reparar la sintaxi JSON",
- "certmanager_old_letsencrypt_app_detected": "\nYunohost a detectat que l’aplicacion ’letsencrypt’ es installada, aquò es en conflicte amb las novèlas foncionalitats integradas de gestion dels certificats de Yunohost. Se volètz utilizar aquelas foncionalitats integradas, mercés de lançar las comandas seguentas per migrar vòstra installacion :\n\n yunohost app remove letsencrypt\n yunohost domain cert-install\n\nN.B. : aquò provarà de tornar installar los certificats de totes los domenis amb un certificat Let’s Encrypt o las auto-signats",
- "diagnosis_monitor_disk_error": "Impossible de supervisar los disques : {error}",
- "diagnosis_monitor_network_error": "Impossible de supervisar la ret : {error}",
- "diagnosis_monitor_system_error": "Impossible de supervisar lo sistèma : {error}",
"executing_command": "Execucion de la comanda « {command:s} »…",
"executing_script": "Execucion del script « {script:s} »…",
"global_settings_cant_serialize_settings": "Fracàs de la serializacion de las donadas de parametratge, rason : {reason:s}",
"ip6tables_unavailable": "Podètz pas jogar amb ip6tables aquí. Siá sèts dins un contenedor, siá vòstre nuclèu es pas compatible amb aquela opcion",
"iptables_unavailable": "Podètz pas jogar amb iptables aquí. Siá sèts dins un contenedor, siá vòstre nuclèu es pas compatible amb aquela opcion",
- "update_cache_failed": "Impossible d’actualizar lo cache de l’APT",
"mail_alias_remove_failed": "Supression impossibla de l’alias de corrièl « {mail:s} »",
"mail_forward_remove_failed": "Supression impossibla del corrièl de transferiment « {mail:s} »",
- "migrate_tsig_start": "L’algorisme de generacion de claus es pas pro securizat per la signatura TSIG del domeni « {domain} », lançament de la migracion cap a hmac-sha512 que’s mai securizat",
+ "migrate_tsig_start": "L’algorisme de generacion de claus es pas pro securizat per la signatura TSIG del domeni « {domain} », lançament de la migracion cap a un mai segur HMAC-SHA-512",
"migration_description_0001_change_cert_group_to_sslcert": "Càmbia las permissions de grop dels certificats de « metronome » per « ssl-cert »",
"migration_0003_restoring_origin_nginx_conf": "Vòstre fichièr /etc/nginx/nginx.conf es estat modificat manualament. La migracion reïnicializarà d’en primièr son estat origina… Lo fichièr precedent serà disponible coma {backup_dest}.",
"migration_0003_still_on_jessie_after_main_upgrade": "Quicòm a trucat pendent la mesa a nivèl màger : lo sistèma es encara jos Jessie ?!? Per trobar lo problèma, agachatz {log}…",
"migration_0003_general_warning": "Notatz qu’aquesta migracion es una operacion delicata. Encara que la còla YunoHost aguèsse fach çò melhor per la tornar legir e provar, la migracion poiriá copar de parts del sistèma o de las aplicacions.\n\nEn consequéncia, vos recomandam :\n· · · · - de lançar una salvagarda de vòstras donadas o aplicacions criticas. Mai d’informacions a https://yunohost.org/backup ;\n· · · · - d’èsser pacient aprèp aver lançat la migracion : segon vòstra connexion Internet e material, pòt trigar qualques oras per que tot siá mes al nivèl.\n\nEn mai, lo pòrt per SMTP, utilizat pels clients de corrièls extèrns (coma Thunderbird o K9-Mail per exemple) foguèt cambiat de 465 (SSL/TLS) per 587 (STARTTLS). L’ancian pòrt 465 serà automaticament tampat e lo nòu pòrt 587 serà dobèrt dins lo parafuòc. Vosautres e vòstres utilizaires *auretz* d’adaptar la configuracion de vòstre client de corrièl segon aqueles cambiaments !",
"migration_0003_problematic_apps_warning": "Notatz que las aplicacions seguentas, saique problematicas, son estadas desactivadas. Semblan d’aver estadas installadas d’una lista d’aplicacions o que son pas marcadas coma «working ». En consequéncia, podèm pas assegurar que tendràn de foncionar aprèp la mesa a nivèl : {problematic_apps}",
- "migrations_bad_value_for_target": "Nombre invalid pel paramètre « target », los numèros de migracion son 0 o {}",
"migrations_migration_has_failed": "La migracion {id} a pas capitat, abandon. Error : {exception}",
"migrations_skip_migration": "Passatge de la migracion {id}…",
"migrations_to_be_ran_manually": "La migracion {id} deu èsser lançada manualament. Mercés d’anar a Aisinas > Migracion dins l’interfàcia admin, o lançar « yunohost tools migrations migrate ».",
"migrations_need_to_accept_disclaimer": "Per lançar la migracion {id} , avètz d’acceptar aquesta clausa de non-responsabilitat :\n---\n{disclaimer}\n---\nS’acceptatz de lançar la migracion, mercés de tornar executar la comanda amb l’opcion accept-disclaimer.",
- "monitor_disabled": "La supervision del servidor es desactivada",
- "monitor_enabled": "La supervision del servidor es activada",
- "mysql_db_initialized": "La basa de donadas MySQL es estada inicializada",
- "no_restore_script": "Lo script de salvagarda es pas estat trobat per l’aplicacion « {app:s} »",
"pattern_backup_archive_name": "Deu èsser un nom de fichièr valid compausat de 30 caractèrs alfanumerics al maximum e « -_. »",
- "pattern_listname": "Deu èsser compausat solament de caractèrs alfanumerics e de tirets basses",
"service_description_dovecot": "permet als clients de messatjariá d’accedir/recuperar los corrièls (via IMAP e POP3)",
"service_description_fail2ban": "protegís contra los atacs brute-force e d’autres atacs venents d’Internet",
- "service_description_glances": "susvelha las informacions sistèma de vòstre servidor",
"service_description_metronome": "gerís los comptes de messatjariás instantanèas XMPP",
"service_description_nginx": "fornís o permet l’accès a totes los sites web albergats sus vòstre servidor",
"service_description_nslcd": "gerís la connexion en linha de comanda dels utilizaires YunoHost",
"service_description_redis-server": "una basa de donadas especializada per un accès rapid a las donadas, las filas d’espèra e la comunicacion entre programas",
"service_description_rspamd": "filtra lo corrièl pas desirat e mai foncionalitats ligadas al corrièl",
- "migrations_backward": "Migracion en darrièr.",
- "migrations_forward": "Migracion en avant",
- "network_check_smtp_ko": "Lo trafic de corrièl sortent (pòrt 25 SMTP) sembla blocat per vòstra ret",
- "network_check_smtp_ok": "Lo trafic de corrièl sortent (pòrt 25 SMTP) es pas blocat",
"pattern_mailbox_quota": "Deu èsser una talha amb lo sufixe b/k/M/G/T o 0 per desactivar la quòta",
- "backup_archive_writing_error": "Impossible d’ajustar los fichièrs a la salvagarda dins l’archiu comprimit",
+ "backup_archive_writing_error": "Impossible d’ajustar los fichièrs « {source:s} » a la salvagarda (nomenats dins l’archiu « {dest:s} »)dins l’archiu comprimit « {archive:s} »",
"backup_cant_mount_uncompress_archive": "Impossible de montar en lectura sola lo repertòri de l’archiu descomprimit",
"backup_no_uncompress_archive_dir": "Lo repertòri de l’archiu descomprimit existís pas",
"pattern_username": "Deu èsser compausat solament de caractèrs alfanumerics en letras minusculas e de tirets basses",
"experimental_feature": "Atencion : aquesta foncionalitat es experimentala e deu pas èsser considerada coma establa, deuriatz pas l’utilizar levat que sapiatz çò que fasètz.",
- "log_corrupted_md_file": "Lo fichièr yaml de metadonada amb los jornals d’audit es corromput : « {md_file} »\nError : {error:s}",
+ "log_corrupted_md_file": "Lo fichièr YAML de metadonadas ligat als jornals d’audit es damatjat : « {md_file} »\nError : {error:s}",
"log_category_404": "La categoria de jornals d’audit « {category} » existís pas",
"log_link_to_log": "Jornal complèt d’aquesta operacion : {desc}",
"log_help_to_get_log": "Per veire lo jornal d’aquesta operacion « {desc} », utilizatz la comanda « yunohost log display {name} »",
- "backup_php5_to_php7_migration_may_fail": "Impossible de convertir vòstre archiu per prendre en carga PHP 7, la restauracion de vòstras aplicacions PHP pòt reüssir pas (rason : {error:s})",
+ "backup_php5_to_php7_migration_may_fail": "Impossible de convertir vòstre archiu per prendre en carga PHP 7, la restauracion de vòstras aplicacions PHP pòt reüssir pas a restaurar vòstras aplicacions PHP (rason : {error:s})",
"log_link_to_failed_log": "L’operacion « {desc} » a pas capitat ! Per obténer d’ajuda, mercés de fornir lo jornal complèt de l’operacion",
"log_help_to_get_failed_log": "L’operacion « {desc} » a pas reüssit ! Per obténer d’ajuda, mercés de partejar lo jornal d’audit complèt d’aquesta operacion en utilizant la comanda « yunohost log display {name} --share »",
"log_does_exists": "I a pas cap de jornal d’audit per l’operacion amb lo nom « {log} », utilizatz « yunohost log list » per veire totes los jornals d’operacion disponibles",
"log_operation_unit_unclosed_properly": "L’operacion a pas acabat corrèctament",
- "log_app_addaccess": "Ajustar l’accès a « {} »",
- "log_app_removeaccess": "Tirar l’accès a « {} »",
- "log_app_clearaccess": "Tirar totes los accèsses a « {} »",
- "log_app_fetchlist": "Ajustar una lista d’aplicacions",
- "log_app_removelist": "Levar una lista d’aplicacions",
"log_app_change_url": "Cambiar l’URL de l’aplicacion « {} »",
"log_app_install": "Installar l’aplicacion « {} »",
"log_app_remove": "Levar l’aplicacion « {} »",
- "log_app_upgrade": "Actualizacion de l’aplicacion « {} »",
+ "log_app_upgrade": "Actualizar l’aplicacion « {} »",
"log_app_makedefault": "Far venir « {} » l’aplicacion per defaut",
"log_available_on_yunopaste": "Lo jornal es ara disponible via {url}",
"log_backup_restore_system": "Restaurar lo sistèma a partir d’una salvagarda",
@@ -432,35 +320,30 @@
"log_domain_remove": "Tirar lo domeni « {} » d’a la configuracion sistèma",
"log_dyndns_subscribe": "S’abonar al subdomeni YunoHost « {} »",
"log_dyndns_update": "Actualizar l’adreça IP ligada a vòstre jos-domeni YunoHost « {} »",
- "log_letsencrypt_cert_install": "Installar lo certificat Let's encrypt sul domeni « {} »",
+ "log_letsencrypt_cert_install": "Installar un certificat Let's Encrypt sul domeni « {} »",
"log_selfsigned_cert_install": "Installar lo certificat auto-signat sul domeni « {} »",
- "log_letsencrypt_cert_renew": "Renovar lo certificat Let's encrypt de « {} »",
- "log_service_enable": "Activar lo servici « {} »",
- "log_service_regen_conf": "Regenerar la configuracion sistèma de « {} »",
+ "log_letsencrypt_cert_renew": "Renovar lo certificat Let's Encrypt de « {} »",
"log_user_create": "Ajustar l’utilizaire « {} »",
"log_user_delete": "Levar l’utilizaire « {} »",
- "log_user_update": "Actualizar las informacions a l’utilizaire « {} »",
- "log_tools_maindomain": "Far venir « {} » lo domeni màger",
+ "log_user_update": "Actualizar las informacions de l’utilizaire « {} »",
+ "log_domain_main_domain": "Far venir « {} » lo domeni màger",
"log_tools_migrations_migrate_forward": "Migrar",
- "log_tools_migrations_migrate_backward": "Tornar en arrièr",
"log_tools_postinstall": "Realizar la post installacion del servidor YunoHost",
"log_tools_upgrade": "Actualizacion dels paquets sistèma",
"log_tools_shutdown": "Atudar lo servidor",
"log_tools_reboot": "Reaviar lo servidor",
"mail_unavailable": "Aquesta adreça electronica es reservada e deu èsser automaticament atribuida al tot bèl just primièr utilizaire",
"migration_description_0004_php5_to_php7_pools": "Tornar configurar lo pools PHP per utilizar PHP 7 allòc del 5",
- "migration_description_0005_postgresql_9p4_to_9p6": "Migracion de las basas de donadas de postgresql 9.4 cap a 9.6",
- "migration_0005_postgresql_94_not_installed": "Postgresql es pas installat sul sistèma. Pas res de far !",
+ "migration_description_0005_postgresql_9p4_to_9p6": "Migracion de las basas de donadas de PostgreSQL9.4 cap a 9.6",
+ "migration_0005_postgresql_94_not_installed": "PostgreSQL es pas installat sul sistèma. I a pas res per far.",
"migration_0005_postgresql_96_not_installed": "Avèm trobat que Postgresql 9.4 es installat, mas cap de version de Postgresql 9.6 pas trobada !? Quicòm d’estranh a degut arribar a vòstre sistèma :( …",
"migration_0005_not_enough_space": "I a pas pro d’espaci disponible sus {path} per lançar la migracion d’aquela passa :(.",
- "recommend_to_add_first_user": "La post installacion es acabada, mas YunoHost fa besonh d’almens un utilizaire per foncionar coma cal. Vos cal n’ajustar un en utilizant la comanda « yunohost user create Jornals d’audit (dins l’interfàcia d’administracion) o amb « yunohost log list » (en linha de comanda).",
"update_apt_cache_failed": "I a agut d’errors en actualizar la memòria cache d’APT (lo gestionari de paquets de Debian). Aquí avètz las linhas de sources.list que pòdon vos ajudar a identificar las linhas problematicas : \n{sourceslist}",
"update_apt_cache_warning": "I a agut d’errors en actualizar la memòria cache d’APT (lo gestionari de paquets de Debian). Aquí avètz las linhas de sources.list que pòdon vos ajudar a identificar las linhas problematicas : \n{sourceslist}",
- "apps_permission_not_found": "Cap d’autorizacion pas trobada per las aplicacions installadas",
"backup_permission": "Autorizacion de salvagarda per l’aplicacion {app:s}",
- "edit_group_not_allowed": "Sètz pas autorizat a cambiar lo grop {group:s}",
- "error_when_removing_sftpuser_group": "Error en ensajar de suprimir lo grop sftpusers",
- "group_name_already_exist": "Lo grop {name:s} existís ja",
- "group_created": "Lo grop « {group} » es estat corrèctament creat",
- "group_creation_failed": "Fracàs de la creacion del grop « {group} »",
+ "group_created": "Grop « {group} » creat",
+ "group_creation_failed": "Fracàs de la creacion del grop « {group} » : {error}",
"group_deleted": "Lo grop « {group} » es estat suprimit",
- "group_deletion_failed": "Fracàs de la supression del grop « {group} »",
- "group_deletion_not_allowed": "Lo grop « {group} » pòt pas èsser suprimir manualament.",
- "group_info_failed": "Recuperacion de las informacions del grop « {group} » impossibla",
+ "group_deletion_failed": "Fracàs de la supression del grop « {group} » : {error}",
"group_unknown": "Lo grop « {group} » es desconegut",
- "log_user_group_add": "Ajustar lo grop « {} »",
"log_user_group_delete": "Suprimir lo grop « {} »",
"migration_0011_backup_before_migration": "Creacion d’una còpia de seguretat de la basa de donadas LDAP e de la configuracion de las aplicacions abans d’efectuar la migracion.",
"migration_0011_create_group": "Creacion d’un grop per cada utilizaire…",
@@ -568,49 +441,135 @@
"migration_0011_LDAP_update_failed": "Actualizacion impossibla de LDAP. Error : {error:s}",
"migration_0011_migration_failed_trying_to_rollback": "La migracion a fracassat… ensag de tornar lo sistèma a l’estat anterio.",
"migration_0011_rollback_success": "Restauracion del sistèma reüssida.",
- "apps_permission_restoration_failed": "Fracàs de la permission « {permission:s} » per la restauracion de l’aplicacion {app:s}",
- "group_already_allowed": "Lo grop « {group:s} » a ja la permission « {permission:s} » activada per l’aplicacion « {app:s} »",
- "group_already_disallowed": "Lo grop « {group:s} »a ja las permissions « {permission:s} » desactivadas per l’aplicacion « {app:s} »",
"group_updated": "Lo grop « {group} » es estat actualizat",
- "group_update_failed": "Actualizacion impossibla del grop « {group} »",
- "log_permission_add": "Ajustar la permission « {} » per l’aplicacion « {} »",
- "log_permission_remove": "Suprimir la permission « {} »",
- "log_permission_update": "Actualizacion de la permission « {} » per l’aplicacion « {} »",
+ "group_update_failed": "Actualizacion impossibla del grop « {group} » : {error}",
"log_user_group_update": "Actualizar lo grop « {} »",
- "log_user_permission_add": "Actualizar la permission « {} »",
- "log_user_permission_remove": "Actualizar la permission « {} »",
"migration_description_0011_setup_group_permission": "Configurar lo grop d’utilizaire e las permission de las aplicacions e dels servicis",
"migration_0011_can_not_backup_before_migration": "La salvagarda del sistèma abans la migracion a pas capitat. La migracion a fracassat. Error : {error:s}",
"migration_0011_migrate_permission": "Migracion de las permission dels paramètres d’aplicacion a LDAP…",
"migration_0011_update_LDAP_database": "Actualizacion de la basa de donadas LDAP…",
"migration_0011_update_LDAP_schema": "Actualizacion de l’esquèma LDAP…",
- "permission_already_exist": "La permission « {permission:s} » per l’aplicacion {app:s} existís ja",
- "permission_created": "Permission creada « {permission:s} » per l’aplicacion{app:s}",
+ "permission_already_exist": "La permission « {permission:s} » existís ja",
+ "permission_created": "Permission « {permission:s} » creada",
"permission_creation_failed": "Creacion impossibla de la permission",
- "permission_deleted": "Permission « {permission:s} » per l’aplicacion {app:s} suprimida",
- "permission_deletion_failed": "Fracàs de la supression de la permission « {permission:s} » per l’aplicacion {app:s}",
- "permission_not_found": "Permission « {permission:s} » pas trobada per l’aplicacion {app:s}",
- "permission_name_not_valid": "Lo nom de la permission « {permission:s} » es pas valid",
+ "permission_deleted": "Permission « {permission:s} » suprimida",
+ "permission_deletion_failed": "Fracàs de la supression de la permission « {permission:s} »",
+ "permission_not_found": "Permission « {permission:s} » pas trobada",
"permission_update_failed": "Fracàs de l’actualizacion de la permission",
- "permission_generated": "La basa de donadas de las permission es estada actualizada",
- "permission_updated": "La permission « {permission:s} » per l’aplicacion {app:s} es estada actualizada",
+ "permission_updated": "La permission « {permission:s} » es estada actualizada",
"permission_update_nothing_to_do": "Cap de permission d’actualizar",
- "remove_main_permission_not_allowed": "Se pòt pas suprimir la permission màger",
- "remove_user_of_group_not_allowed": "Sètz pas autorizat a suprimir {user:s} del grop {group:s}",
- "system_groupname_exists": "Lo nom del grop existís ja dins lo sistèma de grops",
- "tools_update_failed_to_app_fetchlist": "Fracàs de l’actualizacion de la lista d’aplicacions de YunoHost a causa de : {error}",
- "user_already_in_group": "L’utilizaire {user:} es ja dins lo grop {group:s}",
- "user_not_in_group": "L’utilizaire {user:} es pas dins lo grop {group:s}",
- "edit_permission_with_group_all_users_not_allowed": "Podètz pas modificar las permissions del grop « all_users », utilizatz « yunohost user permission clear APP » o « yunohost user permission add APP -u USER ».",
"mailbox_disabled": "La bóstia de las letras es desactivada per l’utilizaire {user:s}",
- "migration_0011_LDAP_config_dirty": "Sembla qu’avètz modificat manualament la configuracion LDAP. Per far aquesta migracion cal actualizar la configuracion LDAP.\nSalvagardatz la configuracion actuala, reïnicializatz la configuracion originala amb la comanda « yunohost tools regen-conf -f » e tornatz ensajar la migracion",
- "need_define_permission_before": "Vos cal tornar definir las permission en utilizant « yunohost user permission add -u USER » abans de suprimir un grop permés",
- "permission_already_clear": "La permission « {permission:s} » ja levada per l’aplicacion {app:s}",
- "migration_description_0012_postgresql_password_to_md5_authentication": "Forçar l’autentificacion postgresql a utilizar md5 per las connexions localas",
+ "migration_description_0012_postgresql_password_to_md5_authentication": "Forçar l’autentificacion PostgreSQL a utilizar MD5 per las connexions localas",
"migrations_success_forward": "Migracion {id} corrèctament realizada !",
"migrations_running_forward": "Execucion de la migracion {id}…",
"migrations_must_provide_explicit_targets": "Devètz fornir una cibla explicita quand utilizatz using --skip o --force-rerun",
"migrations_exclusive_options": "--auto, --skip, e --force-rerun son las opcions exclusivas.",
"migrations_failed_to_load_migration": "Cargament impossible de la migracion {id} : {error}",
- "migrations_already_ran": "Aquelas migracions s’executèron ja : {ids}"
+ "migrations_already_ran": "Aquelas migracions s’executèron ja : {ids}",
+ "diagnosis_basesystem_ynh_main_version": "Lo servidor fonciona amb YunoHost {main_version} ({repo})",
+ "migrations_dependencies_not_satisfied": "Executatz aquestas migracions : « {dependencies_id} », abans la migracion {id}.",
+ "migrations_no_such_migration": "I a pas cap de migracion apelada « {id} »",
+ "migrations_not_pending_cant_skip": "Aquestas migracions son pas en espèra, las podètz pas doncas ignorar : {ids}",
+ "app_action_broke_system": "Aquesta accion sembla aver copat de servicis importants : {services}",
+ "diagnosis_display_tip_web": "Podètz anar a la seccion Diagnostic (dins l’ecran d’acuèlh) per veire los problèmas trobats.",
+ "diagnosis_ip_no_ipv6": "Lo servidor a pas d’adreça IPv5 activa.",
+ "diagnosis_ip_not_connected_at_all": "Lo servidor sembla pas connectat a Internet ?!",
+ "diagnosis_security_all_good": "Cap de vulnerabilitat de seguretat critica pas trobada.",
+ "diagnosis_description_regenconf": "Configuracion sistèma",
+ "diagnosis_http_ok": "Lo domeni {domain} accessible de l’exterior.",
+ "app_full_domain_unavailable": "Aquesta aplicacion a d’èsser installada sul seu pròpri domeni, mas i a d’autras aplicacions installadas sus aqueste domeni « {domain} ». Podètz utilizar allòc un josdomeni dedicat a aquesta aplicacion.",
+ "diagnosis_dns_bad_conf": "Configuracion DNS incorrècta o inexistenta pel domeni {domain} (categoria {category})",
+ "diagnosis_ram_verylow": "Lo sistèma a solament {available} ({available_percent}%) de memòria RAM disponibla ! (d’un total de {total})",
+ "diagnosis_ram_ok": "Lo sistèma a encara {available} ({available_percent}%) de memòria RAM disponibla d’un total de {total}).",
+ "permission_already_allowed": "Lo grop « {group} » a ja la permission « {permission} » activada",
+ "permission_already_disallowed": "Lo grop « {group} » a ja la permission « {permission} » desactivada",
+ "permission_cannot_remove_main": "La supression d’una permission màger es pas autorizada",
+ "log_permission_url": "Actualizacion de l’URL ligada a la permission « {} »",
+ "app_install_failed": "Installacion impossibla de {app} : {error}",
+ "app_install_script_failed": "Una error s’es producha en installar lo script de l’aplicacion",
+ "migration_0011_failed_to_remove_stale_object": "Supression impossibla d’un objècte obsolèt {dn} : {error}",
+ "apps_already_up_to_date": "Totas las aplicacions son ja al jorn",
+ "app_remove_after_failed_install": "Supression de l’aplicacion aprèp fracàs de l’installacion…",
+ "group_already_exist": "Lo grop {group} existís ja",
+ "group_already_exist_on_system": "Lo grop {group} existís ja dins lo sistèma de grops",
+ "group_user_not_in_group": "L’utilizaire {user} es pas dins lo grop {group}",
+ "log_user_permission_reset": "Restablir la permission « {} »",
+ "user_already_exists": "L’utilizaire {user} existís ja",
+ "diagnosis_basesystem_host": "Lo servidor fonciona amb Debian {debian_version}.",
+ "diagnosis_basesystem_kernel": "Lo servidor fonciona amb lo nuclèu Linuxl {kernel_version}",
+ "diagnosis_basesystem_ynh_single_version": "{package} version : {version} ({repo})",
+ "diagnosis_basesystem_ynh_inconsistent_versions": "Utilizatz de versions inconsistentas dels paquets de YunoHost… probablament a causa d'una actualizacion fracassada o parciala.",
+ "diagnosis_display_tip_cli": "Podètz executar « yunohost diagnosis show --issues » per mostrar las errors trobadas.",
+ "diagnosis_ignored_issues": "(+ {nb_ignored} problèma(es) ignorat(s))",
+ "diagnosis_everything_ok": "Tot sembla corrècte per {category} !",
+ "diagnosis_ip_connected_ipv4": "Lo servidor es connectat a Internet via IPv4 !",
+ "diagnosis_ip_no_ipv4": "Lo servidor a pas d’adreça IPv4 activa.",
+ "diagnosis_ip_connected_ipv6": "Lo servidor es connectat a Internet via IPv6 !",
+ "diagnosis_ip_dnsresolution_working": "La resolucion del nom de domeni fonciona !",
+ "diagnosis_dns_good_conf": "Bona configuracion DNS pel domeni {domain} (categoria {category})",
+ "diagnosis_failed_for_category": "Lo diagnostic a reüssit per la categoria « {category} » : {error}",
+ "diagnosis_cache_still_valid": "(Memòria cache totjorn valida pel diagnostic {category}. Cap d’autre diagnostic pel moment !)",
+ "diagnosis_found_errors": "{errors} errors importantas trobadas ligadas a {category} !",
+ "diagnosis_services_bad_status": "Lo servici {service} es {status} :(",
+ "diagnosis_swap_ok": "Lo sistèma a {total} d’escambi !",
+ "diagnosis_regenconf_allgood": "Totes los fichièrs de configuracion son confòrmes a la configuracion recomandada !",
+ "diagnosis_regenconf_manually_modified": "Lo fichièr de configuracion {file} foguèt modificat manualament.",
+ "diagnosis_regenconf_manually_modified_details": "Es probablament bon tan que sabètz çò que fasètz ;) !",
+ "diagnosis_security_vulnerable_to_meltdown": "Semblatz èsser vulnerable a la vulnerabilitat de seguretat critica de Meltdown",
+ "diagnosis_description_basesystem": "Sistèma de basa",
+ "diagnosis_description_ip": "Connectivitat Internet",
+ "diagnosis_description_dnsrecords": "Enregistraments DNS",
+ "diagnosis_description_services": "Verificacion d’estat de servicis",
+ "diagnosis_description_systemresources": "Resorgas sistèma",
+ "diagnosis_description_ports": "Exposicion dels pòrts",
+ "diagnosis_description_security": "Verificacion de seguretat",
+ "diagnosis_ports_unreachable": "Lo pòrt {port} es pas accessible de l’exterior.",
+ "diagnosis_ports_ok": "Lo pòrt {port} es accessible de l’exterior.",
+ "diagnosis_http_unreachable": "Lo domeni {domain} es pas accessible via HTTP de l’exterior.",
+ "diagnosis_unknown_categories": "La categorias seguentas son desconegudas : {categories}",
+ "diagnosis_ram_low": "Lo sistèma a {available} ({available_percent}%) de memòria RAM disponibla d’un total de {total}). Atencion.",
+ "diagnosis_regenconf_manually_modified_debian": "Lo fichier de configuracion {file} foguèt modificat manualament respècte al fichièr per defaut de Debian.",
+ "log_permission_create": "Crear la permission « {} »",
+ "log_permission_delete": "Suprimir la permission « {} »",
+ "log_user_group_create": "Crear lo grop « {} »",
+ "log_user_permission_update": "Actualizacion dels accèsses per la permission « {} »",
+ "operation_interrupted": "L’operacion es estada interrompuda manualament ?",
+ "group_cannot_be_deleted": "Lo grop « {group} » pòt pas èsser suprimit manualament.",
+ "diagnosis_found_warnings": "Trobat {warnings} element(s) que se poirián melhorar per {category}.",
+ "diagnosis_dns_missing_record": "Segon la configuracion DNS recomandada, vos calriá ajustar un enregistrament DNS\ntipe: {type}\nnom: {name}\nvalor: {value}",
+ "diagnosis_dns_discrepancy": "Segon la configuracion DNS recomandada, la valor per l’enregistrament DNS\ntipe: {type}\nnom: {name}\ndeuriá èsser: {current}\nallòc de: {value}",
+ "diagnosis_regenconf_manually_modified_debian_details": "Es pas problematic, mas car téner d’agacher...",
+ "diagnosis_ports_could_not_diagnose": "Impossible de diagnosticar se los pòrts son accessibles de l’exterior.",
+ "diagnosis_ports_could_not_diagnose_details": "Error : {error}",
+ "diagnosis_http_could_not_diagnose": "Impossible de diagnosticar se lo domeni es accessible de l’exterior.",
+ "diagnosis_http_could_not_diagnose_details": "Error : {error}",
+ "apps_catalog_updating": "Actualizacion del catalòg d’aplicacion…",
+ "apps_catalog_failed_to_download": "Telecargament impossible del catalòg d’aplicacions {apps_catalog} : {error}",
+ "apps_catalog_obsolete_cache": "La memòria cache del catalòg d’aplicacion es voida o obsolèta.",
+ "apps_catalog_update_success": "Lo catalòg d’aplicacions es a jorn !",
+ "diagnosis_mail_ougoing_port_25_ok": "Lo pòrt de sortida 25 es pas blocat e lo corrièr electronic pòt partir als autres servidors.",
+ "diagnosis_description_mail": "Corrièl",
+ "app_upgrade_script_failed": "Una error s’es producha pendent l’execucion de l’script de mesa a nivèl de l’aplicacion",
+ "diagnosis_cant_run_because_of_dep": "Execucion impossibla del diagnostic per {category} mentre que i a de problèmas importants ligats amb {dep}.",
+ "diagnosis_found_errors_and_warnings": "Avèm trobat {errors} problèma(s) important(s) (e {warnings} avis(es)) ligats a {category} !",
+ "diagnosis_failed": "Recuperacion impossibla dels resultats del diagnostic per la categoria « {category} » : {error}",
+ "diagnosis_ip_broken_dnsresolution": "La resolucion del nom de domeni es copada per una rason… Lo parafuòc bloca las requèstas DNS ?",
+ "diagnosis_no_cache": "I a pas encara de diagnostic de cache per la categoria « {category} »",
+ "apps_catalog_init_success": "Sistèma de catalòg d’aplicacion iniciat !",
+ "diagnosis_services_running": "Lo servici {service} es lançat !",
+ "diagnosis_services_conf_broken": "La configuracion es copada pel servici {service} !",
+ "diagnosis_ports_needed_by": "Es necessari qu’aqueste pòrt siá accessible pel servici {service}",
+ "diagnosis_diskusage_low": "Lo lòc d’emmagazinatge {mountpoint} (sul periferic {device}) a solament {free} ({free_percent}%). Siatz prudent.",
+ "migration_description_0014_remove_app_status_json": "Suprimir los fichièrs d’aplicacion status.json eretats",
+ "dyndns_provider_unreachable": "Impossible d’atenher lo provesidor Dyndns : siá vòstre YunoHost es pas corrèctament connectat a Internet siá lo servidor dynette es copat.",
+ "diagnosis_services_bad_status_tip": "Podètz ensajar de reaviar lo servici, e se non fonciona pas, podètz agachar los jornals en utilizant « yunohost service log {service} » o via la seccion « Servicis » de pas la pagina web d’administracion.",
+ "diagnosis_http_connection_error": "Error de connexion : connexion impossibla al domeni demandat, benlèu qu’es pas accessible.",
+ "group_user_already_in_group": "L’utilizaire {user} es ja dins lo grop « {group} »",
+ "diagnosis_ip_broken_resolvconf": "La resolucion del nom de domeni sembla copada sul servidor, poiriá èsser ligada al fait que /etc/resolv.conf manda pas a 127.0.0.1.",
+ "diagnosis_ip_weird_resolvconf": "La resolucion del nom de domeni sembla foncionar, mas siatz prudent en utilizant un fichièr /etc/resolv.con personalizat.",
+ "diagnosis_diskusage_verylow": "Lo lòc d’emmagazinatge {mountpoint} (sul periferic {device}) a solament {free} ({free_percent}%). Deuriatz considerar de liberar un pauc d’espaci.",
+ "global_settings_setting_pop3_enabled": "Activar lo protocòl POP3 pel servidor de corrièr",
+ "diagnosis_diskusage_ok": "Lo lòc d’emmagazinatge {mountpoint} (sul periferic {device}) a encara {free} ({free_percent}%) de liure !",
+ "diagnosis_swap_none": "Lo sistèma a pas cap de memòria d’escambi. Auriatz de considerar d’ajustar almens {recommended} d’escambi per evitar las situacions ont lo sistèma manca de memòria.",
+ "diagnosis_swap_notsomuch": "Lo sistèma a solament {total} de memòria d’escambi. Auriatz de considerar d’ajustar almens {recommended} d’escambi per evitar las situacions ont lo sistèma manca de memòria."
}
diff --git a/locales/pl.json b/locales/pl.json
index 0967ef424..7ff9fbcd2 100644
--- a/locales/pl.json
+++ b/locales/pl.json
@@ -1 +1,3 @@
-{}
+{
+ "password_too_simple_1": "Hasło musi mieć co najmniej 8 znaków"
+}
\ No newline at end of file
diff --git a/locales/pt.json b/locales/pt.json
index 80a0d5ddd..bd5ba3bc6 100644
--- a/locales/pt.json
+++ b/locales/pt.json
@@ -1,54 +1,39 @@
{
"action_invalid": "Acção Inválida '{action:s}'",
"admin_password": "Senha de administração",
- "admin_password_change_failed": "Não foi possível alterar a senha",
- "admin_password_changed": "A palavra-passe de administração foi alterada com sucesso",
+ "admin_password_change_failed": "Não é possível alterar a senha",
+ "admin_password_changed": "A senha da administração foi alterada",
"app_already_installed": "{app:s} já está instalada",
"app_extraction_failed": "Não foi possível extrair os ficheiros para instalação",
"app_id_invalid": "A ID da aplicação é inválida",
"app_install_files_invalid": "Ficheiros para instalação corrompidos",
- "app_location_already_used": "A aplicação {app} Já está instalada nesta localização ({path})",
- "app_location_install_failed": "Não é possível instalar a aplicação neste diretório porque está em conflito com a aplicação '{other_app}', que já está instalada no diretório '{other_path}'",
"app_manifest_invalid": "Manifesto da aplicação inválido: {error}",
- "app_no_upgrade": "Não existem aplicações para atualizar",
"app_not_installed": "{app:s} não está instalada",
- "app_recent_version_required": "{:s} requer uma versão mais recente da moulinette",
"app_removed": "{app:s} removida com êxito",
"app_sources_fetch_failed": "Incapaz obter os ficheiros fonte",
"app_unknown": "Aplicação desconhecida",
"app_upgrade_failed": "Não foi possível atualizar {app:s}",
"app_upgraded": "{app:s} atualizada com sucesso",
- "appslist_fetched": "A lista de aplicações, {appslist:s}, foi trazida com sucesso",
- "appslist_removed": "A Lista de aplicações {appslist:s} foi removida",
- "appslist_retrieve_error": "Não foi possível obter a lista de aplicações remotas {appslist:s}: {error:s}",
- "appslist_unknown": "Desconhece-se a lista de aplicaçoes {appslist:s}.",
- "ask_current_admin_password": "Senha atual da administração",
"ask_email": "Endereço de Email",
"ask_firstname": "Primeiro nome",
"ask_lastname": "Último nome",
- "ask_list_to_remove": "Lista para remover",
"ask_main_domain": "Domínio principal",
"ask_new_admin_password": "Nova senha de administração",
"ask_password": "Senha",
"backup_created": "Backup completo",
- "backup_creating_archive": "A criar ficheiro de backup...",
"backup_invalid_archive": "Arquivo de backup inválido",
"backup_output_directory_not_empty": "A pasta de destino não se encontra vazia",
"custom_app_url_required": "Deve fornecer um link para atualizar a sua aplicação personalizada {app:s}",
- "custom_appslist_name_required": "Deve fornecer um nome para a sua lista de aplicações personalizada",
"domain_cert_gen_failed": "Não foi possível gerar o certificado",
"domain_created": "Domínio criado com êxito",
"domain_creation_failed": "Não foi possível criar o domínio",
"domain_deleted": "Domínio removido com êxito",
"domain_deletion_failed": "Não foi possível eliminar o domínio",
"domain_dyndns_already_subscribed": "Já subscreveu um domínio DynDNS",
- "domain_dyndns_invalid": "Domínio inválido para ser utilizado com DynDNS",
"domain_dyndns_root_unknown": "Domínio root (administrador) DynDNS desconhecido",
"domain_exists": "O domínio já existe",
"domain_uninstall_app_first": "Existem uma ou mais aplicações instaladas neste domínio. Por favor desinstale-as antes de proceder com a remoção do domínio.",
"domain_unknown": "Domínio desconhecido",
- "domain_zone_exists": "Ficheiro para zona DMZ já existe",
- "domain_zone_not_found": "Ficheiro para zona DMZ não encontrado no domínio {:s}",
"done": "Concluído.",
"downloading": "Transferência em curso...",
"dyndns_cron_installed": "Gestor de tarefas cron DynDNS instalado com êxito",
@@ -64,44 +49,22 @@
"extracting": "Extração em curso...",
"field_invalid": "Campo inválido '{:s}'",
"firewall_reloaded": "Firewall recarregada com êxito",
- "hook_argument_missing": "Argumento em falta '{:s}'",
- "hook_choice_invalid": "Escolha inválida '{:s}'",
"installation_complete": "Instalação concluída",
"installation_failed": "A instalação falhou",
"iptables_unavailable": "Não pode alterar aqui a iptables. Ou o seu kernel não o suporta ou está num espaço reservado.",
"ldap_initialized": "LDAP inicializada com êxito",
- "license_undefined": "indefinido",
"mail_alias_remove_failed": "Não foi possível remover a etiqueta de correio '{mail:s}'",
- "mail_domain_unknown": "Domínio de endereço de correio desconhecido '{domain:s}'",
+ "mail_domain_unknown": "Domínio de endereço de correio '{domain:s}' inválido. Por favor, usa um domínio administrado per esse servidor.",
"mail_forward_remove_failed": "Não foi possível remover o reencaminhamento de correio '{mail:s}'",
- "maindomain_change_failed": "Incapaz alterar o domínio raiz",
- "maindomain_changed": "Domínio raiz alterado com êxito",
- "monitor_disabled": "Monitorização do servidor parada com êxito",
- "monitor_enabled": "Monitorização do servidor ativada com êxito",
- "monitor_glances_con_failed": "Não foi possível ligar ao servidor Glances",
- "monitor_not_enabled": "A monitorização do servidor não está ativa",
- "monitor_period_invalid": "Período de tempo inválido",
- "monitor_stats_file_not_found": "Ficheiro de estatísticas não encontrado",
- "monitor_stats_no_update": "Não existem estatísticas de monitorização para atualizar",
- "monitor_stats_period_unavailable": "Não existem estatísticas disponíveis para este período",
- "mountpoint_unknown": "Ponto de montagem desconhecido",
- "mysql_db_creation_failed": "Criação da base de dados MySQL falhou",
- "mysql_db_init_failed": "Inicialização da base de dados MySQL falhou",
- "mysql_db_initialized": "Base de dados MySQL iniciada com êxito",
- "new_domain_required": "Deve escrever um novo domínio principal",
- "no_appslist_found": "Não foi encontrada a lista de aplicações",
+ "main_domain_change_failed": "Incapaz alterar o domínio raiz",
+ "main_domain_changed": "Domínio raiz alterado com êxito",
"no_internet_connection": "O servidor não está ligado à Internet",
- "packages_no_upgrade": "Não existem pacotes para atualizar",
- "packages_upgrade_critical_later": "Os pacotes críticos ({packages:s}) serão atualizados depois",
"packages_upgrade_failed": "Não foi possível atualizar todos os pacotes",
- "path_removal_failed": "Incapaz remover o caminho {:s}",
"pattern_domain": "Deve ser um nome de domínio válido (p.e. meu-dominio.org)",
"pattern_email": "Deve ser um endereço de correio válido (p.e. alguem@dominio.org)",
"pattern_firstname": "Deve ser um primeiro nome válido",
"pattern_lastname": "Deve ser um último nome válido",
- "pattern_listname": "Apenas são permitidos caracteres alfanuméricos e travessões",
"pattern_password": "Deve ter no mínimo 3 caracteres",
- "pattern_port": "Deve ser um número de porta válido (entre 0-65535)",
"pattern_username": "Devem apenas ser carácteres minúsculos alfanuméricos e subtraços",
"restore_confirm_yunohost_installed": "Quer mesmo restaurar um sistema já instalado? [{answers:s}]",
"service_add_failed": "Incapaz adicionar serviço '{service:s}'",
@@ -113,12 +76,10 @@
"service_disabled": "O serviço '{service:s}' foi desativado com êxito",
"service_enable_failed": "Incapaz de ativar o serviço '{service:s}'",
"service_enabled": "Serviço '{service:s}' ativado com êxito",
- "service_no_log": "Não existem registos para mostrar do serviço '{service:s}'",
"service_remove_failed": "Incapaz de remover o serviço '{service:s}'",
"service_removed": "Serviço eliminado com êxito",
"service_start_failed": "Não foi possível iniciar o serviço '{service:s}'",
"service_started": "O serviço '{service:s}' foi iniciado com êxito",
- "service_status_failed": "Incapaz determinar o estado do serviço '{service:s}'",
"service_stop_failed": "Incapaz parar o serviço '{service:s}'",
"service_stopped": "O serviço '{service:s}' foi parado com êxito",
"service_unknown": "Serviço desconhecido '{service:s}'",
@@ -127,8 +88,6 @@
"system_upgraded": "Sistema atualizado com êxito",
"system_username_exists": "O utilizador já existe no registo do sistema",
"unexpected_error": "Ocorreu um erro inesperado",
- "unit_unknown": "Unidade desconhecida '{unit:s}'",
- "update_cache_failed": "Não foi possível atualizar os cabeçalhos APT",
"updating_apt_cache": "A atualizar a lista de pacotes disponíveis...",
"upgrade_complete": "Atualização completa",
"upgrading_packages": "Atualização de pacotes em curso...",
@@ -136,7 +95,6 @@
"user_creation_failed": "Não foi possível criar o utilizador",
"user_deleted": "Utilizador eliminado com êxito",
"user_deletion_failed": "Incapaz eliminar o utilizador",
- "user_info_failed": "Incapaz obter informações sobre o utilizador",
"user_unknown": "Utilizador desconhecido",
"user_update_failed": "Não foi possível atualizar o utilizador",
"user_updated": "Utilizador atualizado com êxito",
@@ -145,21 +103,18 @@
"yunohost_configured": "YunoHost configurada com êxito",
"yunohost_installing": "A instalar a YunoHost...",
"yunohost_not_installed": "YunoHost ainda não está corretamente configurado. Por favor execute as 'ferramentas pós-instalação yunohost'.",
- "app_incompatible": "A aplicação {app} é incompatível com a sua versão de Yunohost",
"app_not_correctly_installed": "{app:s} parece não estar corretamente instalada",
"app_not_properly_removed": "{app:s} não foi corretamente removido",
"app_requirements_checking": "Verificando os pacotes necessários para {app}...",
"app_unsupported_remote_type": "A aplicação não possui suporte ao tipo remoto utilizado",
"backup_archive_app_not_found": "A aplicação '{app:s}' não foi encontrada no arquivo de backup",
"backup_archive_broken_link": "Impossível acessar o arquivo de backup (link quebrado ao {path:s})",
- "backup_archive_hook_not_exec": "O gancho '{hook:s}' não foi executado neste backup",
"backup_archive_name_exists": "O nome do arquivo de backup já existe",
"backup_archive_open_failed": "Não é possível abrir o arquivo de backup",
"backup_cleaning_failed": "Não é possível limpar a pasta temporária de backups",
"backup_creation_failed": "A criação do backup falhou",
"backup_delete_error": "Impossível apagar '{path:s}'",
"backup_deleted": "O backup foi suprimido",
- "backup_extracting_archive": "Extraindo arquivo de backup...",
"backup_hook_unknown": "Gancho de backup '{hook:s}' desconhecido",
"backup_nothings_done": "Não há nada para guardar",
"backup_output_directory_forbidden": "Diretório de saída proibido. Os backups não podem ser criados em /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var ou /home/yunohost.backup/archives subpastas",
@@ -169,29 +124,20 @@
"app_argument_invalid": "Valor inválido de argumento '{name:s}': {error:s}",
"app_argument_required": "O argumento '{name:s}' é obrigatório",
"app_change_url_failed_nginx_reload": "Falha ao reiniciar o nginx. Aqui está o retorno de 'nginx -t':\n{nginx_errors:s}",
- "app_change_no_change_url_script": "A aplicação {app_name:s} ainda não permite mudança da URL, talvez seja necessário atualiza-la.",
"app_location_unavailable": "Esta url não está disponível ou está em conflito com outra aplicação já instalada",
- "app_package_need_update": "O pacote da aplicação {app} precisa ser atualizado para aderir as mudanças do YunoHost",
- "app_requirements_failed": "Não foi possível atender aos requisitos da aplicação {app}: {error}",
"app_upgrade_app_name": "Atualizando aplicação {app}…",
"app_upgrade_some_app_failed": "Não foi possível atualizar algumas aplicações",
- "appslist_corrupted_json": "Falha ao carregar a lista de aplicações. O arquivo {filename:s} aparenta estar corrompido.",
- "appslist_migrating": "Migando lista de aplicações {appslist:s}…",
- "appslist_name_already_tracked": "Já existe uma lista de aplicações registrada com o nome {name:s}.",
- "appslist_retrieve_bad_format": "O arquivo recuperado para a lista de aplicações {appslist:s} é invalido",
- "appslist_url_already_tracked": "Já existe uma lista de aplicações registrada com a url {url:s}.",
- "ask_path": "Caminho",
"backup_abstract_method": "Este metodo de backup ainda não foi implementado",
- "backup_action_required": "Deve-se especificar algo a salvar",
"backup_app_failed": "Não foi possível fazer o backup dos aplicativos '{app:s}'",
"backup_applying_method_custom": "Chamando o metodo personalizado de backup '{method:s}'…",
"backup_applying_method_tar": "Criando o arquivo tar de backup…",
- "backup_archive_mount_failed": "Falha ao montar o arquivo de backup",
"backup_archive_name_unknown": "Desconhece-se o arquivo local de backup de nome '{name:s}'",
"backup_archive_system_part_not_available": "A seção do sistema '{part:s}' está indisponivel neste backup",
"backup_ask_for_copying_if_needed": "Alguns arquivos não consiguiram ser preparados para backup utilizando o metodo que não gasta espaço de disco temporariamente. Para realizar o backup {size:s}MB precisam ser usados temporariamente. Você concorda?",
"backup_borg_not_implemented": "O método de backup Borg ainda não foi implementado.",
"backup_cant_mount_uncompress_archive": "Não foi possível montar em modo leitura o diretorio de arquivos não comprimido",
"backup_copying_to_organize_the_archive": "Copiando {size:s}MB para organizar o arquivo",
- "app_change_url_identical_domains": "O antigo e o novo domínio / url_path são idênticos ('{domain:s}{path:s}'), nada para fazer."
-}
+ "app_change_url_identical_domains": "O antigo e o novo domínio / url_path são idênticos ('{domain:s}{path:s}'), nada para fazer.",
+ "password_too_simple_1": "A senha precisa ter pelo menos 8 caracteres",
+ "admin_password_too_long": "Escolha uma senha que contenha menos de 127 caracteres"
+}
\ No newline at end of file
diff --git a/locales/ru.json b/locales/ru.json
index 306a8763a..afe8e06f0 100644
--- a/locales/ru.json
+++ b/locales/ru.json
@@ -9,23 +9,17 @@
"app_argument_invalid": "Недопустимое значение аргумента '{name:s}': {error:s}'",
"app_already_up_to_date": "{app:s} уже обновлено",
"app_argument_required": "Аргумент '{name:s}' необходим",
- "app_change_no_change_url_script": "Приложение {app_name:s} не поддерживает изменение URL, вы должны обновить его.",
"app_change_url_identical_domains": "Старый и новый domain/url_path идентичны ('{domain:s}{path:s}'), ничего делать не надо.",
"app_change_url_no_script": "Приложение '{app_name:s}' не поддерживает изменение url. Наверное, вам нужно обновить приложение.",
"app_change_url_success": "Успешно изменён {app:s} url на {domain:s}{path:s}",
"app_extraction_failed": "Невозможно извлечь файлы для инсталляции",
"app_id_invalid": "Неправильный id приложения",
- "app_incompatible": "Приложение {app} несовместимо с вашей версией YonoHost",
"app_install_files_invalid": "Неправильные файлы инсталляции",
- "app_location_already_used": "Приложение '{app}' уже установлено по этому адресу ({path})",
- "app_location_install_failed": "Невозможно установить приложение в это место, потому что оно конфликтует с приложением, '{other_app}' установленном на '{other_path}'",
"app_location_unavailable": "Этот url отсутствует или конфликтует с уже установленным приложением или приложениями: {apps:s}",
"app_manifest_invalid": "Недопустимый манифест приложения: {error}",
- "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} должен быть обновлён в соответствии с изменениями YonoHost",
"app_removed": "{app:s} удалено",
"app_requirements_checking": "Проверяю необходимые пакеты для {app}...",
"app_sources_fetch_failed": "Невозможно получить исходные файлы",
@@ -34,13 +28,6 @@
"app_upgrade_failed": "Невозможно обновить {app:s}",
"app_upgrade_some_app_failed": "Невозможно обновить некоторые приложения",
"app_upgraded": "{app:s} обновлено",
- "appslist_corrupted_json": "Не могу загрузить список приложений. Кажется, {filename:s} поврежден.",
- "appslist_fetched": "Был выбран список приложений {appslist:s}",
- "appslist_name_already_tracked": "Уже есть зарегистрированный список приложений по имени {name:s}.",
- "appslist_removed": "Список приложений {appslist:s} удалён",
- "appslist_retrieve_bad_format": "Неверный файл списка приложений{appslist:s}",
- "appslist_retrieve_error": "Невозможно получить список удаленных приложений {appslist:s}: {error:s}",
- "appslist_unknown": "Список приложений {appslist:s} неизвестен.",
- "appslist_url_already_tracked": "Это уже зарегистрированный список приложений с url{url:s}.",
- "installation_complete": "Установка завершена"
-}
+ "installation_complete": "Установка завершена",
+ "password_too_simple_1": "Пароль должен быть не менее 8 символов"
+}
\ No newline at end of file
diff --git a/locales/sv.json b/locales/sv.json
index 0967ef424..26162419e 100644
--- a/locales/sv.json
+++ b/locales/sv.json
@@ -1 +1,11 @@
-{}
+{
+ "password_too_simple_1": "Lösenordet måste bestå av minst åtta tecken",
+ "app_action_broke_system": "Åtgärden verkar ha fått följande viktiga tjänster att haverera: {services}",
+ "already_up_to_date": "Ingenting att göra. Allt är redan uppdaterat.",
+ "admin_password": "Administratörslösenord",
+ "admin_password_too_long": "Välj gärna ett lösenord som inte innehåller fler än 127 tecken",
+ "admin_password_change_failed": "Kan inte byta lösenord",
+ "action_invalid": "Ej tillåten åtgärd '{action:s}'",
+ "admin_password_changed": "Administratörskontots lösenord ändrades",
+ "aborting": "Avbryter."
+}
\ No newline at end of file
diff --git a/locales/tr.json b/locales/tr.json
index 0967ef424..6c881eec7 100644
--- a/locales/tr.json
+++ b/locales/tr.json
@@ -1 +1,3 @@
-{}
+{
+ "password_too_simple_1": "Şifre en az 8 karakter uzunluğunda olmalı"
+}
\ No newline at end of file
diff --git a/locales/zh_Hans.json b/locales/zh_Hans.json
index 0967ef424..dee71a1d4 100644
--- a/locales/zh_Hans.json
+++ b/locales/zh_Hans.json
@@ -1 +1,3 @@
-{}
+{
+ "password_too_simple_1": "密码长度至少为8个字符"
+}
\ No newline at end of file
diff --git a/src/yunohost/app.py b/src/yunohost/app.py
index 5a51e57bb..25f856c10 100644
--- a/src/yunohost/app.py
+++ b/src/yunohost/app.py
@@ -33,15 +33,14 @@ import re
import urlparse
import subprocess
import glob
-import pwd
-import grp
import urllib
from collections import OrderedDict
from datetime import datetime
from moulinette import msignals, m18n, msettings
from moulinette.utils.log import getActionLogger
-from moulinette.utils.filesystem import read_json, read_toml
+from moulinette.utils.network import download_json
+from moulinette.utils.filesystem import read_file, read_json, read_toml, read_yaml, write_to_file, write_to_json, write_to_yaml, chmod, chown, mkdir
from yunohost.service import service_log, service_status, _run_service_command
from yunohost.utils import packages
@@ -50,12 +49,16 @@ from yunohost.log import is_unit_operation, OperationLogger
logger = getActionLogger('yunohost.app')
-REPO_PATH = '/var/cache/yunohost/repo'
APPS_PATH = '/usr/share/yunohost/apps'
APPS_SETTING_PATH = '/etc/yunohost/apps/'
INSTALL_TMP = '/var/cache/yunohost'
APP_TMP_FOLDER = INSTALL_TMP + '/from_file'
-APPSLISTS_JSON = '/etc/yunohost/appslists.json'
+
+APPS_CATALOG_CACHE = '/var/cache/yunohost/repo'
+APPS_CATALOG_CONF = '/etc/yunohost/apps_catalog.yml'
+APPS_CATALOG_CRON_PATH = "/etc/cron.daily/yunohost-fetch-apps-catalog"
+APPS_CATALOG_API_VERSION = 2
+APPS_CATALOG_DEFAULT_URL = "https://app.yunohost.org/default"
re_github_repo = re.compile(
r'^(http[s]?://|git@)github.com[/:]'
@@ -68,331 +71,133 @@ re_app_instance_name = re.compile(
)
-def app_listlists():
+def app_catalog(full=False, with_categories=False):
"""
- List fetched lists
-
+ Return a dict of apps available to installation from Yunohost's app catalog
"""
- # Migrate appslist system if needed
- # XXX move to a migration when those are implemented
- if _using_legacy_appslist_system():
- _migrate_appslist_system()
+ # Get app list from catalog cache
+ catalog = _load_apps_catalog()
+ installed_apps = set(_installed_apps())
- # Get the list
- appslist_list = _read_appslist_list()
+ # Trim info for apps if not using --full
+ for app, infos in catalog["apps"].items():
+ infos["installed"] = app in installed_apps
- # Convert 'lastUpdate' timestamp to datetime
- for name, infos in appslist_list.items():
- if infos["lastUpdate"] is None:
- infos["lastUpdate"] = 0
- infos["lastUpdate"] = datetime.utcfromtimestamp(infos["lastUpdate"])
+ infos["manifest"]["description"] = _value_for_locale(infos['manifest']['description'])
- return appslist_list
+ if not full:
+ catalog["apps"][app] = {
+ "description": infos['manifest']['description'],
+ "level": infos["level"],
+ }
+ # Trim info for categories if not using --full
+ for category in catalog["categories"]:
+ category["title"] = _value_for_locale(category["title"])
+ category["description"] = _value_for_locale(category["description"])
+ for subtags in category.get("subtags", []):
+ subtags["title"] = _value_for_locale(subtags["title"])
-def app_fetchlist(url=None, name=None):
- """
- Fetch application list(s) from app server. By default, fetch all lists.
+ if not full:
+ catalog["categories"] = [{"id": c["id"],
+ "description": c["description"]}
+ for c in catalog["categories"]]
- Keyword argument:
- name -- Name of the list
- url -- URL of remote JSON list
- """
- if url and not url.endswith(".json"):
- raise YunohostError("This is not a valid application list url. It should end with .json.")
-
- # If needed, create folder where actual appslists are stored
- if not os.path.exists(REPO_PATH):
- os.makedirs(REPO_PATH)
-
- # Migrate appslist system if needed
- # XXX move that to a migration once they are finished
- if _using_legacy_appslist_system():
- _migrate_appslist_system()
-
- # Read the list of appslist...
- appslists = _read_appslist_list()
-
- # Determine the list of appslist to be fetched
- appslists_to_be_fetched = []
-
- # If a url and and a name is given, try to register new list,
- # the fetch only this list
- if url is not None:
- if name:
- operation_logger = OperationLogger('app_fetchlist')
- operation_logger.start()
- _register_new_appslist(url, name)
- # Refresh the appslists dict
- appslists = _read_appslist_list()
- appslists_to_be_fetched = [name]
- operation_logger.success()
- else:
- raise YunohostError('custom_appslist_name_required')
-
- # If a name is given, look for an appslist with that name and fetch it
- elif name is not None:
- if name not in appslists.keys():
- raise YunohostError('appslist_unknown', appslist=name)
- else:
- appslists_to_be_fetched = [name]
-
- # Otherwise, fetch all lists
+ if not with_categories:
+ return {"apps": catalog["apps"]}
else:
- appslists_to_be_fetched = appslists.keys()
+ return {"apps": catalog["apps"], "categories": catalog["categories"]}
- import requests # lazy loading this module for performance reasons
- # Fetch all appslists to be fetched
- for name in appslists_to_be_fetched:
- url = appslists[name]["url"]
- logger.debug("Attempting to fetch list %s at %s" % (name, url))
+# Old legacy function...
+def app_fetchlist():
+ logger.warning("'yunohost app fetchlist' is deprecated. Please use 'yunohost tools update --apps' instead")
+ from yunohost.tools import tools_update
+ tools_update(apps=True)
- # Download file
- try:
- appslist_request = requests.get(url, timeout=30)
- except requests.exceptions.SSLError:
- logger.error(m18n.n('appslist_retrieve_error',
- appslist=name,
- error="SSL connection error"))
+
+def app_list(full=False, installed=False, filter=None):
+ """
+ List installed apps
+ """
+
+ # Old legacy argument ... app_list was a combination of app_list and
+ # app_catalog before 3.8 ...
+ if installed:
+ logger.warning("Argument --installed ain't needed anymore when using 'yunohost app list'. It directly returns the list of installed apps..")
+
+ # Filter is a deprecated option...
+ if filter:
+ logger.warning("Using -f $appname in 'yunohost app list' is deprecated. Just use 'yunohost app list | grep -q 'id: $appname' to check a specific app is installed")
+
+ out = []
+ for app_id in sorted(_installed_apps()):
+
+ if filter and not app_id.startswith(filter):
continue
+
+ try:
+ app_info_dict = app_info(app_id, full=full)
except Exception as e:
- logger.error(m18n.n('appslist_retrieve_error',
- appslist=name,
- error=str(e)))
- continue
- if appslist_request.status_code != 200:
- logger.error(m18n.n('appslist_retrieve_error',
- appslist=name,
- error="Server returned code %s " %
- str(appslist_request.status_code)))
+ logger.error("Failed to read info for %s : %s" % (app_id, e))
continue
+ app_info_dict["id"] = app_id
+ out.append(app_info_dict)
- # Validate app list format
- # TODO / Possible improvement : better validation for app list (check
- # that json fields actually look like an app list and not any json
- # file)
- appslist = appslist_request.text
- try:
- json.loads(appslist)
- except ValueError as e:
- logger.error(m18n.n('appslist_retrieve_bad_format',
- appslist=name))
- continue
-
- # Write app list to file
- list_file = '%s/%s.json' % (REPO_PATH, name)
- try:
- with open(list_file, "w") as f:
- f.write(appslist)
- except Exception as e:
- raise YunohostError("Error while writing appslist %s: %s" % (name, str(e)), raw_msg=True)
-
- now = int(time.time())
- appslists[name]["lastUpdate"] = now
-
- logger.success(m18n.n('appslist_fetched', appslist=name))
-
- # Write updated list of appslist
- _write_appslist_list(appslists)
+ return {'apps': out}
-@is_unit_operation()
-def app_removelist(operation_logger, name):
+def app_info(app, full=False):
"""
- Remove list from the repositories
-
- Keyword argument:
- name -- Name of the list to remove
-
- """
- appslists = _read_appslist_list()
-
- # Make sure we know this appslist
- if name not in appslists.keys():
- raise YunohostError('appslist_unknown', appslist=name)
-
- operation_logger.start()
-
- # Remove json
- json_path = '%s/%s.json' % (REPO_PATH, name)
- if os.path.exists(json_path):
- os.remove(json_path)
-
- # Forget about this appslist
- del appslists[name]
- _write_appslist_list(appslists)
-
- logger.success(m18n.n('appslist_removed', appslist=name))
-
-
-def app_list(filter=None, raw=False, installed=False, with_backup=False):
- """
- List apps
-
- Keyword argument:
- filter -- Name filter of app_id or app_name
- offset -- Starting number for app fetching
- limit -- Maximum number of app fetched
- raw -- Return the full app_dict
- installed -- Return only installed apps
- with_backup -- Return only apps with backup feature (force --installed filter)
-
- """
- installed = with_backup or installed
-
- app_dict = {}
- list_dict = {} if raw else []
-
- appslists = _read_appslist_list()
-
- for appslist in appslists.keys():
-
- json_path = "%s/%s.json" % (REPO_PATH, appslist)
-
- # If we don't have the json yet, try to fetch it
- if not os.path.exists(json_path):
- app_fetchlist(name=appslist)
-
- # If it now exist
- if os.path.exists(json_path):
- appslist_content = read_json(json_path)
- for app, info in appslist_content.items():
- if app not in app_dict:
- info['repository'] = appslist
- app_dict[app] = info
- else:
- logger.warning("Uh there's no data for applist '%s' ... (That should be just a temporary issue?)" % appslist)
-
- # Get app list from the app settings directory
- for app in os.listdir(APPS_SETTING_PATH):
- if app not in app_dict:
- # Handle multi-instance case like wordpress__2
- if '__' in app:
- original_app = app[:app.index('__')]
- if original_app in app_dict:
- app_dict[app] = app_dict[original_app]
- continue
- # FIXME : What if it's not !?!?
-
- manifest = _get_manifest_of_app(os.path.join(APPS_SETTING_PATH, app))
- app_dict[app] = {"manifest": manifest}
-
- app_dict[app]['repository'] = None
-
- # Sort app list
- sorted_app_list = sorted(app_dict.keys())
-
- for app_id in sorted_app_list:
-
- app_info_dict = app_dict[app_id]
-
- # Apply filter if there's one
- if (filter and
- (filter not in app_id) and
- (filter not in app_info_dict['manifest']['name'])):
- continue
-
- # Ignore non-installed app if user wants only installed apps
- app_installed = _is_installed(app_id)
- if installed and not app_installed:
- continue
-
- # Ignore apps which don't have backup/restore script if user wants
- # only apps with backup features
- if with_backup and (
- not os.path.isfile(APPS_SETTING_PATH + app_id + '/scripts/backup') or
- not os.path.isfile(APPS_SETTING_PATH + app_id + '/scripts/restore')
- ):
- continue
-
- if raw:
- app_info_dict['installed'] = app_installed
- if app_installed:
- app_info_dict['status'] = _get_app_status(app_id)
-
- # dirty: we used to have manifest containing multi_instance value in form of a string
- # but we've switched to bool, this line ensure retrocompatibility
- app_info_dict["manifest"]["multi_instance"] = is_true(app_info_dict["manifest"].get("multi_instance", False))
-
- list_dict[app_id] = app_info_dict
-
- else:
- label = None
- if app_installed:
- app_info_dict_raw = app_info(app=app_id, raw=True)
- label = app_info_dict_raw['settings']['label']
-
- list_dict.append({
- 'id': app_id,
- 'name': app_info_dict['manifest']['name'],
- 'label': label,
- 'description': _value_for_locale(app_info_dict['manifest']['description']),
- # FIXME: Temporarly allow undefined license
- 'license': app_info_dict['manifest'].get('license', m18n.n('license_undefined')),
- 'installed': app_installed
- })
-
- return {'apps': list_dict} if not raw else list_dict
-
-
-def app_info(app, show_status=False, raw=False):
- """
- Get app info
-
- Keyword argument:
- app -- Specific app ID
- show_status -- Show app installation status
- raw -- Return the full app_dict
-
+ Get info for a specific app
"""
if not _is_installed(app):
raise YunohostError('app_not_installed', app=app, all_apps=_get_all_installed_apps_id())
- app_setting_path = APPS_SETTING_PATH + app
+ local_manifest = _get_manifest_of_app(os.path.join(APPS_SETTING_PATH, app))
- if raw:
- ret = app_list(filter=app, raw=True)[app]
- ret['settings'] = _get_app_settings(app)
+ settings = _get_app_settings(app)
- # Determine upgradability
- # In case there is neither update_time nor install_time, we assume the app can/has to be upgraded
- local_update_time = ret['settings'].get('update_time', ret['settings'].get('install_time', 0))
-
- if 'lastUpdate' not in ret or 'git' not in ret:
- upgradable = "url_required"
- elif ret['lastUpdate'] > local_update_time:
- upgradable = "yes"
- else:
- upgradable = "no"
-
- ret['upgradable'] = upgradable
- ret['change_url'] = os.path.exists(os.path.join(app_setting_path, "scripts", "change_url"))
-
- manifest = _get_manifest_of_app(os.path.join(APPS_SETTING_PATH, app))
-
- ret['version'] = manifest.get('version', '-')
+ ret = {
+ 'description': _value_for_locale(local_manifest['description']),
+ 'name': local_manifest['name'],
+ 'version': local_manifest.get('version', '-'),
+ }
+ if not full:
return ret
- # Retrieve manifest and status
- manifest = _get_manifest_of_app(app_setting_path)
- status = _get_app_status(app, format_date=True)
+ ret["manifest"] = local_manifest
+ ret['settings'] = settings
- info = {
- 'name': manifest['name'],
- 'description': _value_for_locale(manifest['description']),
- # FIXME: Temporarly allow undefined license
- 'license': manifest.get('license', m18n.n('license_undefined')),
- # FIXME: Temporarly allow undefined version
- 'version': manifest.get('version', '-'),
- # TODO: Add more info
- }
- if show_status:
- info['status'] = status
- return info
+ absolute_app_name = app if "__" not in app else app[:app.index('__')] # idk this is the name of the app even for multiinstance apps (so wordpress__2 -> wordpress)
+ ret["from_catalog"] = _load_apps_catalog()["apps"].get(absolute_app_name, {})
+ ret['upgradable'] = _app_upgradable(ret)
+ ret['supports_change_url'] = os.path.exists(os.path.join(APPS_SETTING_PATH, app, "scripts", "change_url"))
+ ret['supports_backup_restore'] = (os.path.exists(os.path.join(APPS_SETTING_PATH, app, "scripts", "backup")) and
+ os.path.exists(os.path.join(APPS_SETTING_PATH, app, "scripts", "restore")))
+ ret['supports_multi_instance'] = is_true(local_manifest.get("multi_instance", False))
+ return ret
+
+
+def _app_upgradable(app_infos):
+
+ # Determine upgradability
+ # In case there is neither update_time nor install_time, we assume the app can/has to be upgraded
+
+ if not app_infos.get("from_catalog", None):
+ return "url_required"
+ if not app_infos["from_catalog"].get("lastUpdate") or not app_infos["from_catalog"].get("git"):
+ return "url_required"
+
+ settings = app_infos["settings"]
+ local_update_time = settings.get('update_time', settings.get('install_time', 0))
+ if app_infos["from_catalog"]['lastUpdate'] > local_update_time:
+ return "yes"
+ else:
+ return "no"
def app_map(app=None, raw=False, user=None):
@@ -405,11 +210,12 @@ def app_map(app=None, raw=False, user=None):
app -- Specific app to map
"""
+
from yunohost.permission import user_permission_list
- from yunohost.utils.ldap import _get_ldap_interface
apps = []
result = {}
+ permissions = user_permission_list(full=True)["permissions"]
if app is not None:
if not _is_installed(app):
@@ -427,27 +233,86 @@ def app_map(app=None, raw=False, user=None):
if 'path' not in app_settings:
# we assume that an app that doesn't have a path doesn't have an HTTP api
continue
+ # This 'no_sso' settings sound redundant to not having $path defined ....
+ # At least from what I can see, all apps using it don't have a path defined ...
if 'no_sso' in app_settings: # I don't think we need to check for the value here
continue
- if user is not None:
- ldap = _get_ldap_interface()
- if not ldap.search(base='ou=permission,dc=yunohost,dc=org',
- filter='(&(objectclass=permissionYnh)(cn=main.%s)(inheritPermission=uid=%s,ou=users,dc=yunohost,dc=org))' % (app_id, user),
- attrs=['cn']):
+ # Users must at least have access to the main permission to have access to extra permissions
+ if user:
+ if not app_id + ".main" in permissions:
+ logger.warning("Uhoh, no main permission was found for app %s ... sounds like an app was only partially removed due to another bug :/" % app_id)
+ continue
+ main_perm = permissions[app_id + ".main"]
+ if user not in main_perm["corresponding_users"]:
continue
domain = app_settings['domain']
- path = app_settings['path']
+ path = app_settings['path'].rstrip('/')
+ label = app_settings['label']
- if raw:
- if domain not in result:
- result[domain] = {}
- result[domain][path] = {
- 'label': app_settings['label'],
- 'id': app_settings['id']
- }
- else:
- result[domain + path] = app_settings['label']
+ def _sanitized_absolute_url(perm_url):
+ # Nominal case : url is relative to the app's path
+ if perm_url.startswith("/"):
+ perm_domain = domain
+ perm_path = path + perm_url.rstrip("/")
+ # Otherwise, the urls starts with a domain name, like domain.tld/foo/bar
+ # We want perm_domain = domain.tld and perm_path = "/foo/bar"
+ else:
+ perm_domain, perm_path = perm_url.split("/", 1)
+ perm_path = "/" + perm_path.rstrip("/")
+
+ perm_path = perm_path if perm_path.strip() != "" else "/"
+
+ return perm_domain, perm_path
+
+ this_app_perms = {p: i for p, i in permissions.items() if p.startswith(app_id + ".") and i["url"]}
+ for perm_name, perm_info in this_app_perms.items():
+ # If we're building the map for a specific user, check the user
+ # actually is allowed for this specific perm
+ if user and user not in perm_info["corresponding_users"]:
+ continue
+ if perm_info["url"].startswith("re:"):
+ # Here, we have an issue if the chosen url is a regex, because
+ # the url we want to add to the dict is going to be turned into
+ # a clickable link (or analyzed by other parts of yunohost
+ # code...). To put it otherwise : in the current code of ssowat,
+ # you can't give access a user to a regex.
+ #
+ # Instead, as drafted by Josue, we could rework the ssowat logic
+ # about how routes and their permissions are defined. So for example,
+ # have a dict of
+ # { "/route1": ["visitors", "user1", "user2", ...], # Public route
+ # "/route2_with_a_regex$": ["user1", "user2"], # Private route
+ # "/route3": None, # Skipped route idk
+ # }
+ # then each time a user try to request and url, we only keep the
+ # longest matching rule and check the user is allowed etc...
+ #
+ # The challenge with this is (beside actually implementing it)
+ # is that it creates a whole new mechanism that ultimately
+ # replace all the existing logic about
+ # protected/unprotected/skipped uris and regexes and we gotta
+ # handle / migrate all the legacy stuff somehow if we don't
+ # want to end up with a total mess in the future idk
+ logger.error("Permission %s can't be added to the SSOwat configuration because it doesn't support regexes so far..." % perm_name)
+ continue
+
+ perm_domain, perm_path = _sanitized_absolute_url(perm_info["url"])
+ if perm_name.endswith(".main"):
+ perm_label = label
+ else:
+ # e.g. if perm_name is wordpress.admin, we want "Blog (Admin)" (where Blog is the label of this app)
+ perm_label = "%s (%s)" % (label, perm_name.rsplit(".")[-1].replace("_", " ").title())
+
+ if raw:
+ if domain not in result:
+ result[perm_domain] = {}
+ result[perm_domain][perm_path] = {
+ 'label': perm_label,
+ 'id': app_id
+ }
+ else:
+ result[perm_domain + perm_path] = perm_label
return result
@@ -465,7 +330,6 @@ def app_change_url(operation_logger, app, domain, path):
"""
from yunohost.hook import hook_exec, hook_callback
from yunohost.domain import _normalize_domain_path, _get_conflicting_apps
- from yunohost.permission import permission_update
installed = _is_installed(app)
if not installed:
@@ -502,7 +366,7 @@ def app_change_url(operation_logger, app, domain, path):
# Retrieve arguments list for change_url script
# TODO: Allow to specify arguments
args_odict = _parse_args_from_manifest(manifest, 'change_url')
- args_list = [ value[0] for value in args_odict.values() ]
+ args_list = [value[0] for value in args_odict.values()]
args_list.append(app)
# Prepare env. var. to pass to script
@@ -555,7 +419,7 @@ def app_change_url(operation_logger, app, domain, path):
app_setting(app, 'domain', value=domain)
app_setting(app, 'path', value=path)
- permission_update(app, permission="main", add_url=[domain+path], remove_url=[old_domain+old_path], sync_perm=True)
+ app_ssowatconf()
# avoid common mistakes
if _run_service_command("reload", "nginx") is False:
@@ -587,27 +451,20 @@ def app_upgrade(app=[], url=None, file=None):
from yunohost.hook import hook_add, hook_remove, hook_exec, hook_callback
from yunohost.permission import permission_sync_to_user
- try:
- app_list()
- except YunohostError:
- raise YunohostError('apps_already_up_to_date')
-
- not_upgraded_apps = []
-
apps = app
# If no app is specified, upgrade all apps
if not apps:
# FIXME : not sure what's supposed to happen if there is a url and a file but no apps...
if not url and not file:
- apps = [app["id"] for app in app_list(installed=True)["apps"]]
+ apps = _installed_apps()
elif not isinstance(app, list):
apps = [app]
# Remove possible duplicates
- apps = [app for i,app in enumerate(apps) if apps not in apps[:i]]
+ apps = [app_ for i, app_ in enumerate(apps) if app_ not in apps[:i]]
# Abort if any of those app is in fact not installed..
- for app in [app for app in apps if not _is_installed(app)]:
+ for app in [app_ for app_ in apps if not _is_installed(app_)]:
raise YunohostError('app_not_installed', app=app, all_apps=_get_all_installed_apps_id())
if len(apps) == 0:
@@ -618,7 +475,7 @@ def app_upgrade(app=[], url=None, file=None):
for number, app_instance_name in enumerate(apps):
logger.info(m18n.n('app_upgrade_app_name', app=app_instance_name))
- app_dict = app_info(app_instance_name, raw=True)
+ app_dict = app_info(app_instance_name, full=True)
if file and isinstance(file, dict):
# We use this dirty hack to test chained upgrades in unit/functional tests
@@ -642,14 +499,10 @@ def app_upgrade(app=[], url=None, file=None):
app_setting_path = APPS_SETTING_PATH + '/' + app_instance_name
- # Retrieve current app status
- status = _get_app_status(app_instance_name)
- status['remote'] = manifest.get('remote', None)
-
# Retrieve arguments list for upgrade script
# TODO: Allow to specify arguments
args_odict = _parse_args_from_manifest(manifest, 'upgrade')
- args_list = [ value[0] for value in args_odict.values() ]
+ args_list = [value[0] for value in args_odict.values()]
args_list.append(app_instance_name)
# Prepare env. var. to pass to script
@@ -664,88 +517,92 @@ def app_upgrade(app=[], url=None, file=None):
operation_logger = OperationLogger('app_upgrade', related_to, env=env_dict)
operation_logger.start()
+ # Attempt to patch legacy helpers ...
+ _patch_legacy_helpers(extracted_app_folder)
+
# Apply dirty patch to make php5 apps compatible with php7
_patch_php5(extracted_app_folder)
# Execute App upgrade script
os.system('chown -hR admin: %s' % INSTALL_TMP)
+ # Execute the app upgrade script
+ upgrade_failed = True
try:
upgrade_retcode = hook_exec(extracted_app_folder + '/scripts/upgrade',
args=args_list, env=env_dict)[0]
+
+ upgrade_failed = True if upgrade_retcode != 0 else False
+ if upgrade_failed:
+ error = m18n.n('app_upgrade_script_failed')
+ logger.error(m18n.n("app_upgrade_failed", app=app_instance_name, error=error))
+ failure_message_with_debug_instructions = operation_logger.error(error)
+ if msettings.get('interface') != 'api':
+ dump_app_log_extract_for_debugging(operation_logger)
+ # Script got manually interrupted ... N.B. : KeyboardInterrupt does not inherit from Exception
except (KeyboardInterrupt, EOFError):
upgrade_retcode = -1
+ error = m18n.n('operation_interrupted')
+ logger.error(m18n.n("app_upgrade_failed", app=app_instance_name, error=error))
+ failure_message_with_debug_instructions = operation_logger.error(error)
+ # Something wrong happened in Yunohost's code (most probably hook_exec)
except Exception:
import traceback
- logger.exception(m18n.n('unexpected_error', error=u"\n" + traceback.format_exc()))
+ error = m18n.n('unexpected_error', error=u"\n" + traceback.format_exc())
+ logger.error(m18n.n("app_install_failed", app=app_instance_name, error=error))
+ failure_message_with_debug_instructions = operation_logger.error(error)
finally:
-
- # Did the script succeed ?
- if upgrade_retcode == -1:
- error_msg = m18n.n('operation_interrupted')
- operation_logger.error(error_msg)
- elif upgrade_retcode != 0:
- error_msg = m18n.n('app_upgrade_failed', app=app_instance_name)
- operation_logger.error(error_msg)
-
- # Did it broke the system ?
+ # Whatever happened (install success or failure) we check if it broke the system
+ # and warn the user about it
try:
broke_the_system = False
_assert_system_is_sane_for_app(manifest, "post")
except Exception as e:
broke_the_system = True
- error_msg = operation_logger.error(str(e))
+ logger.error(m18n.n("app_upgrade_failed", app=app_instance_name, error=str(e)))
+ failure_message_with_debug_instructions = operation_logger.error(str(e))
# If upgrade failed or broke the system,
# raise an error and interrupt all other pending upgrades
- if upgrade_retcode != 0 or broke_the_system:
+ if upgrade_failed or broke_the_system:
# display this if there are remaining apps
if apps[number + 1:]:
- logger.error(m18n.n('app_upgrade_stopped'))
not_upgraded_apps = apps[number:]
- # we don't want to continue upgrading apps here in case that breaks
- # everything
- raise YunohostError('app_not_upgraded',
+ logger.error(m18n.n('app_not_upgraded',
failed_app=app_instance_name,
- apps=', '.join(not_upgraded_apps))
- else:
- raise YunohostError(error_msg, raw_msg=True)
+ apps=', '.join(not_upgraded_apps)))
+
+ raise YunohostError(failure_message_with_debug_instructions, raw_msg=True)
# Otherwise we're good and keep going !
- else:
- now = int(time.time())
- # TODO: Move install_time away from app_setting
- app_setting(app_instance_name, 'update_time', now)
- status['upgraded_at'] = now
+ now = int(time.time())
+ app_setting(app_instance_name, 'update_time', now)
+ app_setting(app_instance_name, 'current_revision', manifest.get('remote', {}).get('revision', "?"))
- # Clean hooks and add new ones
- hook_remove(app_instance_name)
- if 'hooks' in os.listdir(extracted_app_folder):
- for hook in os.listdir(extracted_app_folder + '/hooks'):
- hook_add(app_instance_name, extracted_app_folder + '/hooks/' + hook)
+ # Clean hooks and add new ones
+ hook_remove(app_instance_name)
+ if 'hooks' in os.listdir(extracted_app_folder):
+ for hook in os.listdir(extracted_app_folder + '/hooks'):
+ hook_add(app_instance_name, extracted_app_folder + '/hooks/' + hook)
- # Store app status
- with open(app_setting_path + '/status.json', 'w+') as f:
- json.dump(status, f)
+ # Replace scripts and manifest and conf (if exists)
+ os.system('rm -rf "%s/scripts" "%s/manifest.toml %s/manifest.json %s/conf"' % (app_setting_path, app_setting_path, app_setting_path, app_setting_path))
- # Replace scripts and manifest and conf (if exists)
- os.system('rm -rf "%s/scripts" "%s/manifest.toml %s/manifest.json %s/conf"' % (app_setting_path, app_setting_path, app_setting_path, app_setting_path))
+ if os.path.exists(os.path.join(extracted_app_folder, "manifest.json")):
+ os.system('mv "%s/manifest.json" "%s/scripts" %s' % (extracted_app_folder, extracted_app_folder, app_setting_path))
+ if os.path.exists(os.path.join(extracted_app_folder, "manifest.toml")):
+ os.system('mv "%s/manifest.toml" "%s/scripts" %s' % (extracted_app_folder, extracted_app_folder, app_setting_path))
- if os.path.exists(os.path.join(extracted_app_folder, "manifest.json")):
- os.system('mv "%s/manifest.json" "%s/scripts" %s' % (extracted_app_folder, extracted_app_folder, app_setting_path))
- if os.path.exists(os.path.join(extracted_app_folder, "manifest.toml")):
- os.system('mv "%s/manifest.toml" "%s/scripts" %s' % (extracted_app_folder, extracted_app_folder, app_setting_path))
+ for file_to_copy in ["actions.json", "actions.toml", "config_panel.json", "config_panel.toml", "conf"]:
+ if os.path.exists(os.path.join(extracted_app_folder, file_to_copy)):
+ os.system('cp -R %s/%s %s' % (extracted_app_folder, file_to_copy, app_setting_path))
- for file_to_copy in ["actions.json", "actions.toml", "config_panel.json", "config_panel.toml", "conf"]:
- if os.path.exists(os.path.join(extracted_app_folder, file_to_copy)):
- os.system('cp -R %s/%s %s' % (extracted_app_folder, file_to_copy, app_setting_path))
+ # So much win
+ logger.success(m18n.n('app_upgraded', app=app_instance_name))
- # So much win
- logger.success(m18n.n('app_upgraded', app=app_instance_name))
-
- hook_callback('post_app_upgrade', args=args_list, env=env_dict)
- operation_logger.success()
+ hook_callback('post_app_upgrade', args=args_list, env=env_dict)
+ operation_logger.success()
permission_sync_to_user()
@@ -765,44 +622,56 @@ def app_install(operation_logger, app, label=None, args=None, no_remove_on_failu
force -- Do not ask for confirmation when installing experimental / low-quality apps
"""
- from yunohost.utils.ldap import _get_ldap_interface
from yunohost.hook import hook_add, hook_remove, hook_exec, hook_callback
from yunohost.log import OperationLogger
- from yunohost.permission import permission_add, permission_update, permission_remove, permission_sync_to_user
- ldap = _get_ldap_interface()
+ from yunohost.permission import user_permission_list, permission_create, permission_url, permission_delete, permission_sync_to_user, user_permission_update
# Fetch or extract sources
if not os.path.exists(INSTALL_TMP):
os.makedirs(INSTALL_TMP)
- status = {
- 'installed_at': int(time.time()),
- 'upgraded_at': None,
- 'remote': {
- 'type': None,
- },
- }
-
def confirm_install(confirm):
# Ignore if there's nothing for confirm (good quality app), if --force is used
# or if request on the API (confirm already implemented on the API side)
if confirm is None or force or msettings.get('interface') == 'api':
return
- answer = msignals.prompt(m18n.n('confirm_app_install_' + confirm,
- answers='Y/N'))
- if answer.upper() != "Y":
- raise YunohostError("aborting")
+ if confirm in ["danger", "thirdparty"]:
+ answer = msignals.prompt(m18n.n('confirm_app_install_' + confirm,
+ answers='Yes, I understand'),
+ color="red")
+ if answer != "Yes, I understand":
+ raise YunohostError("aborting")
- raw_app_list = app_list(raw=True)
+ else:
+ answer = msignals.prompt(m18n.n('confirm_app_install_' + confirm,
+ answers='Y/N'),
+ color="yellow")
+ if answer.upper() != "Y":
+ raise YunohostError("aborting")
+
+ raw_app_list = _load_apps_catalog()["apps"]
if app in raw_app_list or ('@' in app) or ('http://' in app) or ('https://' in app):
+
+ # If we got an app name directly (e.g. just "wordpress"), we gonna test this name
if app in raw_app_list:
- state = raw_app_list[app].get("state", "notworking")
- level = raw_app_list[app].get("level", None)
+ app_name_to_test = app
+ # If we got an url like "https://github.com/foo/bar_ynh, we want to
+ # extract "bar" and test if we know this app
+ elif ('http://' in app) or ('https://' in app):
+ app_name_to_test = app.strip("/").split("/")[-1].replace("_ynh", "")
+ else:
+ # FIXME : watdo if '@' in app ?
+ app_name_to_test = None
+
+ if app_name_to_test in raw_app_list:
+
+ state = raw_app_list[app_name_to_test].get("state", "notworking")
+ level = raw_app_list[app_name_to_test].get("level", None)
confirm = "danger"
if state in ["working", "validated"]:
- if isinstance(level, int) and level >= 3:
+ if isinstance(level, int) and level >= 5:
confirm = None
elif isinstance(level, int) and level > 0:
confirm = "warning"
@@ -817,7 +686,6 @@ def app_install(operation_logger, app, label=None, args=None, no_remove_on_failu
manifest, extracted_app_folder = _extract_app_from_file(app)
else:
raise YunohostError('app_unknown')
- status['remote'] = manifest.get('remote', {})
# Check ID
if 'id' not in manifest or '__' in manifest['id']:
@@ -844,9 +712,12 @@ def app_install(operation_logger, app, label=None, args=None, no_remove_on_failu
args_dict = {} if not args else \
dict(urlparse.parse_qsl(args, keep_blank_values=True))
args_odict = _parse_args_from_manifest(manifest, 'install', args=args_dict)
- args_list = [ value[0] for value in args_odict.values() ]
+ args_list = [value[0] for value in args_odict.values()]
args_list.append(app_instance_name)
+ # Validate domain / path availability for webapps
+ _validate_and_normalize_webpath(manifest, args_odict, extracted_app_folder)
+
# Prepare env. var. to pass to script
env_dict = _make_environment_dict(args_odict)
env_dict["YNH_APP_ID"] = app_id
@@ -859,8 +730,8 @@ def app_install(operation_logger, app, label=None, args=None, no_remove_on_failu
# Tell the operation_logger to redact all password-type args
# Also redact the % escaped version of the password that might appear in
# the 'args' section of metadata (relevant for password with non-alphanumeric char)
- data_to_redact = [ value[0] for value in args_odict.values() if value[1] == "password" ]
- data_to_redact += [ urllib.quote(data) for data in data_to_redact if urllib.quote(data) != data ]
+ data_to_redact = [value[0] for value in args_odict.values() if value[1] == "password"]
+ data_to_redact += [urllib.quote(data) for data in data_to_redact if urllib.quote(data) != data]
operation_logger.data_to_redact.extend(data_to_redact)
operation_logger.related_to = [s for s in operation_logger.related_to if s[0] != "app"]
@@ -879,11 +750,14 @@ def app_install(operation_logger, app, label=None, args=None, no_remove_on_failu
app_settings = {
'id': app_instance_name,
'label': label if label else manifest['name'],
+ 'install_time': int(time.time()),
+ 'current_revision': manifest.get('remote', {}).get('revision', "?")
}
- # TODO: Move install_time away from app settings
- app_settings['install_time'] = status['installed_at']
_set_app_settings(app_instance_name, app_settings)
+ # Attempt to patch legacy helpers ...
+ _patch_legacy_helpers(extracted_app_folder)
+
# Apply dirty patch to make php5 apps compatible with php7
_patch_php5(extracted_app_folder)
@@ -902,83 +776,108 @@ def app_install(operation_logger, app, label=None, args=None, no_remove_on_failu
if os.path.exists(os.path.join(extracted_app_folder, file_to_copy)):
os.system('cp -R %s/%s %s' % (extracted_app_folder, file_to_copy, app_setting_path))
- # Create permission before the install (useful if the install script redefine the permission)
- # Note that sync_perm is disabled to avoid triggering a whole bunch of code and messages
- # can't be sure that we don't have one case when it's needed
- permission_add(app=app_instance_name, permission="main", sync_perm=False)
+ # Initialize the main permission for the app
+ # After the install, if apps don't have a domain and path defined, the default url '/' is removed from the permission
+ permission_create(app_instance_name+".main", url="/", allowed=["all_users"])
# Execute the app install script
- install_retcode = 1
+ install_failed = True
try:
install_retcode = hook_exec(
os.path.join(extracted_app_folder, 'scripts/install'),
args=args_list, env=env_dict
)[0]
+ # "Common" app install failure : the script failed and returned exit code != 0
+ install_failed = True if install_retcode != 0 else False
+ if install_failed:
+ error = m18n.n('app_install_script_failed')
+ logger.error(m18n.n("app_install_failed", app=app_id, error=error))
+ failure_message_with_debug_instructions = operation_logger.error(error)
+ if msettings.get('interface') != 'api':
+ dump_app_log_extract_for_debugging(operation_logger)
+ # Script got manually interrupted ... N.B. : KeyboardInterrupt does not inherit from Exception
except (KeyboardInterrupt, EOFError):
- install_retcode = -1
- except Exception:
+ error = m18n.n('operation_interrupted')
+ logger.error(m18n.n("app_install_failed", app=app_id, error=error))
+ failure_message_with_debug_instructions = operation_logger.error(error)
+ # Something wrong happened in Yunohost's code (most probably hook_exec)
+ except Exception as e:
import traceback
- logger.exception(m18n.n('unexpected_error', error=u"\n" + traceback.format_exc()))
+ error = m18n.n('unexpected_error', error=u"\n" + traceback.format_exc())
+ logger.error(m18n.n("app_install_failed", app=app_id, error=error))
+ failure_message_with_debug_instructions = operation_logger.error(error)
finally:
+ # Whatever happened (install success or failure) we check if it broke the system
+ # and warn the user about it
try:
broke_the_system = False
_assert_system_is_sane_for_app(manifest, "post")
except Exception as e:
broke_the_system = True
- error_msg = operation_logger.error(str(e))
+ logger.error(m18n.n("app_install_failed", app=app_id, error=str(e)))
+ failure_message_with_debug_instructions = operation_logger.error(str(e))
- if install_retcode != 0:
- error_msg = operation_logger.error(m18n.n('unexpected_error', error='shell command return code: %s' % install_retcode))
+ # If the install failed or broke the system, we remove it
+ if install_failed or broke_the_system:
- if install_retcode != 0 or broke_the_system:
- if not no_remove_on_failure:
- # Setup environment for remove script
- env_dict_remove = {}
- env_dict_remove["YNH_APP_ID"] = app_id
- env_dict_remove["YNH_APP_INSTANCE_NAME"] = app_instance_name
- env_dict_remove["YNH_APP_INSTANCE_NUMBER"] = str(instance_number)
+ # This option is meant for packagers to debug their apps more easily
+ if no_remove_on_failure:
+ raise YunohostError("The installation of %s failed, but was not cleaned up as requested by --no-remove-on-failure." % app_id, raw_msg=True)
+ else:
+ logger.warning(m18n.n("app_remove_after_failed_install"))
- # Execute remove script
- operation_logger_remove = OperationLogger('remove_on_failed_install',
- [('app', app_instance_name)],
- env=env_dict_remove)
- operation_logger_remove.start()
+ # Setup environment for remove script
+ env_dict_remove = {}
+ env_dict_remove["YNH_APP_ID"] = app_id
+ env_dict_remove["YNH_APP_INSTANCE_NAME"] = app_instance_name
+ env_dict_remove["YNH_APP_INSTANCE_NUMBER"] = str(instance_number)
+ # Execute remove script
+ operation_logger_remove = OperationLogger('remove_on_failed_install',
+ [('app', app_instance_name)],
+ env=env_dict_remove)
+ operation_logger_remove.start()
+
+ # Try to remove the app
+ try:
remove_retcode = hook_exec(
os.path.join(extracted_app_folder, 'scripts/remove'),
args=[app_instance_name], env=env_dict_remove
)[0]
- # Remove all permission in LDAP
- result = ldap.search(base='ou=permission,dc=yunohost,dc=org',
- filter='(&(objectclass=permissionYnh)(cn=*.%s))' % app_instance_name, attrs=['cn'])
- permission_list = [p['cn'][0] for p in result]
- for l in permission_list:
- permission_remove(app_instance_name, l.split('.')[0], force=True)
+ # Here again, calling hook_exec could fail miserably, or get
+ # manually interrupted (by mistake or because script was stuck)
+ # In that case we still want to proceed with the rest of the
+ # removal (permissions, /etc/yunohost/apps/{app} ...)
+ except (KeyboardInterrupt, EOFError, Exception):
+ remove_retcode = -1
+ import traceback
+ logger.error(m18n.n('unexpected_error', error=u"\n" + traceback.format_exc()))
- if remove_retcode != 0:
- msg = m18n.n('app_not_properly_removed',
- app=app_instance_name)
- logger.warning(msg)
- operation_logger_remove.error(msg)
+ # Remove all permission in LDAP
+ for permission_name in user_permission_list()["permissions"].keys():
+ if permission_name.startswith(app_instance_name+"."):
+ permission_delete(permission_name, force=True, sync_perm=False)
+
+ if remove_retcode != 0:
+ msg = m18n.n('app_not_properly_removed',
+ app=app_instance_name)
+ logger.warning(msg)
+ operation_logger_remove.error(msg)
+ else:
+ try:
+ _assert_system_is_sane_for_app(manifest, "post")
+ except Exception as e:
+ operation_logger_remove.error(e)
else:
- try:
- _assert_system_is_sane_for_app(manifest, "post")
- except Exception as e:
- operation_logger_remove.error(e)
- else:
- operation_logger_remove.success()
+ operation_logger_remove.success()
# Clean tmp folders
shutil.rmtree(app_setting_path)
shutil.rmtree(extracted_app_folder)
- app_ssowatconf()
+ permission_sync_to_user()
- if install_retcode == -1:
- msg = m18n.n('operation_interrupted') + " " + error_msg
- raise YunohostError(msg, raw_msg=True)
- msg = error_msg
- raise YunohostError(msg, raw_msg=True)
+ raise YunohostError(failure_message_with_debug_instructions, raw_msg=True)
# Clean hooks and add new ones
hook_remove(app_instance_name)
@@ -986,22 +885,20 @@ def app_install(operation_logger, app, label=None, args=None, no_remove_on_failu
for file in os.listdir(extracted_app_folder + '/hooks'):
hook_add(app_instance_name, extracted_app_folder + '/hooks/' + file)
- # Store app status
- with open(app_setting_path + '/status.json', 'w+') as f:
- json.dump(status, f)
-
# Clean and set permissions
shutil.rmtree(extracted_app_folder)
os.system('chmod -R 400 %s' % app_setting_path)
os.system('chown -R root: %s' % app_setting_path)
os.system('chown -R admin: %s/scripts' % app_setting_path)
- # Add path in permission if it's defined in the app install script
+ # If an app doesn't have at least a domain and a path, assume it's not a webapp and remove the default "/" permission
app_settings = _get_app_settings(app_instance_name)
domain = app_settings.get('domain', None)
path = app_settings.get('path', None)
- if domain and path:
- permission_update(app_instance_name, permission="main", add_url=[domain+path], sync_perm=False)
+ if not (domain and path):
+ permission_url(app_instance_name + ".main", url=None, sync_perm=False)
+
+ _migrate_legacy_permissions(app_instance_name)
permission_sync_to_user()
@@ -1010,6 +907,61 @@ def app_install(operation_logger, app, label=None, args=None, no_remove_on_failu
hook_callback('post_app_install', args=args_list, env=env_dict)
+def dump_app_log_extract_for_debugging(operation_logger):
+
+ with open(operation_logger.log_path, "r") as f:
+ lines = f.readlines()
+
+ lines_to_display = []
+ for line in lines:
+
+ if not ": " in line.strip():
+ continue
+
+ # A line typically looks like
+ # 2019-10-19 16:10:27,611: DEBUG - + mysql -u piwigo --password=********** -B piwigo
+ # And we just want the part starting by "DEBUG - "
+ line = line.strip().split(": ", 1)[1]
+ lines_to_display.append(line)
+
+ if line.endswith("+ ynh_exit_properly") or " + ynh_die " in line:
+ break
+ elif len(lines_to_display) > 20:
+ lines_to_display.pop(0)
+
+ logger.warning("Here's an extract of the logs before the crash. It might help debugging the error:")
+ for line in lines_to_display:
+ logger.info(line)
+
+
+def _migrate_legacy_permissions(app):
+
+ from yunohost.permission import user_permission_list, user_permission_update
+
+ # Check if app is apparently using the legacy permission management, defined by the presence of something like
+ # ynh_app_setting_set on unprotected_uris (or yunohost app setting)
+ install_script_path = os.path.join(APPS_SETTING_PATH, app, 'scripts/install')
+ install_script_content = open(install_script_path, "r").read()
+ if not re.search(r"(yunohost app setting|ynh_app_setting_set) .*(unprotected|skipped)_uris", install_script_content):
+ return
+
+ app_settings = _get_app_settings(app)
+ app_perm_currently_allowed = user_permission_list()["permissions"][app + ".main"]["allowed"]
+
+ settings_say_it_should_be_public = (app_settings.get("unprotected_uris", None) == "/"
+ or app_settings.get("skipped_uris", None) == "/")
+
+ # If the current permission says app is protected, but there are legacy rules saying it should be public...
+ if app_perm_currently_allowed == ["all_users"] and settings_say_it_should_be_public:
+ # Make it public
+ user_permission_update(app + ".main", add="visitors", sync_perm=False)
+
+ # If the current permission says app is public, but there are no setting saying it should be public...
+ if app_perm_currently_allowed == ["visitors"] and not settings_say_it_should_be_public:
+ # Make is private
+ user_permission_update(app + ".main", remove="visitors", sync_perm=False)
+
+
@is_unit_operation()
def app_remove(operation_logger, app):
"""
@@ -1019,9 +971,8 @@ def app_remove(operation_logger, app):
app -- App(s) to delete
"""
- from yunohost.utils.ldap import _get_ldap_interface
from yunohost.hook import hook_exec, hook_remove, hook_callback
- from yunohost.permission import permission_remove, permission_sync_to_user
+ from yunohost.permission import user_permission_list, permission_delete, permission_sync_to_user
if not _is_installed(app):
raise YunohostError('app_not_installed', app=app, all_apps=_get_all_installed_apps_id())
@@ -1034,9 +985,12 @@ def app_remove(operation_logger, app):
# TODO: display fail messages from script
try:
shutil.rmtree('/tmp/yunohost_remove')
- except:
+ except Exception:
pass
+ # Attempt to patch legacy helpers ...
+ _patch_legacy_helpers(app_setting_path)
+
# Apply dirty patch to make php5 apps compatible with php7 (e.g. the remove
# script might date back from jessie install)
_patch_php5(app_setting_path)
@@ -1057,11 +1011,24 @@ def app_remove(operation_logger, app):
operation_logger.extra.update({'env': env_dict})
operation_logger.flush()
- if hook_exec('/tmp/yunohost_remove/scripts/remove', args=args_list,
- env=env_dict)[0] == 0:
- logger.success(m18n.n('app_removed', app=app))
+ try:
+ ret = hook_exec('/tmp/yunohost_remove/scripts/remove',
+ args=args_list,
+ env=env_dict)[0]
+ # Here again, calling hook_exec could fail miserably, or get
+ # manually interrupted (by mistake or because script was stuck)
+ # In that case we still want to proceed with the rest of the
+ # removal (permissions, /etc/yunohost/apps/{app} ...)
+ except (KeyboardInterrupt, EOFError, Exception):
+ ret = -1
+ import traceback
+ logger.error(m18n.n('unexpected_error', error=u"\n" + traceback.format_exc()))
+ if ret == 0:
+ logger.success(m18n.n('app_removed', app=app))
hook_callback('post_app_remove', args=args_list, env=env_dict)
+ else:
+ logger.warning(m18n.n('app_not_properly_removed', app=app))
if os.path.exists(app_setting_path):
shutil.rmtree(app_setting_path)
@@ -1069,19 +1036,15 @@ def app_remove(operation_logger, app):
hook_remove(app)
# Remove all permission in LDAP
- ldap = _get_ldap_interface()
- result = ldap.search(base='ou=permission,dc=yunohost,dc=org',
- filter='(&(objectclass=permissionYnh)(cn=*.%s))' % app, attrs=['cn'])
- permission_list = [p['cn'][0] for p in result]
- for l in permission_list:
- permission_remove(app, l.split('.')[0], force=True, sync_perm=False)
+ for permission_name in user_permission_list()["permissions"].keys():
+ if permission_name.startswith(app+"."):
+ permission_delete(permission_name, force=True, sync_perm=False)
permission_sync_to_user()
_assert_system_is_sane_for_app(manifest, "post")
-@is_unit_operation(['permission','app'])
-def app_addaccess(operation_logger, apps, users=[]):
+def app_addaccess(apps, users=[]):
"""
Grant access right to users (everyone by default)
@@ -1092,15 +1055,17 @@ def app_addaccess(operation_logger, apps, users=[]):
"""
from yunohost.permission import user_permission_update
- permission = user_permission_update(operation_logger, app=apps, permission="main", add_username=users)
+ logger.warning("/!\\ Packagers ! This app is using the legacy permission system. Please use the new helpers ynh_permission_{create,url,update,delete} and the 'visitors' group to manage permissions.")
- result = {p : v['main']['allowed_users'] for p, v in permission['permissions'].items()}
+ output = {}
+ for app in apps:
+ permission = user_permission_update(app+".main", add=users, remove="all_users")
+ output[app] = permission["corresponding_users"]
- return {'allowed_users': result}
+ return {'allowed_users': output}
-@is_unit_operation(['permission','app'])
-def app_removeaccess(operation_logger, apps, users=[]):
+def app_removeaccess(apps, users=[]):
"""
Revoke access right to users (everyone by default)
@@ -1111,15 +1076,17 @@ def app_removeaccess(operation_logger, apps, users=[]):
"""
from yunohost.permission import user_permission_update
- permission = user_permission_update(operation_logger, app=apps, permission="main", del_username=users)
+ logger.warning("/!\\ Packagers ! This app is using the legacy permission system. Please use the new helpers ynh_permission_{create,url,update,delete} and the 'visitors' group to manage permissions.")
- result = {p : v['main']['allowed_users'] for p, v in permission['permissions'].items()}
+ output = {}
+ for app in apps:
+ permission = user_permission_update(app+".main", remove=users)
+ output[app] = permission["corresponding_users"]
- return {'allowed_users': result}
+ return {'allowed_users': output}
-@is_unit_operation(['permission','app'])
-def app_clearaccess(operation_logger, apps):
+def app_clearaccess(apps):
"""
Reset access rights for the app
@@ -1127,34 +1094,16 @@ def app_clearaccess(operation_logger, apps):
apps
"""
- from yunohost.permission import user_permission_clear
+ from yunohost.permission import user_permission_reset
- permission = user_permission_clear(operation_logger, app=apps, permission="main")
+ logger.warning("/!\\ Packagers ! This app is using the legacy permission system. Please use the new helpers ynh_permission_{create,url,update,delete} and the 'visitors' group to manage permissions.")
- result = {p : v['main']['allowed_users'] for p, v in permission['permissions'].items()}
+ output = {}
+ for app in apps:
+ permission = user_permission_reset(app+".main")
+ output[app] = permission["corresponding_users"]
- return {'allowed_users': result}
-
-def app_debug(app):
- """
- Display debug informations for an app
-
- Keyword argument:
- app
- """
- manifest = _get_manifest_of_app(os.path.join(APPS_SETTING_PATH, app))
-
- return {
- 'name': manifest['id'],
- 'label': manifest['name'],
- 'services': [{
- "name": x,
- "logs": [{
- "file_name": y,
- "file_content": "\n".join(z),
- } for (y, z) in sorted(service_log(x).items(), key=lambda x: x[0])],
- } for x in sorted(manifest.get("services", []))]
- }
+ return {'allowed_users': output}
@is_unit_operation()
@@ -1179,30 +1128,27 @@ def app_makedefault(operation_logger, app, domain=None):
elif domain not in domain_list()['domains']:
raise YunohostError('domain_unknown')
- operation_logger.start()
if '/' in app_map(raw=True)[domain]:
raise YunohostError('app_make_default_location_already_used', app=app, domain=app_domain,
other_app=app_map(raw=True)[domain]["/"]["id"])
- try:
- with open('/etc/ssowat/conf.json.persistent') as json_conf:
- ssowat_conf = json.loads(str(json_conf.read()))
- except ValueError as e:
- raise YunohostError('ssowat_persistent_conf_read_error', error=e)
- except IOError:
+ operation_logger.start()
+
+ # TODO / FIXME : current trick is to add this to conf.json.persisten
+ # This is really not robust and should be improved
+ # e.g. have a flag in /etc/yunohost/apps/$app/ to say that this is the
+ # default app or idk...
+ if not os.path.exists('/etc/ssowat/conf.json.persistent'):
ssowat_conf = {}
+ else:
+ ssowat_conf = read_json('/etc/ssowat/conf.json.persistent')
if 'redirected_urls' not in ssowat_conf:
ssowat_conf['redirected_urls'] = {}
ssowat_conf['redirected_urls'][domain + '/'] = app_domain + app_path
- try:
- with open('/etc/ssowat/conf.json.persistent', 'w+') as f:
- json.dump(ssowat_conf, f, sort_keys=True, indent=4)
- except IOError as e:
- raise YunohostError('ssowat_persistent_conf_write_error', error=e)
-
+ write_to_json('/etc/ssowat/conf.json.persistent', ssowat_conf)
os.system('chmod 644 /etc/ssowat/conf.json.persistent')
logger.success(m18n.n('ssowat_conf_updated'))
@@ -1227,33 +1173,25 @@ def app_setting(app, key, value=None, delete=False):
except Exception as e:
logger.debug("cannot get app setting '%s' for '%s' (%s)", key, app, e)
return None
- else:
- if delete and key in app_settings:
+
+ if delete:
+ if key in app_settings:
del app_settings[key]
- else:
- # FIXME: Allow multiple values for some keys?
- if key in ['redirected_urls', 'redirected_regex']:
- value = yaml.load(value)
- app_settings[key] = value
- _set_app_settings(app, app_settings)
-
-
-def app_checkport(port):
- """
- Check availability of a local port
-
- Keyword argument:
- port -- Port to check
-
- """
-
- # This import cannot be moved on top of file because it create a recursive
- # import...
- from yunohost.tools import tools_port_available
- if tools_port_available(port):
- logger.success(m18n.n('port_available', port=int(port)))
else:
- raise YunohostError('port_unavailable', port=int(port))
+ # FIXME: Allow multiple values for some keys?
+ if key in ['redirected_urls', 'redirected_regex']:
+ value = yaml.load(value)
+ if any(key.startswith(word+"_") for word in ["unprotected", "protected", "skipped"]):
+ logger.warning("/!\\ Packagers! This app is still using the skipped/protected/unprotected_uris/regex settings which are now obsolete and deprecated... Instead, you should use the new helpers 'ynh_permission_{create,urls,update,delete}' and the 'visitors' group to initialize the public/private access. Check out the documentation at the bottom of yunohost.org/groups_and_permissions to learn how to use the new permission mechanism.")
+
+ app_settings[key] = value
+ _set_app_settings(app, app_settings)
+
+ # Fucking legacy permission management.
+ # We need this because app temporarily set the app as unprotected to configure it with curl...
+ if key.startswith("unprotected_") or key.startswith("skipped_") and value == "/":
+ from permission import user_permission_update
+ user_permission_update(app + ".main", add="visitors")
def app_register_url(app, domain, path):
@@ -1275,8 +1213,7 @@ def app_register_url(app, domain, path):
# We cannot change the url of an app already installed simply by changing
# the settings...
- installed = app in app_list(installed=True, raw=True).keys()
- if installed:
+ if _is_installed(app):
settings = _get_app_settings(app)
if "path" in settings.keys() and "domain" in settings.keys():
raise YunohostError('app_already_installed_cant_change_url')
@@ -1299,93 +1236,6 @@ def app_register_url(app, domain, path):
app_setting(app, 'path', value=path)
-def app_checkurl(url, app=None):
- """
- Check availability of a web path
-
- Keyword argument:
- url -- Url to check
- app -- Write domain & path to app settings for further checks
-
- """
-
- logger.error("Packagers /!\\ : 'app checkurl' is deprecated ! Please use the helper 'ynh_webpath_register' instead !")
-
- from yunohost.domain import domain_list, _normalize_domain_path
-
- if "https://" == url[:8]:
- url = url[8:]
- elif "http://" == url[:7]:
- url = url[7:]
-
- if url[-1:] != '/':
- url = url + '/'
-
- domain = url[:url.index('/')]
- path = url[url.index('/'):]
- installed = False
-
- domain, path = _normalize_domain_path(domain, path)
-
- apps_map = app_map(raw=True)
-
- if domain not in domain_list()['domains']:
- raise YunohostError('domain_unknown')
-
- if domain in apps_map:
- # Loop through apps
- for p, a in apps_map[domain].items():
- # Skip requested app checking
- if app is not None and a['id'] == app:
- installed = True
- continue
- if path == p:
- raise YunohostError('app_location_already_used', app=a["id"], path=path)
- # can't install "/a/b/" if "/a/" exists
- elif path.startswith(p) or p.startswith(path):
- raise YunohostError('app_location_install_failed', other_path=p, other_app=a['id'])
-
- if app is not None and not installed:
- app_setting(app, 'domain', value=domain)
- app_setting(app, 'path', value=path)
-
-
-def app_initdb(user, password=None, db=None, sql=None):
- """
- Create database and initialize it with optionnal attached script
-
- Keyword argument:
- db -- DB name (user unless set)
- user -- Name of the DB user
- password -- Password of the DB (generated unless set)
- sql -- Initial SQL file
-
- """
-
- logger.error("Packagers /!\\ : 'app initdb' is deprecated ! Please use the helper 'ynh_mysql_setup_db' instead !")
-
- if db is None:
- db = user
-
- return_pwd = False
- if password is None:
- password = random_password(12)
- return_pwd = True
-
- mysql_root_pwd = open('/etc/yunohost/mysql').read().rstrip()
- mysql_command = 'mysql -u root -p%s -e "CREATE DATABASE %s ; GRANT ALL PRIVILEGES ON %s.* TO \'%s\'@localhost IDENTIFIED BY \'%s\';"' % (mysql_root_pwd, db, db, user, password)
- if os.system(mysql_command) != 0:
- raise YunohostError('mysql_db_creation_failed')
- if sql is not None:
- if os.system('mysql -u %s -p%s %s < %s' % (user, password, db, sql)) != 0:
- raise YunohostError('mysql_db_init_failed')
-
- if return_pwd:
- return password
-
- logger.success(m18n.n('mysql_db_initialized'))
-
-
def app_ssowatconf():
"""
Regenerate SSOwat configuration file
@@ -1398,6 +1248,7 @@ def app_ssowatconf():
main_domain = _get_maindomain()
domains = domain_list()['domains']
+ all_permissions = user_permission_list(full=True)['permissions']
skipped_urls = []
skipped_regex = []
@@ -1408,59 +1259,97 @@ def app_ssowatconf():
redirected_regex = {main_domain + '/yunohost[\/]?$': 'https://' + main_domain + '/yunohost/sso/'}
redirected_urls = {}
- try:
- apps_list = app_list(installed=True)['apps']
- except Exception as e:
- logger.debug("cannot get installed app list because %s", e)
- apps_list = []
-
def _get_setting(settings, name):
s = settings.get(name, None)
return s.split(',') if s else []
- for app in apps_list:
- with open(APPS_SETTING_PATH + app['id'] + '/settings.yml') as f:
- app_settings = yaml.load(f)
+ for app in _installed_apps():
- if 'no_sso' in app_settings:
+ app_settings = read_yaml(APPS_SETTING_PATH + app + '/settings.yml')
+
+ if 'domain' not in app_settings:
+ continue
+ if 'path' not in app_settings:
+ continue
+
+ # This 'no_sso' settings sound redundant to not having $path defined ....
+ # At least from what I can see, all apps using it don't have a path defined ...
+ if 'no_sso' in app_settings:
+ continue
+
+ domain = app_settings['domain']
+ path = app_settings['path'].rstrip('/')
+
+ def _sanitized_absolute_url(perm_url):
+ # Nominal case : url is relative to the app's path
+ if perm_url.startswith("/"):
+ perm_domain = domain
+ perm_path = path + perm_url.rstrip("/")
+ # Otherwise, the urls starts with a domain name, like domain.tld/foo/bar
+ # We want perm_domain = domain.tld and perm_path = "/foo/bar"
+ else:
+ perm_domain, perm_path = perm_url.split("/", 1)
+ perm_path = "/" + perm_path.rstrip("/")
+
+ perm_path = perm_path if perm_path.strip() != "" else "/"
+
+ return perm_domain + perm_path
+
+ # Skipped
+ skipped_urls += [_sanitized_absolute_url(uri) for uri in _get_setting(app_settings, 'skipped_uris')]
+ skipped_regex += _get_setting(app_settings, 'skipped_regex')
+
+ # Redirected
+ redirected_urls.update(app_settings.get('redirected_urls', {}))
+ redirected_regex.update(app_settings.get('redirected_regex', {}))
+
+ # Legacy permission system using (un)protected_uris and _regex managed in app settings...
+ unprotected_urls += [_sanitized_absolute_url(uri) for uri in _get_setting(app_settings, 'unprotected_uris')]
+ protected_urls += [_sanitized_absolute_url(uri) for uri in _get_setting(app_settings, 'protected_uris')]
+ unprotected_regex += _get_setting(app_settings, 'unprotected_regex')
+ protected_regex += _get_setting(app_settings, 'protected_regex')
+
+ # New permission system
+ this_app_perms = {name: info for name, info in all_permissions.items() if name.startswith(app + ".")}
+ for perm_name, perm_info in this_app_perms.items():
+
+ # Ignore permissions for which there's no url defined
+ if not perm_info["url"]:
continue
- for item in _get_setting(app_settings, 'skipped_uris'):
- if item[-1:] == '/':
- item = item[:-1]
- skipped_urls.append(app_settings['domain'] + app_settings['path'].rstrip('/') + item)
- for item in _get_setting(app_settings, 'skipped_regex'):
- skipped_regex.append(item)
- for item in _get_setting(app_settings, 'unprotected_uris'):
- if item[-1:] == '/':
- item = item[:-1]
- unprotected_urls.append(app_settings['domain'] + app_settings['path'].rstrip('/') + item)
- for item in _get_setting(app_settings, 'unprotected_regex'):
- unprotected_regex.append(item)
- for item in _get_setting(app_settings, 'protected_uris'):
- if item[-1:] == '/':
- item = item[:-1]
- protected_urls.append(app_settings['domain'] + app_settings['path'].rstrip('/') + item)
- for item in _get_setting(app_settings, 'protected_regex'):
- protected_regex.append(item)
- if 'redirected_urls' in app_settings:
- redirected_urls.update(app_settings['redirected_urls'])
- if 'redirected_regex' in app_settings:
- redirected_regex.update(app_settings['redirected_regex'])
+ # FIXME : gotta handle regex-urls here... meh
+ url = _sanitized_absolute_url(perm_info["url"])
+ perm_info["url"] = url
+ if "visitors" in perm_info["allowed"]:
+ if url not in unprotected_urls:
+ unprotected_urls.append(url)
+
+ # Legacy stuff : we remove now protected-urls that might have been declared as unprotected earlier...
+ protected_urls = [u for u in protected_urls if u != url]
+ else:
+ if url not in protected_urls:
+ protected_urls.append(url)
+
+ # Legacy stuff : we remove now unprotected-urls / skipped-urls that might have been declared as protected earlier...
+ unprotected_urls = [u for u in unprotected_urls if u != url]
+ skipped_urls = [u for u in skipped_urls if u != url]
for domain in domains:
skipped_urls.extend([domain + '/yunohost/admin', domain + '/yunohost/api'])
- # Authorize ACME challenge url
+ # Authorize ynh remote diagnosis, ACME challenge and mail autoconfig urls
+ skipped_regex.append("^[^/]*/%.well%-known/ynh%-diagnosis/.*$")
skipped_regex.append("^[^/]*/%.well%-known/acme%-challenge/.*$")
skipped_regex.append("^[^/]*/%.well%-known/autoconfig/mail/config%-v1%.1%.xml.*$")
- permission = {}
- for a in user_permission_list()['permissions'].values():
- for p in a.values():
- if 'URL' in p:
- for u in p['URL']:
- permission[u] = p['allowed_users']
+
+ permissions_per_url = {}
+ for perm_name, perm_info in all_permissions.items():
+ # Ignore permissions for which there's no url defined
+ if not perm_info["url"]:
+ continue
+ permissions_per_url[perm_info["url"]] = perm_info['corresponding_users']
+
conf_dict = {
'portal_domain': main_domain,
@@ -1482,7 +1371,7 @@ def app_ssowatconf():
'redirected_regex': redirected_regex,
'users': {username: app_map(user=username)
for username in user_list()['users'].keys()},
- 'permission': permission,
+ 'permissions': permissions_per_url,
}
with open('/etc/ssowat/conf.json', 'w+') as f:
@@ -1716,6 +1605,7 @@ def app_config_apply(operation_logger, app, args):
logger.success("Config updated as expected")
return {
+ "app": app,
"logs": operation_logger.success(),
}
@@ -1728,8 +1618,7 @@ def _get_all_installed_apps_id():
* ...'
"""
- all_apps_ids = [x["id"] for x in app_list(installed=True)["apps"]]
- all_apps_ids = sorted(all_apps_ids)
+ all_apps_ids = sorted(_installed_apps())
all_apps_ids_formatted = "\n * ".join(all_apps_ids)
all_apps_ids_formatted = "\n * " + all_apps_ids_formatted
@@ -1902,8 +1791,7 @@ def _get_app_config_panel(app_id):
"panel": [],
}
- panels = filter(lambda (key, value): key not in ("name", "version")
- and isinstance(value, OrderedDict),
+ panels = filter(lambda (key, value): key not in ("name", "version") and isinstance(value, OrderedDict),
toml_config_panel.items())
for key, value in panels:
@@ -1913,8 +1801,7 @@ def _get_app_config_panel(app_id):
"sections": [],
}
- sections = filter(lambda (k, v): k not in ("name",)
- and isinstance(v, OrderedDict),
+ sections = filter(lambda (k, v): k not in ("name",) and isinstance(v, OrderedDict),
value.items())
for section_key, section_value in sections:
@@ -1924,8 +1811,7 @@ def _get_app_config_panel(app_id):
"options": [],
}
- options = filter(lambda (k, v): k not in ("name",)
- and isinstance(v, OrderedDict),
+ options = filter(lambda (k, v): k not in ("name",) and isinstance(v, OrderedDict),
section_value.items())
for option_key, option_value in options:
@@ -1965,7 +1851,7 @@ def _get_app_settings(app_id):
if app_id == settings['id']:
return settings
except (IOError, TypeError, KeyError):
- logger.exception(m18n.n('app_not_correctly_installed',
+ logger.error(m18n.n('app_not_correctly_installed',
app=app_id))
return {}
@@ -1984,51 +1870,6 @@ def _set_app_settings(app_id, settings):
yaml.safe_dump(settings, f, default_flow_style=False)
-def _get_app_status(app_id, format_date=False):
- """
- Get app status or create it if needed
-
- Keyword arguments:
- app_id -- The app id
- format_date -- Format date fields
-
- """
- app_setting_path = APPS_SETTING_PATH + app_id
- if not os.path.isdir(app_setting_path):
- raise YunohostError('app_unknown')
- status = {}
-
- regen_status = True
- try:
- with open(app_setting_path + '/status.json') as f:
- status = json.loads(str(f.read()))
- regen_status = False
- except IOError:
- logger.debug("status file not found for '%s'", app_id,
- exc_info=1)
- except Exception as e:
- logger.warning("could not open or decode %s : %s ... regenerating.", app_setting_path + '/status.json', str(e))
-
- if regen_status:
- # Create app status
- status = {
- 'installed_at': app_setting(app_id, 'install_time'),
- 'upgraded_at': app_setting(app_id, 'update_time'),
- 'remote': {'type': None},
- }
- with open(app_setting_path + '/status.json', 'w+') as f:
- json.dump(status, f)
-
- if format_date:
- for f in ['installed_at', 'upgraded_at']:
- v = status.get(f, None)
- if not v:
- status[f] = '-'
- else:
- status[f] = datetime.utcfromtimestamp(v)
- return status
-
-
def _extract_app_from_file(path, remove=False):
"""
Unzip or untar application tarball in APP_TMP_FOLDER, or copy it from a directory
@@ -2216,7 +2057,7 @@ def _get_manifest_of_app(path):
elif os.path.exists(os.path.join(path, "manifest.json")):
return read_json(os.path.join(path, "manifest.json"))
else:
- return None
+ raise YunohostError("There doesn't seem to be any manifest file in %s ... It looks like an app was not correctly installed/removed." % path, raw_msg=True)
def _get_git_last_commit_hash(repository, reference='HEAD'):
@@ -2316,17 +2157,16 @@ def _fetch_app_from_git(app):
else:
manifest['remote']['revision'] = revision
else:
- app_dict = app_list(raw=True)
+ app_dict = _load_apps_catalog()["apps"]
- if app in app_dict:
- app_info = app_dict[app]
- app_info['manifest']['lastUpdate'] = app_info['lastUpdate']
- manifest = app_info['manifest']
- else:
+ if app not in app_dict:
raise YunohostError('app_unknown')
-
- if 'git' not in app_info:
+ elif 'git' not in app_dict[app]:
raise YunohostError('app_unsupported_remote_type')
+
+ app_info = app_dict[app]
+ app_info['manifest']['lastUpdate'] = app_info['lastUpdate']
+ manifest = app_info['manifest']
url = app_info['git']['url']
if 'github.com' in url:
@@ -2424,6 +2264,10 @@ def _is_installed(app):
return os.path.isdir(APPS_SETTING_PATH + app)
+def _installed_apps():
+ return os.listdir(APPS_SETTING_PATH)
+
+
def _value_for_locale(values):
"""
Return proper value for current locale
@@ -2461,38 +2305,14 @@ def _check_manifest_requirements(manifest, app_instance_name):
"""Check if required packages are met from the manifest"""
requirements = manifest.get('requirements', dict())
- # FIXME: Deprecate min_version key
- if 'min_version' in manifest:
- requirements['yunohost'] = '>> {0}'.format(manifest['min_version'])
- logger.debug("the manifest key 'min_version' is deprecated, "
- "use 'requirements' instead.")
-
- # Validate multi-instance app
- if is_true(manifest.get('multi_instance', False)):
- # Handle backward-incompatible change introduced in yunohost >= 2.3.6
- # See https://github.com/YunoHost/issues/issues/156
- yunohost_req = requirements.get('yunohost', None)
- if (not yunohost_req or
- not packages.SpecifierSet(yunohost_req) & '>= 2.3.6'):
- raise YunohostError('{0}{1}'.format(
- m18n.g('colon', m18n.n('app_incompatible'), app=app_instance_name),
- m18n.n('app_package_need_update', app=app_instance_name)))
- elif not requirements:
+ if not requirements:
return
logger.debug(m18n.n('app_requirements_checking', app=app_instance_name))
- # Retrieve versions of each required package
- try:
- versions = packages.get_installed_version(
- *requirements.keys(), strict=True, as_dict=True)
- except packages.PackageException as e:
- raise YunohostError('app_requirements_failed', error=str(e), app=app_instance_name)
-
# Iterate over requirements
for pkgname, spec in requirements.items():
- version = versions[pkgname]
- if version not in packages.SpecifierSet(spec):
+ if not packages.meets_version_specifier(pkgname, spec):
raise YunohostError('app_requirements_unmeet',
pkgname=pkgname, version=version,
spec=spec, app=app_instance_name)
@@ -2547,8 +2367,7 @@ def _parse_args_for_action(action, args={}):
def _parse_args_in_yunohost_format(args, action_args):
"""Parse arguments store in either manifest.json or actions.json
"""
- from yunohost.domain import (domain_list, _get_maindomain,
- _get_conflicting_apps, _normalize_domain_path)
+ from yunohost.domain import domain_list, _get_maindomain
from yunohost.user import user_info, user_list
args_dict = OrderedDict()
@@ -2641,10 +2460,8 @@ def _parse_args_in_yunohost_format(args, action_args):
if arg_value not in domain_list()['domains']:
raise YunohostError('app_argument_invalid', name=arg_name, error=m18n.n('domain_unknown'))
elif arg_type == 'user':
- try:
- user_info(arg_value)
- except YunohostError as e:
- raise YunohostError('app_argument_invalid', name=arg_name, error=e)
+ if not arg_value in user_list()["users"].keys():
+ raise YunohostError('app_argument_invalid', name=arg_name, error=m18n.n('user_unknown', user=arg_value))
elif arg_type == 'app':
if not _is_installed(arg_value):
raise YunohostError('app_argument_invalid', name=arg_name, error=m18n.n('app_unknown'))
@@ -2666,13 +2483,18 @@ def _parse_args_in_yunohost_format(args, action_args):
assert_password_is_strong_enough('user', arg_value)
args_dict[arg_name] = (arg_value, arg_type)
- # END loop over action_args...
+ return args_dict
+
+
+def _validate_and_normalize_webpath(manifest, args_dict, app_folder):
+
+ from yunohost.domain import _get_conflicting_apps, _normalize_domain_path
# If there's only one "domain" and "path", validate that domain/path
# is an available url and normalize the path.
- domain_args = [ (name, value[0]) for name, value in args_dict.items() if value[1] == "domain" ]
- path_args = [ (name, value[0]) for name, value in args_dict.items() if value[1] == "path" ]
+ domain_args = [(name, value[0]) for name, value in args_dict.items() if value[1] == "domain"]
+ path_args = [(name, value[0]) for name, value in args_dict.items() if value[1] == "path"]
if len(domain_args) == 1 and len(path_args) == 1:
@@ -2698,7 +2520,25 @@ def _parse_args_in_yunohost_format(args, action_args):
# standard path format to deal with no matter what the user inputted)
args_dict[path_args[0][0]] = (path, "path")
- return args_dict
+ # This is likely to be a full-domain app...
+ elif len(domain_args) == 1 and len(path_args) == 0:
+
+ # Confirm that this is a full-domain app This should cover most cases
+ # ... though anyway the proper solution is to implement some mechanism
+ # in the manifest for app to declare that they require a full domain
+ # (among other thing) so that we can dynamically check/display this
+ # requirement on the webadmin form and not miserably fail at submit time
+
+ # Full-domain apps typically declare something like path_url="/" or path=/
+ # and use ynh_webpath_register or yunohost_app_checkurl inside the install script
+ install_script_content = open(os.path.join(app_folder, 'scripts/install')).read()
+
+ if re.search(r"\npath(_url)?=[\"']?/[\"']?\n", install_script_content) \
+ and re.search(r"(ynh_webpath_register|yunohost app checkurl)", install_script_content):
+
+ domain = domain_args[0][1]
+ if _get_conflicting_apps(domain, "/"):
+ raise YunohostError('app_full_domain_unavailable', domain=domain)
def _make_environment_dict(args_dict, prefix="APP_ARG_"):
@@ -2745,151 +2585,167 @@ def _parse_app_instance_name(app_instance_name):
return (appid, app_instance_nb)
-def _using_legacy_appslist_system():
+#
+# ############################### #
+# Applications list management #
+# ############################### #
+#
+
+
+def _initialize_apps_catalog_system():
"""
- Return True if we're using the old fetchlist scheme.
- This is determined by the presence of some cron job yunohost-applist-foo
+ This function is meant to intialize the apps_catalog system with YunoHost's default app catalog.
+
+ It also creates the cron job that will update the list every day
"""
- return glob.glob("/etc/cron.d/yunohost-applist-*") != []
-
-
-def _migrate_appslist_system():
- """
- Migrate from the legacy fetchlist system to the new one
- """
- legacy_crons = glob.glob("/etc/cron.d/yunohost-applist-*")
-
- for cron_path in legacy_crons:
- appslist_name = os.path.basename(cron_path).replace("yunohost-applist-", "")
- logger.debug(m18n.n('appslist_migrating', appslist=appslist_name))
-
- # Parse appslist url in cron
- cron_file_content = open(cron_path).read().strip()
- appslist_url_parse = re.search("-u (https?://[^ ]+)", cron_file_content)
-
- # Abort if we did not find an url
- if not appslist_url_parse or not appslist_url_parse.groups():
- # Bkp the old cron job somewhere else
- bkp_file = "/etc/yunohost/%s.oldlist.bkp" % appslist_name
- os.rename(cron_path, bkp_file)
- # Notice the user
- logger.warning(m18n.n('appslist_could_not_migrate',
- appslist=appslist_name,
- bkp_file=bkp_file))
- # Otherwise, register the list and remove the legacy cron
- else:
- appslist_url = appslist_url_parse.groups()[0]
- try:
- _register_new_appslist(appslist_url, appslist_name)
- # Might get an exception if two legacy cron jobs conflict
- # in terms of url...
- except Exception as e:
- logger.error(str(e))
- # Bkp the old cron job somewhere else
- bkp_file = "/etc/yunohost/%s.oldlist.bkp" % appslist_name
- os.rename(cron_path, bkp_file)
- # Notice the user
- logger.warning(m18n.n('appslist_could_not_migrate',
- appslist=appslist_name,
- bkp_file=bkp_file))
- else:
- os.remove(cron_path)
-
-
-def _install_appslist_fetch_cron():
-
- cron_job_file = "/etc/cron.daily/yunohost-fetch-appslists"
-
- logger.debug("Installing appslist fetch cron job")
+ default_apps_catalog_list = [{"id": "default", "url": APPS_CATALOG_DEFAULT_URL}]
cron_job = []
cron_job.append("#!/bin/bash")
# We add a random delay between 0 and 60 min to avoid every instance fetching
- # the appslist at the same time every night
+ # the apps catalog at the same time every night
cron_job.append("(sleep $((RANDOM%3600));")
- cron_job.append("yunohost app fetchlist > /dev/null 2>&1) &")
-
- with open(cron_job_file, "w") as f:
- f.write('\n'.join(cron_job))
-
- _set_permissions(cron_job_file, "root", "root", 0o755)
-
-
-# FIXME - Duplicate from certificate.py, should be moved into a common helper
-# thing...
-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 _read_appslist_list():
- """
- Read the json corresponding to the list of appslists
- """
-
- # If file does not exists yet, return empty dict
- if not os.path.exists(APPSLISTS_JSON):
- return {}
-
- # Read file content
- with open(APPSLISTS_JSON, "r") as f:
- appslists_json = f.read()
-
- # Parse json, throw exception if what we got from file is not a valid json
+ cron_job.append("yunohost tools update --apps > /dev/null) &")
try:
- appslists = json.loads(appslists_json)
- except ValueError:
- raise YunohostError('appslist_corrupted_json', filename=APPSLISTS_JSON)
+ logger.debug("Initializing apps catalog system with YunoHost's default app list")
+ write_to_yaml(APPS_CATALOG_CONF, default_apps_catalog_list)
- return appslists
-
-
-def _write_appslist_list(appslist_lists):
- """
- Update the json containing list of appslists
- """
-
- # Write appslist list
- try:
- with open(APPSLISTS_JSON, "w") as f:
- json.dump(appslist_lists, f)
+ logger.debug("Installing apps catalog fetch daily cron job")
+ write_to_file(APPS_CATALOG_CRON_PATH, '\n'.join(cron_job))
+ chown(APPS_CATALOG_CRON_PATH, uid="root", gid="root")
+ chmod(APPS_CATALOG_CRON_PATH, 0o755)
except Exception as e:
- raise YunohostError("Error while writing list of appslist %s: %s" %
- (APPSLISTS_JSON, str(e)), raw_msg=True)
+ raise YunohostError("Could not initialize the apps catalog system... : %s" % str(e))
+
+ logger.success(m18n.n("apps_catalog_init_success"))
-def _register_new_appslist(url, name):
+def _read_apps_catalog_list():
"""
- Add a new appslist to be fetched regularly.
- Raise an exception if url or name conflicts with an existing list.
+ Read the json corresponding to the list of apps catalogs
"""
- appslist_list = _read_appslist_list()
+ # Legacy code - can be removed after moving to buster (if the migration got merged before buster)
+ if os.path.exists('/etc/yunohost/appslists.json'):
+ from yunohost.tools import _get_migration_by_name
+ migration = _get_migration_by_name("futureproof_apps_catalog_system")
+ migration.run()
- # Check if name conflicts with an existing list
- if name in appslist_list:
- raise YunohostError('appslist_name_already_tracked', name=name)
+ try:
+ list_ = read_yaml(APPS_CATALOG_CONF)
+ # Support the case where file exists but is empty
+ # by returning [] if list_ is None
+ return list_ if list_ else []
+ except Exception as e:
+ raise YunohostError("Could not read the apps_catalog list ... : %s" % str(e))
- # Check if url conflicts with an existing list
- known_appslist_urls = [appslist["url"] for _, appslist in appslist_list.items()]
- if url in known_appslist_urls:
- raise YunohostError('appslist_url_already_tracked', url=url)
+def _actual_apps_catalog_api_url(base_url):
- logger.debug("Registering new appslist %s at %s" % (name, url))
+ return "{base_url}/v{version}/apps.json".format(base_url=base_url, version=APPS_CATALOG_API_VERSION)
- appslist_list[name] = {
- "url": url,
- "lastUpdate": None
+
+def _update_apps_catalog():
+ """
+ Fetches the json for each apps_catalog and update the cache
+
+ apps_catalog_list is for example :
+ [ {"id": "default", "url": "https://app.yunohost.org/default/"} ]
+
+ Then for each apps_catalog, the actual json URL to be fetched is like :
+ https://app.yunohost.org/default/vX/apps.json
+
+ And store it in :
+ /var/cache/yunohost/repo/default.json
+ """
+
+ apps_catalog_list = _read_apps_catalog_list()
+
+ logger.info(m18n.n("apps_catalog_updating"))
+
+ # Create cache folder if needed
+ if not os.path.exists(APPS_CATALOG_CACHE):
+ logger.debug("Initialize folder for apps catalog cache")
+ mkdir(APPS_CATALOG_CACHE, mode=0o750, parents=True, uid='root')
+
+ for apps_catalog in apps_catalog_list:
+ apps_catalog_id = apps_catalog["id"]
+ actual_api_url = _actual_apps_catalog_api_url(apps_catalog["url"])
+
+ # Fetch the json
+ try:
+ apps_catalog_content = download_json(actual_api_url)
+ except Exception as e:
+ raise YunohostError("apps_catalog_failed_to_download", apps_catalog=apps_catalog_id, error=str(e))
+
+ # Remember the apps_catalog api version for later
+ apps_catalog_content["from_api_version"] = APPS_CATALOG_API_VERSION
+
+ # Save the apps_catalog data in the cache
+ cache_file = "{cache_folder}/{list}.json".format(cache_folder=APPS_CATALOG_CACHE, list=apps_catalog_id)
+ try:
+ write_to_json(cache_file, apps_catalog_content)
+ except Exception as e:
+ raise YunohostError("Unable to write cache data for %s apps_catalog : %s" % (apps_catalog_id, str(e)))
+
+ logger.success(m18n.n("apps_catalog_update_success"))
+
+
+def _load_apps_catalog():
+ """
+ Read all the apps catalog cache files and build a single dict (merged_catalog)
+ corresponding to all known apps and categories
+ """
+
+ merged_catalog = {
+ "apps": {},
+ "categories": []
}
- _write_appslist_list(appslist_list)
+ for apps_catalog_id in [L["id"] for L in _read_apps_catalog_list()]:
- _install_appslist_fetch_cron()
+ # Let's load the json from cache for this catalog
+ cache_file = "{cache_folder}/{list}.json".format(cache_folder=APPS_CATALOG_CACHE, list=apps_catalog_id)
+
+ try:
+ apps_catalog_content = read_json(cache_file) if os.path.exists(cache_file) else None
+ except Exception as e:
+ raise ("Unable to read cache for apps_catalog %s : %s" % (apps_catalog_id, str(e)))
+
+ # Check that the version of the data matches version ....
+ # ... otherwise it means we updated yunohost in the meantime
+ # and need to update the cache for everything to be consistent
+ if not apps_catalog_content or apps_catalog_content.get("from_api_version") != APPS_CATALOG_API_VERSION:
+ logger.info(m18n.n("apps_catalog_obsolete_cache"))
+ _update_apps_catalog()
+ apps_catalog_content = read_json(cache_file)
+
+ del apps_catalog_content["from_api_version"]
+
+ # Add apps from this catalog to the output
+ for app, info in apps_catalog_content["apps"].items():
+
+ # (N.B. : there's a small edge case where multiple apps catalog could be listing the same apps ...
+ # in which case we keep only the first one found)
+ if app in merged_catalog["apps"]:
+ logger.warning("Duplicate app %s found between apps catalog %s and %s"
+ % (app, apps_catalog_id, merged_catalog["apps"][app]['repository']))
+ continue
+
+ info['repository'] = apps_catalog_id
+ merged_catalog["apps"][app] = info
+
+ # Annnnd categories
+ merged_catalog["categories"] += apps_catalog_content["categories"]
+
+ return merged_catalog
+
+#
+# ############################### #
+# Small utilities #
+# ############################### #
+#
def is_true(arg):
@@ -2906,11 +2762,7 @@ def is_true(arg):
if isinstance(arg, bool):
return arg
elif isinstance(arg, basestring):
- true_list = ['yes', 'Yes', 'true', 'True']
- for string in true_list:
- if arg == string:
- return True
- return False
+ return arg.lower() in ['yes', 'true', 'on']
else:
logger.debug('arg should be a boolean or a string, got %r', arg)
return True if arg else False
@@ -2933,16 +2785,12 @@ def random_password(length=8):
def unstable_apps():
- raw_app_installed = app_list(installed=True, raw=True)
output = []
- for app, infos in raw_app_installed.items():
+ for infos in app_list(full=True)["apps"]:
- repo = infos.get("repository", None)
- state = infos.get("state", None)
-
- if repo is None or state in ["inprogress", "notworking"]:
- output.append(app)
+ if not infos.get("from_catalog") or infos.get("from_catalog").get("state") in ["inprogress", "notworking"]:
+ output.append(infos["id"])
return output
@@ -2973,7 +2821,7 @@ def _assert_system_is_sane_for_app(manifest, when):
services.append("fail2ban")
# List services currently down and raise an exception if any are found
- faulty_services = [s for s in services if service_status(s)["active"] != "active"]
+ faulty_services = [s for s in services if service_status(s)["status"] != "running"]
if faulty_services:
if when == "pre":
raise YunohostError('app_action_cannot_be_ran_because_required_services_down',
@@ -3009,3 +2857,76 @@ def _patch_php5(app_folder):
"-e 's@php5@php7.0@g' " \
"%s" % filename
os.system(c)
+
+def _patch_legacy_helpers(app_folder):
+
+ files_to_patch = []
+ files_to_patch.extend(glob.glob("%s/scripts/*" % app_folder))
+ files_to_patch.extend(glob.glob("%s/scripts/.*" % app_folder))
+
+ stuff_to_replace = {
+ # Replace
+ # sudo yunohost app initdb $db_user -p $db_pwd
+ # by
+ # ynh_mysql_setup_db --db_user=$db_user --db_name=$db_user --db_pwd=$db_pwd
+ "yunohost app initdb": (
+ r"(sudo )?yunohost app initdb \"?(\$\{?\w+\}?)\"?\s+-p\s\"?(\$\{?\w+\}?)\"?",
+ r"ynh_mysql_setup_db --db_user=\2 --db_name=\2 --db_pwd=\3"),
+ # Replace
+ # sudo yunohost app checkport whaterver
+ # by
+ # ynh_port_available whatever
+ "yunohost app checkport": (
+ r"(sudo )?yunohost app checkport",
+ r"ynh_port_available"),
+ # We can't migrate easily port-available
+ # .. but at the time of writing this code, only two non-working apps are using it.
+ "yunohost tools port-available": (None, None),
+ # Replace
+ # yunohost app checkurl "${domain}${path_url}" -a "${app}"
+ # by
+ # ynh_webpath_register --app=${app} --domain=${domain} --path_url=${path_url}
+ "yunohost app checkurl": (
+ r"(sudo )?yunohost app checkurl \"?(\$\{?\w+\}?)\/?(\$\{?\w+\}?)\"?\s+-a\s\"?(\$\{?\w+\}?)\"?",
+ r"ynh_webpath_register --app=\4 --domain=\2 --path_url=\3"),
+ }
+
+ stuff_to_replace_compiled = {h: (re.compile(r[0]), r[1]) if r[0] else (None,None) for h, r in stuff_to_replace.items()}
+
+ for filename in files_to_patch:
+
+ # Ignore non-regular files
+ if not os.path.isfile(filename):
+ continue
+
+ content = read_file(filename)
+ replaced_stuff = False
+
+ for helper, regexes in stuff_to_replace_compiled.items():
+ pattern, replace = regexes
+ # If helper is used, attempt to patch the file
+ if helper in content and pattern != "":
+ content = pattern.sub(replace, content)
+ replaced_stuff = True
+
+ # If the helpert is *still* in the content, it means that we
+ # couldn't patch the deprecated helper in the previous lines. In
+ # that case, abort the install or whichever step is performed
+ if helper in content:
+ raise YunohostError("This app is likely pretty old and uses deprecated / outdated helpers that can't be migrated easily. It can't be installed anymore.")
+
+ if replaced_stuff:
+
+ # Check the app do load the helper
+ # If it doesn't, add the instruction ourselve (making sure it's after the #!/bin/bash if it's there...
+ if filename.split("/")[-1] in ["install", "remove", "upgrade", "backup", "restore"]:
+ source_helpers = "source /usr/share/yunohost/helpers"
+ if source_helpers not in content:
+ content.replace("#!/bin/bash", "#!/bin/bash\n"+source_helpers)
+ if source_helpers not in content:
+ content = source_helpers + "\n" + content
+
+ # Actually write the new content in the file
+ write_to_file(filename, content)
+ # And complain about those damn deprecated helpers
+ logger.error("/!\ Packagers ! This app uses a very old deprecated helpers ... Yunohost automatically patched the helpers to use the new recommended practice, but please do consider fixing the upstream code right now ...")
diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py
index bd5d5750d..51aa7d6cd 100644
--- a/src/yunohost/backup.py
+++ b/src/yunohost/backup.py
@@ -35,24 +35,24 @@ import tempfile
from datetime import datetime
from glob import glob
from collections import OrderedDict
+from functools import reduce
-from moulinette import msignals, m18n
-from yunohost.utils.error import YunohostError
+from moulinette import msignals, m18n, msettings
from moulinette.utils import filesystem
from moulinette.utils.log import getActionLogger
-from moulinette.utils.filesystem import read_file, mkdir
+from moulinette.utils.filesystem import read_file, mkdir, write_to_yaml, read_yaml
from yunohost.app import (
- app_info, _is_installed, _parse_app_instance_name, _patch_php5
+ app_info, _is_installed, _parse_app_instance_name, _patch_php5, dump_app_log_extract_for_debugging, _patch_legacy_helpers
)
from yunohost.hook import (
hook_list, hook_info, hook_callback, hook_exec, CUSTOM_HOOK_FOLDER
)
-from yunohost.monitor import binary_to_human
from yunohost.tools import tools_postinstall
from yunohost.regenconf import regen_conf
from yunohost.log import OperationLogger
-from functools import reduce
+from yunohost.utils.error import YunohostError
+from yunohost.utils.packages import ynh_packages_version
BACKUP_PATH = '/home/yunohost.backup'
ARCHIVES_PATH = '%s/archives' % BACKUP_PATH
@@ -283,7 +283,8 @@ class BackupManager():
'size': self.size,
'size_details': self.size_details,
'apps': self.apps_return,
- 'system': self.system_return
+ 'system': self.system_return,
+ 'from_yunohost_version': ynh_packages_version()["yunohost"]["version"]
}
@property
@@ -602,10 +603,10 @@ class BackupManager():
env=env_dict,
chdir=self.work_dir)
- ret_succeed = {hook: {path:result["state"] for path, result in infos.items()}
+ ret_succeed = {hook: [path for path, result in infos.items() if result["state"] == "succeed"]
for hook, infos in ret.items()
if any(result["state"] == "succeed" for result in infos.values())}
- ret_failed = {hook: {path:result["state"] for path, result in infos.items.items()}
+ ret_failed = {hook: [path for path, result in infos.items() if result["state"] == "failed"]
for hook, infos in ret.items()
if any(result["state"] == "failed" for result in infos.values())}
@@ -677,6 +678,8 @@ class BackupManager():
backup_app_failed -- Raised at the end if the app backup script
execution failed
"""
+ from yunohost.permission import user_permission_list
+
app_setting_path = os.path.join('/etc/yunohost/apps/', app)
# Prepare environment
@@ -704,8 +707,9 @@ class BackupManager():
# backup permissions
logger.debug(m18n.n('backup_permission', app=app))
- ldap_url = "ldap:///dc=yunohost,dc=org???(&(objectClass=permissionYnh)(cn=*.%s))" % app
- os.system("slapcat -b dc=yunohost,dc=org -H '%s' -l '%s/permission.ldif'" % (ldap_url, settings_dir))
+ permissions = user_permission_list(full=True)["permissions"]
+ this_app_permissions = {name: infos for name, infos in permissions.items() if name.startswith(app + ".")}
+ write_to_yaml("%s/permissions.yml" % settings_dir, this_app_permissions)
except:
abs_tmp_app_dir = os.path.join(self.work_dir, 'apps/', app)
@@ -868,7 +872,7 @@ class RestoreManager():
Read the info file from inside an archive
Exceptions:
- backup_invalid_archive -- Raised if we can't read the info
+ backup_archive_cant_retrieve_info_json -- Raised if we can't read the info
"""
# Retrieve backup info
info_file = os.path.join(self.work_dir, "info.json")
@@ -881,7 +885,7 @@ class RestoreManager():
self.info["system"] = self.info["hooks"]
except IOError:
logger.debug("unable to load '%s'", info_file, exc_info=1)
- raise YunohostError('backup_invalid_archive')
+ raise YunohostError('backup_archive_cant_retrieve_info_json', archive=self.archive_path)
else:
logger.debug("restoring from backup '%s' created on %s", self.name,
datetime.utcfromtimestamp(self.info['created_at']))
@@ -889,10 +893,6 @@ class RestoreManager():
def _postinstall_if_needed(self):
"""
Post install yunohost if needed
-
- Exceptions:
- backup_invalid_archive -- Raised if the current_host isn't in the
- archive
"""
# Check if YunoHost is installed
if not os.path.isfile('/etc/yunohost/installed'):
@@ -904,7 +904,7 @@ class RestoreManager():
logger.debug("unable to retrieve current_host from the backup",
exc_info=1)
# FIXME include the current_host by default ?
- raise YunohostError('backup_invalid_archive')
+ raise YunohostError("The main domain name cannot be retrieved from inside the archive, and is needed to perform the postinstall", raw_msg=True)
logger.debug("executing the post-install...")
tools_postinstall(domain, 'Yunohost', True)
@@ -919,7 +919,7 @@ class RestoreManager():
successfull_apps = self.targets.list("apps", include=["Success", "Warning"])
- permission_sync_to_user(force=False)
+ permission_sync_to_user()
if os.path.ismount(self.work_dir):
ret = subprocess.call(["umount", self.work_dir])
@@ -1131,6 +1131,8 @@ class RestoreManager():
self._restore_system()
self._restore_apps()
+ except Exception as e:
+ raise YunohostError("The following critical error happened during restoration: %s" % e)
finally:
self.clean()
@@ -1144,21 +1146,18 @@ class RestoreManager():
if not os.path.isfile(backup_csv):
return
- try:
- contains_php5 = False
- with open(backup_csv) as csvfile:
- reader = csv.DictReader(csvfile, fieldnames=['source', 'dest'])
- newlines = []
- for row in reader:
- if 'php5' in row['source']:
- contains_php5 = True
- row['source'] = row['source'].replace('/etc/php5', '/etc/php/7.0') \
- .replace('/var/run/php5-fpm', '/var/run/php/php7.0-fpm') \
- .replace('php5', 'php7')
+ contains_php5 = False
+ with open(backup_csv) as csvfile:
+ reader = csv.DictReader(csvfile, fieldnames=['source', 'dest'])
+ newlines = []
+ for row in reader:
+ if 'php5' in row['source']:
+ contains_php5 = True
+ row['source'] = row['source'].replace('/etc/php5', '/etc/php/7.0') \
+ .replace('/var/run/php5-fpm', '/var/run/php/php7.0-fpm') \
+ .replace('php5', 'php7')
- newlines.append(row)
- except (IOError, OSError, csv.Error) as e:
- raise YunohostError('error_reading_file', file=backup_csv, error=str(e))
+ newlines.append(row)
if not contains_php5:
return
@@ -1183,18 +1182,12 @@ class RestoreManager():
if system_targets == []:
return
- from yunohost.utils.ldap import _get_ldap_interface
- ldap = _get_ldap_interface()
+ from yunohost.user import user_group_list
+ from yunohost.permission import permission_create, permission_delete, user_permission_update, user_permission_list, permission_sync_to_user
# Backup old permission for apps
# We need to do that because in case of an app is installed we can't remove the permission for this app
- old_apps_permission = []
- try:
- old_apps_permission = ldap.search('ou=permission,dc=yunohost,dc=org',
- '(&(objectClass=permissionYnh)(!(cn=main.mail))(!(cn=main.metronome))(!(cn=main.sftp)))',
- ['cn', 'objectClass', 'groupPermission', 'URL', 'gidNumber'])
- except:
- logger.info(m18n.n('apps_permission_not_found'))
+ old_apps_permission = user_permission_list(ignore_system_perms=True, full=True)["permissions"]
# Start register change on system
operation_logger = OperationLogger('backup_restore_system')
@@ -1232,12 +1225,11 @@ class RestoreManager():
regen_conf()
- # Check if we need to do the migration 0009 : setup group and permission
+ # Check that at least a group exists (all_users) to know if we need to
+ # do the migration 0011 : setup group and permission
+ #
# Legacy code
- result = ldap.search('ou=groups,dc=yunohost,dc=org',
- '(&(objectclass=groupOfNamesYnh)(cn=all_users))',
- ['cn'])
- if not result:
+ if not "all_users" in user_group_list()["groups"].keys():
from yunohost.tools import _get_migration_by_name
setup_group_permission = _get_migration_by_name("setup_group_permission")
# Update LDAP schema restart slapd
@@ -1245,24 +1237,17 @@ class RestoreManager():
regen_conf(names=['slapd'], force=True)
setup_group_permission.migrate_LDAP_db()
- # Remove all permission for all app which sill in the LDAP
- for per in ldap.search('ou=permission,dc=yunohost,dc=org',
- '(&(objectClass=permissionYnh)(!(cn=main.mail))(!(cn=main.metronome))(!(cn=main.sftp)))',
- ['cn']):
- if not ldap.remove('cn=%s,ou=permission' % per['cn'][0]):
- raise YunohostError('permission_deletion_failed',
- permission=per['cn'][0].split('.')[0],
- app=per['cn'][0].split('.')[1])
+ # Remove all permission for all app which is still in the LDAP
+ for permission_name in user_permission_list(ignore_system_perms=True)["permissions"].keys():
+ permission_delete(permission_name, force=True, sync_perm=False)
# Restore permission for the app which is installed
- for per in old_apps_permission:
- try:
- permission_name, app_name = per['cn'][0].split('.')
- except:
- logger.warning(m18n.n('permission_name_not_valid', permission=per['cn'][0]))
+ for permission_name, permission_infos in old_apps_permission.items():
+ app_name = permission_name.split(".")[0]
if _is_installed(app_name):
- if not ldap.add('cn=%s,ou=permission' % per['cn'][0], per):
- raise YunohostError('apps_permission_restoration_failed', permission=permission_name, app=app_name)
+ permission_create(permission_name, url=permission_infos["url"], allowed=permission_infos["allowed"], sync_perm=False)
+
+ permission_sync_to_user()
def _restore_apps(self):
@@ -1271,7 +1256,6 @@ class RestoreManager():
apps_targets = self.targets.list("apps", exclude=["Skipped"])
for app in apps_targets:
- print(app)
self._restore_app(app)
def _restore_app(self, app_instance_name):
@@ -1301,11 +1285,8 @@ class RestoreManager():
name already exists
restore_app_failed -- Raised if the restore bash script failed
"""
- from moulinette.utils.filesystem import read_ldif
from yunohost.user import user_group_list
- from yunohost.permission import permission_remove
- from yunohost.utils.ldap import _get_ldap_interface
- ldap = _get_ldap_interface()
+ from yunohost.permission import permission_create, permission_delete, user_permission_list, user_permission_update, permission_sync_to_user
def copytree(src, dst, symlinks=False, ignore=None):
for item in os.listdir(src):
@@ -1335,6 +1316,9 @@ class RestoreManager():
app_settings_in_archive = os.path.join(app_dir_in_archive, 'settings')
app_scripts_in_archive = os.path.join(app_settings_in_archive, 'scripts')
+ # Attempt to patch legacy helpers...
+ _patch_legacy_helpers(app_settings_in_archive)
+
# Apply dirty patch to make php5 apps compatible with php7
_patch_php5(app_settings_in_archive)
@@ -1370,22 +1354,27 @@ class RestoreManager():
restore_script = os.path.join(tmp_folder_for_app_restore, 'restore')
# Restore permissions
- if os.path.isfile(app_settings_in_archive + '/permission.ldif'):
- filtred_entries = ['entryUUID', 'creatorsName', 'createTimestamp', 'entryCSN', 'structuralObjectClass',
- 'modifiersName', 'modifyTimestamp', 'inheritPermission', 'memberUid']
- entries = read_ldif('%s/permission.ldif' % app_settings_in_archive, filtred_entries)
- group_list = user_group_list(['cn'])['groups']
- for dn, entry in entries:
- # Remove the group which has been removed
- for group in entry['groupPermission']:
- group_name = group.split(',')[0].split('=')[1]
- if group_name not in group_list:
- entry['groupPermission'].remove(group)
- if not ldap.add('cn=%s,ou=permission' % entry['cn'][0], entry):
- raise YunohostError('apps_permission_restoration_failed',
- permission=entry['cn'][0].split('.')[0],
- app=entry['cn'][0].split('.')[1])
+ if os.path.isfile('%s/permissions.yml' % app_settings_new_path):
+
+ permissions = read_yaml('%s/permissions.yml' % app_settings_new_path)
+ existing_groups = user_group_list()['groups']
+
+ for permission_name, permission_infos in permissions.items():
+
+ if "allowed" not in permission_infos:
+ logger.warning("'allowed' key corresponding to allowed groups for permission %s not found when restoring app %s … You might have to reconfigure permissions yourself." % (permission_name, app_instance_name))
+ should_be_allowed = ["all_users"]
+ else:
+ should_be_allowed = [g for g in permission_infos["allowed"] if g in existing_groups]
+
+ permission_create(permission_name, url=permission_infos.get("url", None), allowed=should_be_allowed, sync_perm=False)
+
+ permission_sync_to_user()
+
+ os.remove('%s/permissions.yml' % app_settings_new_path)
else:
+ # Otherwise, we need to migrate the legacy permissions of this
+ # app (included in its settings.yml)
from yunohost.tools import _get_migration_by_name
setup_group_permission = _get_migration_by_name("setup_group_permission")
setup_group_permission.migrate_app_permission(app=app_instance_name)
@@ -1407,6 +1396,9 @@ class RestoreManager():
logger.exception(msg)
operation_logger.error(msg)
+ if msettings.get('interface') != 'api':
+ dump_app_log_extract_for_debugging(operation_logger)
+
self.targets.set_result("apps", app_instance_name, "Error")
remove_script = os.path.join(app_scripts_in_archive, 'remove')
@@ -1424,7 +1416,6 @@ class RestoreManager():
operation_logger.start()
# Execute remove script
- # TODO: call app_remove instead
if hook_exec(remove_script, args=[app_instance_name],
env=env_dict_remove)[0] != 0:
msg = m18n.n('app_not_properly_removed', app=app_instance_name)
@@ -1436,12 +1427,10 @@ class RestoreManager():
# Cleaning app directory
shutil.rmtree(app_settings_new_path, ignore_errors=True)
- # Remove all permission in LDAP
- result = ldap.search(base='ou=permission,dc=yunohost,dc=org',
- filter='(&(objectclass=permissionYnh)(cn=*.%s))' % app_instance_name, attrs=['cn'])
- permission_list = [p['cn'][0] for p in result]
- for l in permission_list:
- permission_remove(app_instance_name, l.split('.')[0], force=True)
+ # Remove all permission in LDAP for this app
+ for permission_name in user_permission_list()["permissions"].keys():
+ if permission_name.startswith(app_instance_name+"."):
+ permission_delete(permission_name, force=True)
# TODO Cleaning app hooks
else:
@@ -1840,11 +1829,12 @@ class CopyBackupMethod(BackupMethod):
self.work_dir])
if ret == 0:
return
- else:
- logger.warning(m18n.n("bind_mouting_disable"))
- subprocess.call(["mountpoint", "-q", self.work_dir,
- "&&", "umount", "-R", self.work_dir])
- raise YunohostError('backup_cant_mount_uncompress_archive')
+
+ logger.warning("Could not mount the backup in readonly mode with --rbind ... Unmounting")
+ # FIXME : Does this stuff really works ? '&&' is going to be interpreted as an argument for mounpoint here ... Not as a classical '&&' ...
+ subprocess.call(["mountpoint", "-q", self.work_dir,
+ "&&", "umount", "-R", self.work_dir])
+ raise YunohostError('backup_cant_mount_uncompress_archive')
class TarBackupMethod(BackupMethod):
@@ -1921,6 +1911,8 @@ class TarBackupMethod(BackupMethod):
Exceptions:
backup_archive_open_failed -- Raised if the archive can't be open
+ backup_archive_corrupted -- Raised if the archive appears corrupted
+ backup_archive_cant_retrieve_info_json -- If the info.json file can't be retrieved
"""
super(TarBackupMethod, self).mount(restore_manager)
@@ -1932,6 +1924,11 @@ class TarBackupMethod(BackupMethod):
self._archive_file, exc_info=1)
raise YunohostError('backup_archive_open_failed')
+ try:
+ files_in_archive = tar.getnames()
+ except IOError as e:
+ raise YunohostError("backup_archive_corrupted", archive=self._archive_file, error=str(e))
+
# FIXME : Is this really useful to close the archive just to
# reopen it right after this with the same options ...?
tar.close()
@@ -1940,21 +1937,21 @@ class TarBackupMethod(BackupMethod):
logger.debug(m18n.n("restore_extracting"))
tar = tarfile.open(self._archive_file, "r:gz")
- if "info.json" in tar.getnames():
+ if "info.json" in files_in_archive:
leading_dot = ""
tar.extract('info.json', path=self.work_dir)
- elif "./info.json" in tar.getnames():
+ elif "./info.json" in files_in_archive:
leading_dot = "./"
tar.extract('./info.json', path=self.work_dir)
else:
logger.debug("unable to retrieve 'info.json' inside the archive",
exc_info=1)
tar.close()
- raise YunohostError('backup_invalid_archive')
+ raise YunohostError('backup_archive_cant_retrieve_info_json', archive=self._archive_file)
- if "backup.csv" in tar.getnames():
+ if "backup.csv" in files_in_archive:
tar.extract('backup.csv', path=self.work_dir)
- elif "./backup.csv" in tar.getnames():
+ elif "./backup.csv" in files_in_archive:
tar.extract('./backup.csv', path=self.work_dir)
else:
# Old backup archive have no backup.csv file
@@ -2296,7 +2293,7 @@ def backup_list(with_info=False, human_readable=False):
try:
d[a] = backup_info(a, human_readable=human_readable)
except YunohostError as e:
- logger.warning('%s: %s' % (a, e.strerror))
+ logger.warning(str(e))
result = d
@@ -2333,17 +2330,23 @@ def backup_info(name, with_details=False, human_readable=False):
if not os.path.exists(info_file):
tar = tarfile.open(archive_file, "r:gz")
info_dir = info_file + '.d'
+
try:
- if "info.json" in tar.getnames():
+ files_in_archive = tar.getnames()
+ except IOError as e:
+ raise YunohostError("backup_archive_corrupted", archive=archive_file, error=str(e))
+
+ try:
+ if "info.json" in files_in_archive:
tar.extract('info.json', path=info_dir)
- elif "./info.json" in tar.getnames():
+ elif "./info.json" in files_in_archive:
tar.extract('./info.json', path=info_dir)
else:
raise KeyError
except KeyError:
logger.debug("unable to retrieve '%s' inside the archive",
info_file, exc_info=1)
- raise YunohostError('backup_invalid_archive')
+ raise YunohostError('backup_archive_cant_retrieve_info_json', archive=archive_file)
else:
shutil.move(os.path.join(info_dir, 'info.json'), info_file)
finally:
@@ -2356,7 +2359,7 @@ def backup_info(name, with_details=False, human_readable=False):
info = json.load(f)
except:
logger.debug("unable to load '%s'", info_file, exc_info=1)
- raise YunohostError('backup_invalid_archive')
+ raise YunohostError('backup_archive_cant_retrieve_info_json', archive=archive_file)
# Retrieve backup size
size = info.get('size', 0)
@@ -2384,6 +2387,15 @@ def backup_info(name, with_details=False, human_readable=False):
if "size_details" in info.keys():
for category in ["apps", "system"]:
for name, key_info in info[category].items():
+
+ if category == "system":
+ # Stupid legacy fix for weird format between 3.5 and 3.6
+ if isinstance(key_info, dict):
+ key_info = key_info.keys()
+ info[category][name] = key_info = {"paths": key_info}
+ else:
+ info[category][name] = key_info
+
if name in info["size_details"][category].keys():
key_info["size"] = info["size_details"][category][name]
if human_readable:
@@ -2494,3 +2506,23 @@ def disk_usage(path):
du_output = subprocess.check_output(['du', '-sb', path])
return int(du_output.split()[0].decode('utf-8'))
+
+
+def binary_to_human(n, customary=False):
+ """
+ Convert bytes or bits into human readable format with binary prefix
+ Keyword argument:
+ n -- Number to convert
+ customary -- Use customary symbol instead of IEC standard
+ """
+ symbols = ('Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi')
+ if customary:
+ symbols = ('K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y')
+ prefix = {}
+ for i, s in enumerate(symbols):
+ prefix[s] = 1 << (i + 1) * 10
+ for s in reversed(symbols):
+ if n >= prefix[s]:
+ value = float(n) / prefix[s]
+ return '%.1f%s' % (value, s)
+ return "%s" % n
diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py
index d141ac8e5..f3971be06 100644
--- a/src/yunohost/certificate.py
+++ b/src/yunohost/certificate.py
@@ -34,15 +34,14 @@ import glob
from datetime import datetime
-from yunohost.vendor.acme_tiny.acme_tiny import get_crt as sign_certificate
-
-from yunohost.utils.error import YunohostError
+from moulinette import m18n
from moulinette.utils.log import getActionLogger
+from moulinette.utils.filesystem import read_file
+from yunohost.vendor.acme_tiny.acme_tiny import get_crt as sign_certificate
+from yunohost.utils.error import YunohostError
from yunohost.utils.network import get_public_ip
-from moulinette import m18n
-from yunohost.app import app_ssowatconf
from yunohost.service import _run_service_command
from yunohost.regenconf import regen_conf
from yunohost.log import OperationLogger
@@ -285,7 +284,6 @@ def _certificate_install_letsencrypt(domain_list, force=False, no_checks=False,
operation_logger.start()
- _configure_for_acme_challenge(domain)
_fetch_and_enable_new_certificate(domain, staging, no_checks=no_checks)
_install_cron(no_checks=no_checks)
@@ -468,61 +466,16 @@ Subject: %s
smtp.quit()
-def _configure_for_acme_challenge(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 YunohostError('certmanager_conflicting_nginx_file', filepath=path)
-
- # Write the conf
- if os.path.exists(nginx_conf_file):
- logger.debug(
- "Nginx configuration file for ACME challenge already exists for domain, skipping.")
- return
-
- logger.debug(
- "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()
-
-
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:
+ domain_conf = "/etc/nginx/conf.d/%s.conf" % domain
+ if "include /etc/nginx/conf.d/acme-challenge.conf.inc" in read_file(domain_conf):
return True
+ else:
+ # This is for legacy setups which haven't updated their domain conf to
+ # the new conf that include the acme snippet...
+ legacy_acme_conf = "/etc/nginx/conf.d/%s.d/000-acmechallenge.conf" % domain
+ return os.path.exists(legacy_acme_conf)
def _fetch_and_enable_new_certificate(domain, staging=False, no_checks=False):
@@ -639,6 +592,16 @@ def _prepare_certificate_signing_request(domain, key_file, output_folder):
# Set the domain
csr.get_subject().CN = domain
+ from yunohost.domain import domain_list
+ # For "parent" domains, include xmpp-upload subdomain in subject alternate names
+ if domain in domain_list(exclude_subdomains=True)["domains"]:
+ subdomain = "xmpp-upload." + domain
+ try:
+ _dns_ip_match_public_ip(get_public_ip(), subdomain)
+ csr.add_extensions([crypto.X509Extension("subjectAltName", False, "DNS:" + subdomain)])
+ except YunohostError:
+ logger.warning(m18n.n('certmanager_warning_subdomain_dns_record', subdomain=subdomain, domain=domain))
+
# Set the key
with open(key_file, 'rt') as f:
key = crypto.load_privatekey(crypto.FILETYPE_PEM, f.read())
diff --git a/src/yunohost/data_migrations/0003_migrate_to_stretch.py b/src/yunohost/data_migrations/0003_migrate_to_stretch.py
index 19793bbec..60b26169a 100644
--- a/src/yunohost/data_migrations/0003_migrate_to_stretch.py
+++ b/src/yunohost/data_migrations/0003_migrate_to_stretch.py
@@ -108,7 +108,7 @@ class MyMigration(Migration):
# Have > 1 Go free space on /var/ ?
if free_space_in_directory("/var/") / (1024**3) < 1.0:
- raise YunohostError("migration_0003_not_enough_free_space")
+ raise YunohostError("There is not enough free space in /var/ to run the migration. You need at least 1GB free space")
# Check system is up to date
# (but we don't if 'stretch' is already in the sources.list ...
diff --git a/src/yunohost/data_migrations/0010_migrate_to_apps_json.py b/src/yunohost/data_migrations/0010_migrate_to_apps_json.py
index fbcec5117..e5ce65608 100644
--- a/src/yunohost/data_migrations/0010_migrate_to_apps_json.py
+++ b/src/yunohost/data_migrations/0010_migrate_to_apps_json.py
@@ -1,48 +1,13 @@
-import os
-
from moulinette.utils.log import getActionLogger
-from yunohost.app import app_fetchlist, app_removelist, _read_appslist_list, APPSLISTS_JSON
from yunohost.tools import Migration
logger = getActionLogger('yunohost.migration')
-BASE_CONF_PATH = '/home/yunohost.conf'
-BACKUP_CONF_DIR = os.path.join(BASE_CONF_PATH, 'backup')
-APPSLISTS_BACKUP = os.path.join(BACKUP_CONF_DIR, "appslist_before_migration_to_unified_list.json")
-
class MyMigration(Migration):
- "Migrate from official.json to apps.json"
+ "Migrate from official.json to apps.json (outdated, replaced by migration 13)"
def run(self):
-
- # Backup current app list json
- os.system("cp %s %s" % (APPSLISTS_JSON, APPSLISTS_BACKUP))
-
- # Remove all the deprecated lists
- lists_to_remove = [
- "app.yunohost.org/list.json", # Old list on old installs, alias to official.json
- "app.yunohost.org/official.json",
- "app.yunohost.org/community.json",
- "labriqueinter.net/apps/labriqueinternet.json",
- "labriqueinter.net/internetcube.json"
- ]
- try:
- appslists = _read_appslist_list()
- for appslist, infos in appslists.items():
- if infos["url"].split("//")[-1] in lists_to_remove:
- app_removelist(name=appslist)
-
- # Replace by apps.json list
- app_fetchlist(name="yunohost",
- url="https://app.yunohost.org/apps.json")
- except Exception:
- if os.path.exists(APPSLISTS_BACKUP):
- os.system("cp %s %s" % (APPSLISTS_BACKUP, APPSLISTS_JSON))
- raise
- else:
- if os.path.exists(APPSLISTS_BACKUP):
- os.remove(APPSLISTS_BACKUP)
-
-
+ logger.info("This migration is oudated and doesn't do anything anymore. The migration 13 will handle this instead.")
+ pass
diff --git a/src/yunohost/data_migrations/0011_setup_group_permission.py b/src/yunohost/data_migrations/0011_setup_group_permission.py
index 17fd8f4a5..c55e33cab 100644
--- a/src/yunohost/data_migrations/0011_setup_group_permission.py
+++ b/src/yunohost/data_migrations/0011_setup_group_permission.py
@@ -1,17 +1,16 @@
-import yaml
import time
import os
from moulinette import m18n
from yunohost.utils.error import YunohostError
from moulinette.utils.log import getActionLogger
+from moulinette.utils.filesystem import read_yaml
from yunohost.tools import Migration
-from yunohost.user import user_group_add, user_group_update
-from yunohost.app import app_setting, app_list
-from yunohost.regenconf import regen_conf
-from yunohost.permission import permission_add, permission_sync_to_user
-from yunohost.user import user_permission_add
+from yunohost.user import user_list, user_group_create, user_group_update
+from yunohost.app import app_setting, _installed_apps
+from yunohost.regenconf import regen_conf, BACKUP_CONF_DIR
+from yunohost.permission import permission_create, user_permission_update, permission_sync_to_user
logger = getActionLogger('yunohost.migration')
@@ -19,6 +18,7 @@ logger = getActionLogger('yunohost.migration')
# Tools used also for restoration
###################################################
+
class MyMigration(Migration):
"""
Update the LDAP DB to be able to store the permission
@@ -28,6 +28,28 @@ class MyMigration(Migration):
required = True
+ def remove_if_exists(self, target):
+
+ from yunohost.utils.ldap import _get_ldap_interface
+ ldap = _get_ldap_interface()
+
+ try:
+ objects = ldap.search(target + ",dc=yunohost,dc=org")
+ # ldap search will raise an exception if no corresponding object is found >.> ...
+ except Exception as e:
+ logger.debug("%s does not exist, no need to delete it" % target)
+ return
+
+ objects.reverse()
+ for o in objects:
+ for dn in o["dn"]:
+ dn = dn.replace(",dc=yunohost,dc=org", "")
+ logger.debug("Deleting old object %s ..." % dn)
+ try:
+ ldap.remove(dn)
+ except Exception as e:
+ raise YunohostError("migration_0011_failed_to_remove_stale_object", dn=dn, error=e)
+
def migrate_LDAP_db(self):
logger.info(m18n.n("migration_0011_update_LDAP_database"))
@@ -35,21 +57,24 @@ class MyMigration(Migration):
from yunohost.utils.ldap import _get_ldap_interface
ldap = _get_ldap_interface()
- try:
- ldap.remove('cn=sftpusers,ou=groups')
- except:
- logger.warn(m18n.n("error_when_removing_sftpuser_group"))
-
- with open('/usr/share/yunohost/yunohost-config/moulinette/ldap_scheme.yml') as f:
- ldap_map = yaml.load(f)
+ ldap_map = read_yaml('/usr/share/yunohost/yunohost-config/moulinette/ldap_scheme.yml')
try:
+ self.remove_if_exists("ou=permission")
+ self.remove_if_exists('ou=groups')
+
attr_dict = ldap_map['parents']['ou=permission']
ldap.add('ou=permission', attr_dict)
+ attr_dict = ldap_map['parents']['ou=groups']
+ ldap.add('ou=groups', attr_dict)
+
attr_dict = ldap_map['children']['cn=all_users,ou=groups']
ldap.add('cn=all_users,ou=groups', attr_dict)
+ attr_dict = ldap_map['children']['cn=visitors,ou=groups']
+ ldap.add('cn=visitors,ou=groups', attr_dict)
+
for rdn, attr_dict in ldap_map['depends_children'].items():
ldap.add(rdn, attr_dict)
except Exception as e:
@@ -65,39 +90,53 @@ class MyMigration(Migration):
username = user_info['uid'][0]
ldap.update('uid=%s,ou=users' % username,
{'objectClass': ['mailAccount', 'inetOrgPerson', 'posixAccount', 'userPermissionYnh']})
- user_group_add(username, gid=user_info['uidNumber'][0], sync_perm=False)
- user_group_update(groupname=username, add_user=username, force=True, sync_perm=False)
- user_group_update(groupname='all_users', add_user=username, force=True, sync_perm=False)
-
+ user_group_create(username, gid=user_info['uidNumber'][0], primary_group=True, sync_perm=False)
+ user_group_update(groupname='all_users', add=username, force=True, sync_perm=False)
def migrate_app_permission(self, app=None):
logger.info(m18n.n("migration_0011_migrate_permission"))
- if app:
- apps = app_list(installed=True, filter=app)['apps']
- else:
- apps = app_list(installed=True)['apps']
+ apps = _installed_apps()
- for app_info in apps:
- app = app_info['id']
+ if app:
+ if app not in apps:
+ logger.error("Can't migrate permission for app %s because it ain't installed..." % app)
+ apps = []
+ else:
+ apps = [app]
+
+ for app in apps:
permission = app_setting(app, 'allowed_users')
path = app_setting(app, 'path')
domain = app_setting(app, 'domain')
- urls = [domain + path] if domain and path else None
- permission_add(app, permission='main', urls=urls, default_allow=True, sync_perm=False)
+ url = "/" if domain and path else None
if permission:
- allowed_group = permission.split(',')
- user_permission_add([app], permission='main', group=allowed_group, sync_perm=False)
+ known_users = user_list()["users"].keys()
+ allowed = [user for user in permission.split(',') if user in known_users]
+ else:
+ allowed = ["all_users"]
+ permission_create(app+".main", url=url, allowed=allowed, sync_perm=False)
+
app_setting(app, 'allowed_users', delete=True)
+ # Migrate classic public app still using the legacy unprotected_uris
+ if app_setting(app, "unprotected_uris") == "/" or app_setting(app, "skipped_uris") == "/":
+ user_permission_update(app+".main", add="visitors", sync_perm=False)
+
+ permission_sync_to_user()
def run(self):
+
+ # FIXME : what do we really want to do here ...
+ # Imho we should just force-regen the conf in all case, and maybe
+ # just display a warning if we detect that the conf was manually modified
+
# Check if the migration can be processed
ldap_regen_conf_status = regen_conf(names=['slapd'], dry_run=True)
# By this we check if the have been customized
if ldap_regen_conf_status and ldap_regen_conf_status['slapd']['pending']:
- raise YunohostError("migration_0011_LDAP_config_dirty")
+ logger.warning(m18n.n("migration_0011_slapd_config_will_be_overwritten", conf_backup_folder=BACKUP_CONF_DIR))
# Backup LDAP and the apps settings before to do the migration
logger.info(m18n.n("migration_0011_backup_before_migration"))
diff --git a/src/yunohost/data_migrations/0013_futureproof_apps_catalog_system.py b/src/yunohost/data_migrations/0013_futureproof_apps_catalog_system.py
new file mode 100644
index 000000000..ff4925183
--- /dev/null
+++ b/src/yunohost/data_migrations/0013_futureproof_apps_catalog_system.py
@@ -0,0 +1,51 @@
+
+import os
+import shutil
+
+from moulinette.utils.log import getActionLogger
+from moulinette.utils.filesystem import read_json
+
+from yunohost.tools import Migration
+from yunohost.app import (_initialize_apps_catalog_system,
+ _update_apps_catalog,
+ APPS_CATALOG_CACHE,
+ APPS_CATALOG_CONF)
+
+logger = getActionLogger('yunohost.migration')
+
+LEGACY_APPS_CATALOG_CONF = '/etc/yunohost/appslists.json'
+LEGACY_APPS_CATALOG_CONF_BACKUP = LEGACY_APPS_CATALOG_CONF + ".old"
+
+
+class MyMigration(Migration):
+
+ "Migrate to the new future-proof apps catalog system"
+
+ def run(self):
+
+ if not os.path.exists(LEGACY_APPS_CATALOG_CONF):
+ logger.info("No need to do anything")
+
+ # Destroy old lecacy cache
+ if os.path.exists(APPS_CATALOG_CACHE):
+ shutil.rmtree(APPS_CATALOG_CACHE)
+
+ # and legacy cron
+ if os.path.exists("/etc/cron.daily/yunohost-fetch-appslists"):
+ os.remove("/etc/cron.daily/yunohost-fetch-appslists")
+
+ # Backup the legacy file
+ try:
+ legacy_catalogs = read_json(LEGACY_APPS_CATALOG_CONF)
+ # If there's only one catalog, we assume it's just the old official catalog
+ # Otherwise, warn the (power-?)users that they should migrate their old catalogs manually
+ if len(legacy_catalogs) > 1:
+ logger.warning("It looks like you had additional apps_catalog in the configuration file %s! YunoHost now uses %s instead, but it won't migrate your custom apps_catalog. You should do this manually. The old file has been backuped in %s." % (LEGACY_APPS_CATALOG_CONF, APPS_CATALOG_CONF, LEGACY_APPS_CATALOG_CONF_BACKUP))
+ except Exception as e:
+ logger.warning("Unable to parse the legacy conf %s (error : %s) ... migrating anyway" % (LEGACY_APPS_CATALOG_CONF, str(e)))
+
+ if os.path.exists(LEGACY_APPS_CATALOG_CONF):
+ os.rename(LEGACY_APPS_CATALOG_CONF, LEGACY_APPS_CATALOG_CONF_BACKUP)
+
+ _initialize_apps_catalog_system()
+ _update_apps_catalog()
diff --git a/src/yunohost/data_migrations/0014_remove_app_status_json.py b/src/yunohost/data_migrations/0014_remove_app_status_json.py
new file mode 100644
index 000000000..1cb5bc002
--- /dev/null
+++ b/src/yunohost/data_migrations/0014_remove_app_status_json.py
@@ -0,0 +1,31 @@
+import os
+
+from moulinette.utils.log import getActionLogger
+from moulinette.utils.filesystem import read_json
+
+from yunohost.tools import Migration
+from yunohost.app import app_setting, APPS_SETTING_PATH
+
+logger = getActionLogger('yunohost.migration')
+
+class MyMigration(Migration):
+
+ """Remove legacy app status.json files"""
+
+ def run(self):
+
+ apps = os.listdir(APPS_SETTING_PATH)
+
+ for app in apps:
+ status_file = os.path.join(APPS_SETTING_PATH, app, "status.json")
+ if not os.path.exists(status_file):
+ continue
+
+ try:
+ status = read_json(status_file)
+ current_revision = status.get("remote", {}).get("revision", "?")
+ app_setting(app, 'current_revision', current_revision)
+ except Exception as e:
+ logger.warning("Could not migrate status.json from app %s: %s", (app, str(e)))
+ else:
+ os.system("rm %s" % status_file)
diff --git a/src/yunohost/diagnosis.py b/src/yunohost/diagnosis.py
new file mode 100644
index 000000000..bfb2619eb
--- /dev/null
+++ b/src/yunohost/diagnosis.py
@@ -0,0 +1,549 @@
+# -*- coding: utf-8 -*-
+
+""" License
+
+ Copyright (C) 2018 YunoHost
+
+ 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
+
+"""
+
+""" diagnosis.py
+
+ Look for possible issues on the server
+"""
+
+import re
+import os
+import time
+
+from moulinette import m18n, msettings
+from moulinette.utils import log
+from moulinette.utils.filesystem import read_json, write_to_json, read_yaml, write_to_yaml
+
+from yunohost.utils.error import YunohostError
+from yunohost.hook import hook_list, hook_exec
+
+logger = log.getActionLogger('yunohost.diagnosis')
+
+DIAGNOSIS_CACHE = "/var/cache/yunohost/diagnosis/"
+DIAGNOSIS_CONFIG_FILE = '/etc/yunohost/diagnosis.yml'
+DIAGNOSIS_SERVER = "diagnosis.yunohost.org"
+
+def diagnosis_list():
+ all_categories_names = [h for h, _ in _list_diagnosis_categories()]
+ return {"categories": all_categories_names}
+
+
+def diagnosis_get(category, item):
+
+ # Get all the categories
+ all_categories = _list_diagnosis_categories()
+ all_categories_names = [c for c, _ in all_categories]
+
+ if category not in all_categories_names:
+ raise YunohostError('diagnosis_unknown_categories', categories=category)
+
+ if isinstance(item, list):
+ if any("=" not in criteria for criteria in item):
+ raise YunohostError("Criterias should be of the form key=value (e.g. domain=yolo.test)")
+
+ # Convert the provided criteria into a nice dict
+ item = {c.split("=")[0]: c.split("=")[1] for c in item}
+
+ return Diagnoser.get_cached_report(category, item=item)
+
+
+def diagnosis_show(categories=[], issues=False, full=False, share=False):
+
+ if not os.path.exists(DIAGNOSIS_CACHE):
+ logger.warning(m18n.n("diagnosis_never_ran_yet"))
+ return
+
+ # Get all the categories
+ all_categories = _list_diagnosis_categories()
+ all_categories_names = [category for category, _ in all_categories]
+
+ # Check the requested category makes sense
+ if categories == []:
+ categories = all_categories_names
+ else:
+ unknown_categories = [c for c in categories if c not in all_categories_names]
+ if unknown_categories:
+ raise YunohostError('diagnosis_unknown_categories', categories=", ".join(unknown_categories))
+
+ # Fetch all reports
+ all_reports = []
+ for category in categories:
+
+ try:
+ report = Diagnoser.get_cached_report(category)
+ except Exception as e:
+ logger.error(m18n.n("diagnosis_failed", category=category, error=str(e)))
+ continue
+
+ Diagnoser.i18n(report)
+
+ add_ignore_flag_to_issues(report)
+ if not full:
+ del report["timestamp"]
+ del report["cached_for"]
+ report["items"] = [item for item in report["items"] if not item["ignored"]]
+ for item in report["items"]:
+ del item["meta"]
+ del item["ignored"]
+ if "data" in item:
+ del item["data"]
+ if issues:
+ report["items"] = [item for item in report["items"] if item["status"] in ["WARNING", "ERROR"]]
+ # Ignore this category if no issue was found
+ if not report["items"]:
+ continue
+
+ all_reports.append(report)
+
+ if share:
+ from yunohost.utils.yunopaste import yunopaste
+ content = _dump_human_readable_reports(all_reports)
+ url = yunopaste(content)
+
+ logger.info(m18n.n("log_available_on_yunopaste", url=url))
+ if msettings.get('interface') == 'api':
+ return {"url": url}
+ else:
+ return
+ else:
+ return {"reports": all_reports}
+
+def _dump_human_readable_reports(reports):
+
+ output = ""
+
+ for report in reports:
+ output += "=================================\n"
+ output += "{description} ({id})\n".format(**report)
+ output += "=================================\n\n"
+ for item in report["items"]:
+ output += "[{status}] {summary}\n".format(**item)
+ for detail in item.get("details", []):
+ output += " - " + detail + "\n"
+ output += "\n"
+ output += "\n\n"
+
+ return(output)
+
+
+def diagnosis_run(categories=[], force=False, except_if_never_ran_yet=False):
+
+ if except_if_never_ran_yet and not os.path.exists(DIAGNOSIS_CACHE):
+ return
+
+ # Get all the categories
+ all_categories = _list_diagnosis_categories()
+ all_categories_names = [category for category, _ in all_categories]
+
+ # Check the requested category makes sense
+ if categories == []:
+ categories = all_categories_names
+ else:
+ unknown_categories = [c for c in categories if c not in all_categories_names]
+ if unknown_categories:
+ raise YunohostError('diagnosis_unknown_categories', categories=", ".join(unknown_categories))
+
+ issues = []
+ # Call the hook ...
+ diagnosed_categories = []
+ for category in categories:
+ logger.debug("Running diagnosis for %s ..." % category)
+ path = [p for n, p in all_categories if n == category][0]
+
+ try:
+ code, report = hook_exec(path, args={"force": force}, env=None)
+ except Exception as e:
+ import traceback
+ logger.error(m18n.n("diagnosis_failed_for_category", category=category, error='\n'+traceback.format_exc()))
+ else:
+ diagnosed_categories.append(category)
+ if report != {}:
+ issues.extend([item for item in report["items"] if item["status"] in ["WARNING", "ERROR"]])
+
+ if issues and msettings.get("interface") == "cli":
+ logger.warning(m18n.n("diagnosis_display_tip"))
+
+ return
+
+
+def diagnosis_ignore(add_filter=None, remove_filter=None, list=False):
+ """
+ This action is meant for the admin to ignore issues reported by the
+ diagnosis system if they are known and understood by the admin. For
+ example, the lack of ipv6 on an instance, or badly configured XMPP dns
+ records if the admin doesn't care so much about XMPP. The point being that
+ the diagnosis shouldn't keep complaining about those known and "expected"
+ issues, and instead focus on new unexpected issues that could arise.
+
+ For example, to ignore badly XMPP dnsrecords for domain yolo.test:
+
+ yunohost diagnosis ignore --add-filter dnsrecords domain=yolo.test category=xmpp
+ ^ ^ ^
+ the general additional other
+ diagnosis criterias criteria
+ category to to target to target
+ act on specific specific
+ reports reports
+ Or to ignore all dnsrecords issues:
+
+ yunohost diagnosis ignore --add-filter dnsrecords
+
+ The filters are stored in the diagnosis configuration in a data structure like:
+
+ ignore_filters: {
+ "ip": [
+ {"version": 6} # Ignore all issues related to ipv6
+ ],
+ "dnsrecords": [
+ {"domain": "yolo.test", "category": "xmpp"}, # Ignore all issues related to DNS xmpp records for yolo.test
+ {} # Ignore all issues about dnsrecords
+ ]
+ }
+ """
+
+ # Ignore filters are stored in
+ configuration = _diagnosis_read_configuration()
+
+ if list:
+ return {"ignore_filters": configuration.get("ignore_filters", {})}
+
+ def validate_filter_criterias(filter_):
+
+ # Get all the categories
+ all_categories = _list_diagnosis_categories()
+ all_categories_names = [category for category, _ in all_categories]
+
+ # Sanity checks for the provided arguments
+ if len(filter_) == 0:
+ raise YunohostError("You should provide at least one criteria being the diagnosis category to ignore")
+ category = filter_[0]
+ if category not in all_categories_names:
+ raise YunohostError("%s is not a diagnosis category" % category)
+ if any("=" not in criteria for criteria in filter_[1:]):
+ raise YunohostError("Criterias should be of the form key=value (e.g. domain=yolo.test)")
+
+ # Convert the provided criteria into a nice dict
+ criterias = {c.split("=")[0]: c.split("=")[1] for c in filter_[1:]}
+
+ return category, criterias
+
+ if add_filter:
+
+ category, criterias = validate_filter_criterias(add_filter)
+
+ # Fetch current issues for the requested category
+ current_issues_for_this_category = diagnosis_show(categories=[category], issues=True, full=True)
+ current_issues_for_this_category = current_issues_for_this_category["reports"][0].get("items", {})
+
+ # Accept the given filter only if the criteria effectively match an existing issue
+ if not any(issue_matches_criterias(i, criterias) for i in current_issues_for_this_category):
+ raise YunohostError("No issues was found matching the given criteria.")
+
+ # Make sure the subdicts/lists exists
+ if "ignore_filters" not in configuration:
+ configuration["ignore_filters"] = {}
+ if category not in configuration["ignore_filters"]:
+ configuration["ignore_filters"][category] = []
+
+ if criterias in configuration["ignore_filters"][category]:
+ logger.warning("This filter already exists.")
+ return
+
+ configuration["ignore_filters"][category].append(criterias)
+ _diagnosis_write_configuration(configuration)
+ logger.success("Filter added")
+ return
+
+ if remove_filter:
+
+ category, criterias = validate_filter_criterias(remove_filter)
+
+ # Make sure the subdicts/lists exists
+ if "ignore_filters" not in configuration:
+ configuration["ignore_filters"] = {}
+ if category not in configuration["ignore_filters"]:
+ configuration["ignore_filters"][category] = []
+
+ if criterias not in configuration["ignore_filters"][category]:
+ raise YunohostError("This filter does not exists.")
+
+ configuration["ignore_filters"][category].remove(criterias)
+ _diagnosis_write_configuration(configuration)
+ logger.success("Filter removed")
+ return
+
+
+def _diagnosis_read_configuration():
+ if not os.path.exists(DIAGNOSIS_CONFIG_FILE):
+ return {}
+
+ return read_yaml(DIAGNOSIS_CONFIG_FILE)
+
+
+def _diagnosis_write_configuration(conf):
+ write_to_yaml(DIAGNOSIS_CONFIG_FILE, conf)
+
+
+def issue_matches_criterias(issue, criterias):
+ """
+ e.g. an issue with:
+ meta:
+ domain: yolo.test
+ category: xmpp
+
+ matches the criterias {"domain": "yolo.test"}
+ """
+ for key, value in criterias.items():
+ if key not in issue["meta"]:
+ return False
+ if str(issue["meta"][key]) != value:
+ return False
+ return True
+
+def add_ignore_flag_to_issues(report):
+ """
+ Iterate over issues in a report, and flag them as ignored if they match an
+ ignored filter from the configuration
+
+ N.B. : for convenience. we want to make sure the "ignored" key is set for
+ every item in the report
+ """
+
+ ignore_filters = _diagnosis_read_configuration().get("ignore_filters", {}).get(report["id"], [])
+
+ for report_item in report["items"]:
+ report_item["ignored"] = False
+ if report_item["status"] not in ["WARNING", "ERROR"]:
+ continue
+ for criterias in ignore_filters:
+ if issue_matches_criterias(report_item, criterias):
+ report_item["ignored"] = True
+ break
+
+
+############################################################
+
+
+class Diagnoser():
+
+ def __init__(self, args, env, loggers):
+
+ # FIXME ? That stuff with custom loggers is weird ... (mainly inherited from the bash hooks, idk)
+ self.logger_debug, self.logger_warning, self.logger_info = loggers
+ self.env = env
+ self.args = args or {}
+ self.cache_file = Diagnoser.cache_file(self.id_)
+ self.description = Diagnoser.get_description(self.id_)
+
+ def cached_time_ago(self):
+
+ if not os.path.exists(self.cache_file):
+ return 99999999
+ return time.time() - os.path.getmtime(self.cache_file)
+
+ def write_cache(self, report):
+ if not os.path.exists(DIAGNOSIS_CACHE):
+ os.makedirs(DIAGNOSIS_CACHE)
+ return write_to_json(self.cache_file, report)
+
+ def diagnose(self):
+
+ if not self.args.get("force", False) and self.cached_time_ago() < self.cache_duration:
+ self.logger_debug("Cache still valid : %s" % self.cache_file)
+ logger.info(m18n.n("diagnosis_cache_still_valid", category=self.description))
+ return 0, {}
+
+ for dependency in self.dependencies:
+ dep_report = Diagnoser.get_cached_report(dependency)
+
+ if dep_report["timestamp"] == -1: # No cache yet for this dep
+ dep_errors = True
+ else:
+ dep_errors = [item for item in dep_report["items"] if item["status"] == "ERROR"]
+
+ if dep_errors:
+ logger.error(m18n.n("diagnosis_cant_run_because_of_dep", category=self.description, dep=Diagnoser.get_description(dependency)))
+ return 1, {}
+
+ items = list(self.run())
+
+ for item in items:
+ if "details" in item and not item["details"]:
+ del item["details"]
+
+ new_report = {"id": self.id_,
+ "cached_for": self.cache_duration,
+ "items": items}
+
+ self.logger_debug("Updating cache %s" % self.cache_file)
+ self.write_cache(new_report)
+ Diagnoser.i18n(new_report)
+ add_ignore_flag_to_issues(new_report)
+
+ errors = [item for item in new_report["items"] if item["status"] == "ERROR" and not item["ignored"]]
+ warnings = [item for item in new_report["items"] if item["status"] == "WARNING" and not item["ignored"]]
+ errors_ignored = [item for item in new_report["items"] if item["status"] == "ERROR" and item["ignored"]]
+ warning_ignored = [item for item in new_report["items"] if item["status"] == "WARNING" and item["ignored"]]
+ ignored_msg = " " + m18n.n("diagnosis_ignored_issues", nb_ignored=len(errors_ignored+warning_ignored)) if errors_ignored or warning_ignored else ""
+
+ if errors and warnings:
+ logger.error(m18n.n("diagnosis_found_errors_and_warnings", errors=len(errors), warnings=len(warnings), category=new_report["description"]) + ignored_msg)
+ elif errors:
+ logger.error(m18n.n("diagnosis_found_errors", errors=len(errors), category=new_report["description"]) + ignored_msg)
+ elif warnings:
+ logger.warning(m18n.n("diagnosis_found_warnings", warnings=len(warnings), category=new_report["description"]) + ignored_msg)
+ else:
+ logger.success(m18n.n("diagnosis_everything_ok", category=new_report["description"]) + ignored_msg)
+
+ return 0, new_report
+
+ @staticmethod
+ def cache_file(id_):
+ return os.path.join(DIAGNOSIS_CACHE, "%s.json" % id_)
+
+ @staticmethod
+ def get_cached_report(id_, item=None):
+ cache_file = Diagnoser.cache_file(id_)
+ if not os.path.exists(cache_file):
+ logger.warning(m18n.n("diagnosis_no_cache", category=id_))
+ report = {"id": id_,
+ "cached_for": -1,
+ "timestamp": -1,
+ "items": []}
+ else:
+ report = read_json(cache_file)
+ report["timestamp"] = int(os.path.getmtime(cache_file))
+
+ if item:
+ for report_item in report["items"]:
+ if report_item.get("meta") == item:
+ return report_item
+ return {}
+ else:
+ return report
+
+ @staticmethod
+ def get_description(id_):
+ key = "diagnosis_description_" + id_
+ descr = m18n.n(key)
+ # If no description available, fallback to id
+ return descr if descr != key else id_
+
+ @staticmethod
+ def i18n(report):
+
+ # "Render" the strings with m18n.n
+ # N.B. : we do those m18n.n right now instead of saving the already-translated report
+ # because we can't be sure we'll redisplay the infos with the same locale as it
+ # was generated ... e.g. if the diagnosing happened inside a cron job with locale EN
+ # instead of FR used by the actual admin...
+
+ report["description"] = Diagnoser.get_description(report["id"])
+
+ for item in report["items"]:
+
+ # For the summary and each details, we want to call
+ # m18n() on the string, with the appropriate data for string
+ # formatting which can come from :
+ # - infos super-specific to the summary/details (if it's a tuple(key,dict_with_info) and not just a string)
+ # - 'meta' info = parameters of the test (e.g. which domain/category for DNS conf record)
+ # - actual 'data' retrieved from the test (e.g. actual global IP, ...)
+
+ meta_data = item.get("meta", {}).copy()
+ meta_data.update(item.get("data", {}))
+
+ html_tags = re.compile(r'<[^>]+>')
+ def m18n_(info):
+ if not isinstance(info, tuple) and not isinstance(info, list):
+ info = (info, {})
+ info[1].update(meta_data)
+ s = m18n.n(info[0], **(info[1]))
+ # In cli, we remove the html tags
+ if msettings.get("interface") != "api":
+ s = s.replace("", "'").replace("", "'")
+ s = html_tags.sub('', s.replace("
","\n"))
+ else:
+ s = s.replace("", "").replace("
", "")
+ # Make it so that links open in new tabs
+ s = s.replace("URL: %s
Status code: %s
" % (url, r.status_code))
+ if r.status_code == 400:
+ raise Exception("Diagnosis request was refused: %s" % r.content)
+
+ try:
+ r = r.json()
+ except Exception as e:
+ raise Exception("Failed to parse json from diagnosis server response.\nError: %s\nOriginal content: %s" % (e, r.content))
+
+ return r
+
+
+def _list_diagnosis_categories():
+ hooks_raw = hook_list("diagnosis", list_by="priority", show_info=True)["hooks"]
+ hooks = []
+ for _, some_hooks in sorted(hooks_raw.items(), key=lambda h: int(h[0])):
+ for name, info in some_hooks.items():
+ hooks.append((name, info["path"]))
+
+ return hooks
diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py
index 42a4881ba..f1dcefba9 100644
--- a/src/yunohost/domain.py
+++ b/src/yunohost/domain.py
@@ -25,16 +25,14 @@
"""
import os
import re
-import yaml
from moulinette import m18n, msettings
from moulinette.core import MoulinetteError
from yunohost.utils.error import YunohostError
from moulinette.utils.log import getActionLogger
-import yunohost.certificate
-
-from yunohost.regenconf import regen_conf
+from yunohost.app import app_ssowatconf, _installed_apps, _get_app_settings
+from yunohost.regenconf import regen_conf, _force_clear_hashes, _process_regen_conf
from yunohost.utils.network import get_public_ip
from yunohost.log import is_unit_operation
from yunohost.hook import hook_callback
@@ -42,24 +40,26 @@ from yunohost.hook import hook_callback
logger = getActionLogger('yunohost.domain')
-def domain_list():
+def domain_list(exclude_subdomains=False):
"""
List domains
Keyword argument:
- filter -- LDAP filter used to search
- offset -- Starting number for domain fetching
- limit -- Maximum number of domain fetched
+ exclude_subdomains -- Filter out domains that are subdomains of other declared domains
"""
from yunohost.utils.ldap import _get_ldap_interface
ldap = _get_ldap_interface()
- result = ldap.search('ou=domains,dc=yunohost,dc=org', 'virtualdomain=*', ['virtualdomain'])
+ result = [entry['virtualdomain'][0] for entry in ldap.search('ou=domains,dc=yunohost,dc=org', 'virtualdomain=*', ['virtualdomain'])]
result_list = []
for domain in result:
- result_list.append(domain['virtualdomain'][0])
+ if exclude_subdomains:
+ parent_domain = domain.split(".", 1)[1]
+ if parent_domain in result:
+ continue
+ result_list.append(domain)
return {'domains': result_list}
@@ -78,6 +78,9 @@ def domain_add(operation_logger, domain, dyndns=False):
from yunohost.app import app_ssowatconf
from yunohost.utils.ldap import _get_ldap_interface
+ if domain.startswith("xmpp-upload."):
+ raise YunohostError("domain_cannot_add_xmpp_upload")
+
ldap = _get_ldap_interface()
try:
@@ -105,6 +108,7 @@ def domain_add(operation_logger, domain, dyndns=False):
dyndns_subscribe(domain=domain)
try:
+ import yunohost.certificate
yunohost.certificate._certificate_install_selfsigned([domain], False)
attr_dict = {
@@ -112,11 +116,24 @@ def domain_add(operation_logger, domain, dyndns=False):
'virtualdomain': domain,
}
- if not ldap.add('virtualdomain=%s,ou=domains' % domain, attr_dict):
- raise YunohostError('domain_creation_failed')
+ try:
+ ldap.add('virtualdomain=%s,ou=domains' % domain, attr_dict)
+ except Exception as e:
+ raise YunohostError('domain_creation_failed', domain=domain, error=e)
# Don't regen these conf if we're still in postinstall
if os.path.exists('/etc/yunohost/installed'):
+ # Sometime we have weird issues with the regenconf where some files
+ # appears as manually modified even though they weren't touched ...
+ # There are a few ideas why this happens (like backup/restore nginx
+ # conf ... which we shouldnt do ...). This in turns creates funky
+ # situation where the regenconf may refuse to re-create the conf
+ # (when re-creating a domain..)
+ # So here we force-clear the has out of the regenconf if it exists.
+ # This is a pretty ad hoc solution and only applied to nginx
+ # because it's one of the major service, but in the long term we
+ # should identify the root of this bug...
+ _force_clear_hashes(["/etc/nginx/conf.d/%s.conf" % domain])
regen_conf(names=['nginx', 'metronome', 'dnsmasq', 'postfix', 'rspamd'])
app_ssowatconf()
@@ -152,25 +169,47 @@ def domain_remove(operation_logger, domain, force=False):
# Check domain is not the main domain
if domain == _get_maindomain():
- raise YunohostError('domain_cannot_remove_main')
+ other_domains = domain_list()["domains"]
+ other_domains.remove(domain)
+
+ if other_domains:
+ raise YunohostError('domain_cannot_remove_main',
+ domain=domain, other_domains="\n * " + ("\n * ".join(other_domains)))
+ else:
+ raise YunohostError('domain_cannot_remove_main_add_new_one', domain=domain)
# Check if apps are installed on the domain
- for app in os.listdir('/etc/yunohost/apps/'):
- with open('/etc/yunohost/apps/' + app + '/settings.yml') as f:
- try:
- app_domain = yaml.load(f)['domain']
- except:
- continue
- else:
- if app_domain == domain:
- raise YunohostError('domain_uninstall_app_first')
+ app_settings = [_get_app_settings(app) for app in _installed_apps()]
+ if any(s["domain"] == domain for s in app_settings):
+ raise YunohostError('domain_uninstall_app_first')
operation_logger.start()
ldap = _get_ldap_interface()
- if ldap.remove('virtualdomain=' + domain + ',ou=domains') or force:
- os.system('rm -rf /etc/yunohost/certs/%s' % domain)
- else:
- raise YunohostError('domain_deletion_failed')
+ try:
+ ldap.remove('virtualdomain=' + domain + ',ou=domains')
+ except Exception as e:
+ raise YunohostError('domain_deletion_failed', domain=domain, error=e)
+
+ os.system('rm -rf /etc/yunohost/certs/%s' % domain)
+
+ # Sometime we have weird issues with the regenconf where some files
+ # appears as manually modified even though they weren't touched ...
+ # There are a few ideas why this happens (like backup/restore nginx
+ # conf ... which we shouldnt do ...). This in turns creates funky
+ # situation where the regenconf may refuse to re-create the conf
+ # (when re-creating a domain..)
+ #
+ # So here we force-clear the has out of the regenconf if it exists.
+ # This is a pretty ad hoc solution and only applied to nginx
+ # because it's one of the major service, but in the long term we
+ # should identify the root of this bug...
+ _force_clear_hashes(["/etc/nginx/conf.d/%s.conf" % domain])
+ # And in addition we even force-delete the file Otherwise, if the file was
+ # manually modified, it may not get removed by the regenconf which leads to
+ # catastrophic consequences of nginx breaking because it can't load the
+ # cert file which disappeared etc..
+ if os.path.exists("/etc/nginx/conf.d/%s.conf" % domain):
+ _process_regen_conf("/etc/nginx/conf.d/%s.conf" % domain, new_conf=None, save=True)
regen_conf(names=['nginx', 'metronome', 'dnsmasq', 'postfix'])
app_ssowatconf()
@@ -222,22 +261,81 @@ def domain_dns_conf(domain, ttl=None):
for record in record_list:
result += "\n{name} {ttl} IN {type} {value}".format(**record)
- is_cli = True if msettings.get('interface') == 'cli' else False
- if is_cli:
+ if msettings.get('interface') == 'cli':
logger.info(m18n.n("domain_dns_conf_is_just_a_recommendation"))
return result
+@is_unit_operation()
+def domain_main_domain(operation_logger, new_main_domain=None):
+ """
+ Check the current main domain, or change it
+
+ Keyword argument:
+ new_main_domain -- The new domain to be set as the main domain
+
+ """
+ from yunohost.tools import _set_hostname
+
+ # If no new domain specified, we return the current main domain
+ if not new_main_domain:
+ return {'current_main_domain': _get_maindomain()}
+
+ # Check domain exists
+ if new_main_domain not in domain_list()['domains']:
+ raise YunohostError('domain_unknown')
+
+ operation_logger.related_to.append(('domain', new_main_domain))
+ operation_logger.start()
+
+ # Apply changes to ssl certs
+ ssl_key = "/etc/ssl/private/yunohost_key.pem"
+ ssl_crt = "/etc/ssl/private/yunohost_crt.pem"
+ new_ssl_key = "/etc/yunohost/certs/%s/key.pem" % new_main_domain
+ new_ssl_crt = "/etc/yunohost/certs/%s/crt.pem" % new_main_domain
+
+ try:
+ if os.path.exists(ssl_key) or os.path.lexists(ssl_key):
+ os.remove(ssl_key)
+ if os.path.exists(ssl_crt) or os.path.lexists(ssl_crt):
+ os.remove(ssl_crt)
+
+ os.symlink(new_ssl_key, ssl_key)
+ os.symlink(new_ssl_crt, ssl_crt)
+
+ _set_maindomain(new_main_domain)
+ except Exception as e:
+ logger.warning("%s" % e, exc_info=1)
+ raise YunohostError('main_domain_change_failed')
+
+ _set_hostname(new_main_domain)
+
+ # Generate SSOwat configuration file
+ app_ssowatconf()
+
+ # Regen configurations
+ try:
+ with open('/etc/yunohost/installed', 'r'):
+ regen_conf()
+ except IOError:
+ pass
+
+ logger.success(m18n.n('main_domain_changed'))
+
+
def domain_cert_status(domain_list, full=False):
+ import yunohost.certificate
return yunohost.certificate.certificate_status(domain_list, full)
def domain_cert_install(domain_list, force=False, no_checks=False, self_signed=False, staging=False):
+ import yunohost.certificate
return yunohost.certificate.certificate_install(domain_list, force, no_checks, self_signed, staging)
def domain_cert_renew(domain_list, force=False, no_checks=False, email=False, staging=False):
+ import yunohost.certificate
return yunohost.certificate.certificate_renew(domain_list, force, no_checks, email, staging)
@@ -322,7 +420,7 @@ def _normalize_domain_path(domain, path):
return domain, path
-def _build_dns_conf(domain, ttl=3600):
+def _build_dns_conf(domain, ttl=3600, include_empty_AAAA_if_no_ipv6=False):
"""
Internal function that will returns a data structure containing the needed
information to generate/adapt the dns configuration
@@ -332,10 +430,8 @@ def _build_dns_conf(domain, ttl=3600):
"basic": [
# if ipv4 available
{"type": "A", "name": "@", "value": "123.123.123.123", "ttl": 3600},
- {"type": "A", "name": "*", "value": "123.123.123.123", "ttl": 3600},
# if ipv6 available
{"type": "AAAA", "name": "@", "value": "valid-ipv6", "ttl": 3600},
- {"type": "AAAA", "name": "*", "value": "valid-ipv6", "ttl": 3600},
],
"xmpp": [
{"type": "SRV", "name": "_xmpp-client._tcp", "value": "0 5 5222 domain.tld.", "ttl": 3600},
@@ -343,6 +439,7 @@ def _build_dns_conf(domain, ttl=3600):
{"type": "CNAME", "name": "muc", "value": "@", "ttl": 3600},
{"type": "CNAME", "name": "pubsub", "value": "@", "ttl": 3600},
{"type": "CNAME", "name": "vjud", "value": "@", "ttl": 3600}
+ {"type": "CNAME", "name": "xmpp-upload", "value": "@", "ttl": 3600}
],
"mail": [
{"type": "MX", "name": "@", "value": "10 domain.tld.", "ttl": 3600},
@@ -351,6 +448,10 @@ def _build_dns_conf(domain, ttl=3600):
{"type": "TXT", "name": "_dmarc", "value": "\"v=DMARC1; p=none\"", "ttl": 3600}
],
"extra": [
+ # if ipv4 available
+ {"type": "A", "name": "*", "value": "123.123.123.123", "ttl": 3600},
+ # if ipv6 available
+ {"type": "AAAA", "name": "*", "value": "valid-ipv6", "ttl": 3600},
{"type": "CAA", "name": "@", "value": "128 issue \"letsencrypt.org\"", "ttl": 3600},
],
"example_of_a_custom_rule": [
@@ -362,42 +463,26 @@ def _build_dns_conf(domain, ttl=3600):
ipv4 = get_public_ip()
ipv6 = get_public_ip(6)
+ ###########################
+ # Basic ipv4/ipv6 records #
+ ###########################
+
basic = []
-
- # Basic ipv4/ipv6 records
if ipv4:
- basic += [
- ["@", ttl, "A", ipv4],
- ["*", ttl, "A", ipv4],
- ]
+ basic.append(["@", ttl, "A", ipv4])
if ipv6:
- basic += [
- ["@", ttl, "AAAA", ipv6],
- ["*", ttl, "AAAA", ipv6],
- ]
+ basic.append(["@", ttl, "AAAA", ipv6])
+ elif include_empty_AAAA_if_no_ipv6:
+ basic.append(["@", ttl, "AAAA", None])
- # XMPP
- xmpp = [
- ["_xmpp-client._tcp", ttl, "SRV", "0 5 5222 %s." % domain],
- ["_xmpp-server._tcp", ttl, "SRV", "0 5 5269 %s." % domain],
- ["muc", ttl, "CNAME", "@"],
- ["pubsub", ttl, "CNAME", "@"],
- ["vjud", ttl, "CNAME", "@"],
- ]
+ #########
+ # Email #
+ #########
- # SPF record
- spf_record = '"v=spf1 a mx'
- if ipv4:
- spf_record += ' ip4:{ip4}'.format(ip4=ipv4)
- if ipv6:
- spf_record += ' ip6:{ip6}'.format(ip6=ipv6)
- spf_record += ' -all"'
-
- # Email
mail = [
["@", ttl, "MX", "10 %s." % domain],
- ["@", ttl, "TXT", spf_record],
+ ["@", ttl, "TXT", '"v=spf1 a mx -all"'],
]
# DKIM/DMARC record
@@ -409,12 +494,39 @@ def _build_dns_conf(domain, ttl=3600):
["_dmarc", ttl, "TXT", '"v=DMARC1; p=none"'],
]
- # Extra
- extra = [
- ["@", ttl, "CAA", '128 issue "letsencrypt.org"']
+ ########
+ # XMPP #
+ ########
+
+ xmpp = [
+ ["_xmpp-client._tcp", ttl, "SRV", "0 5 5222 %s." % domain],
+ ["_xmpp-server._tcp", ttl, "SRV", "0 5 5269 %s." % domain],
+ ["muc", ttl, "CNAME", "@"],
+ ["pubsub", ttl, "CNAME", "@"],
+ ["vjud", ttl, "CNAME", "@"],
+ ["xmpp-upload", ttl, "CNAME", "@"],
]
- # Official record
+ #########
+ # Extra #
+ #########
+
+ extra = []
+
+ if ipv4:
+ extra.append(["*", ttl, "A", ipv4])
+
+ if ipv6:
+ extra.append(["*", ttl, "AAAA", ipv6])
+ elif include_empty_AAAA_if_no_ipv6:
+ extra.append(["*", ttl, "AAAA", None])
+
+ extra.append(["@", ttl, "CAA", '128 issue "letsencrypt.org"'])
+
+ ####################
+ # Standard records #
+ ####################
+
records = {
"basic": [{"name": name, "ttl": ttl, "type": type_, "value": value} for name, ttl, type_, value in basic],
"xmpp": [{"name": name, "ttl": ttl, "type": type_, "value": value} for name, ttl, type_, value in xmpp],
@@ -422,7 +534,12 @@ def _build_dns_conf(domain, ttl=3600):
"extra": [{"name": name, "ttl": ttl, "type": type_, "value": value} for name, ttl, type_, value in extra],
}
- # Custom records
+ ##################
+ # Custom records #
+ ##################
+
+ # Defined by custom hooks ships in apps for example ...
+
hook_results = hook_callback('custom_dns_rules', args=[domain])
for hook_name, results in hook_results.items():
#
diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py
index a35d39736..efa25f23f 100644
--- a/src/yunohost/dyndns.py
+++ b/src/yunohost/dyndns.py
@@ -212,7 +212,7 @@ def dyndns_update(operation_logger, dyn_host="dyndns.yunohost.org", domain=None,
from yunohost.tools import _get_migration_by_name
migration = _get_migration_by_name("migrate_to_tsig_sha256")
try:
- migration.migrate(dyn_host, domain, key)
+ migration.run(dyn_host, domain, key)
except Exception as e:
logger.error(m18n.n('migrations_migration_has_failed',
exception=e,
@@ -258,7 +258,12 @@ def dyndns_update(operation_logger, dyn_host="dyndns.yunohost.org", domain=None,
logger.info("Updated needed, going on...")
dns_conf = _build_dns_conf(domain)
- del dns_conf["extra"] # Ignore records from the 'extra' category
+
+ # Delete custom DNS records, we don't support them (have to explicitly
+ # authorize them on dynette)
+ for category in dns_conf.keys():
+ if category not in ["basic", "mail", "xmpp", "extra"]:
+ del dns_conf[category]
# Delete the old records for all domain/subdomains
diff --git a/src/yunohost/hook.py b/src/yunohost/hook.py
index 05c5a6b6b..40d3d114f 100644
--- a/src/yunohost/hook.py
+++ b/src/yunohost/hook.py
@@ -196,7 +196,7 @@ def hook_list(action, list_by='name', show_info=False):
else:
_append_folder(result, HOOK_FOLDER)
except OSError:
- logger.debug("system hook folder not found for action '%s' in %s",
+ logger.debug("No default hook for action '%s' in %s",
action, HOOK_FOLDER)
try:
@@ -207,7 +207,7 @@ def hook_list(action, list_by='name', show_info=False):
else:
_append_folder(result, CUSTOM_HOOK_FOLDER)
except OSError:
- logger.debug("custom hook folder not found for action '%s' in %s",
+ logger.debug("No custom hook for action '%s' in %s",
action, CUSTOM_HOOK_FOLDER)
return {'hooks': result}
diff --git a/src/yunohost/log.py b/src/yunohost/log.py
index cbb850e44..cd08bdfe0 100644
--- a/src/yunohost/log.py
+++ b/src/yunohost/log.py
@@ -44,7 +44,7 @@ CATEGORIES = ['operation', 'history', 'package', 'system', 'access', 'service',
'app']
METADATA_FILE_EXT = '.yml'
LOG_FILE_EXT = '.log'
-RELATED_CATEGORIES = ['app', 'domain', 'service', 'user']
+RELATED_CATEGORIES = ['app', 'domain', 'group', 'service', 'user']
logger = getActionLogger('yunohost.log')
@@ -213,7 +213,7 @@ def log_display(path, number=None, share=False):
return infos
-def is_unit_operation(entities=['app', 'domain', 'service', 'user'],
+def is_unit_operation(entities=['app', 'domain', 'group', 'service', 'user'],
exclude=['password'], operation_key=None):
"""
Configure quickly a unit operation
@@ -315,8 +315,9 @@ class RedactingFormatter(Formatter):
try:
# This matches stuff like db_pwd=the_secret or admin_password=other_secret
# (the secret part being at least 3 chars to avoid catching some lines like just "db_pwd=")
- match = re.search(r'(pwd|pass|password|secret|key|token)=(\S{3,})$', record.strip())
- if match and match.group(2) not in self.data_to_redact:
+ # Some names like "key" or "manifest_key" are ignored, used in helpers like ynh_app_setting_set or ynh_read_manifest
+ match = re.search(r'(pwd|pass|password|secret|\w+key|token)=(\S{3,})$', record.strip())
+ if match and match.group(2) not in self.data_to_redact and match.group(1) not in ["key", "manifest_key"]:
self.data_to_redact.append(match.group(2))
except Exception as e:
logger.warning("Failed to parse line to try to identify data to redact ... : %s" % e)
@@ -502,7 +503,10 @@ class OperationLogger(object):
The missing of the message below could help to see an electrical
shortage.
"""
- self.error(m18n.n('log_operation_unit_unclosed_properly'))
+ if self.ended_at is not None or self.started_at is None:
+ return
+ else:
+ self.error(m18n.n('log_operation_unit_unclosed_properly'))
def _get_description_from_name(name):
diff --git a/src/yunohost/monitor.py b/src/yunohost/monitor.py
deleted file mode 100644
index 7af55f287..000000000
--- a/src/yunohost/monitor.py
+++ /dev/null
@@ -1,740 +0,0 @@
-# -*- coding: utf-8 -*-
-
-""" License
-
- Copyright (C) 2013 YunoHost
-
- 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_monitor.py
-
- Monitoring functions
-"""
-import re
-import json
-import time
-import psutil
-import calendar
-import subprocess
-import xmlrpclib
-import os.path
-import os
-import dns.resolver
-import cPickle as pickle
-from datetime import datetime
-
-from moulinette import m18n
-from yunohost.utils.error import YunohostError
-from moulinette.utils.log import getActionLogger
-
-from yunohost.utils.network import get_public_ip
-from yunohost.domain import _get_maindomain
-
-logger = getActionLogger('yunohost.monitor')
-
-GLANCES_URI = 'http://127.0.0.1:61209'
-STATS_PATH = '/var/lib/yunohost/stats'
-CRONTAB_PATH = '/etc/cron.d/yunohost-monitor'
-
-
-def monitor_disk(units=None, mountpoint=None, human_readable=False):
- """
- Monitor disk space and usage
-
- Keyword argument:
- units -- Unit(s) to monitor
- mountpoint -- Device mountpoint
- human_readable -- Print sizes in human readable format
-
- """
- glances = _get_glances_api()
- result_dname = None
- result = {}
-
- if units is None:
- units = ['io', 'filesystem']
-
- _format_dname = lambda d: (os.path.realpath(d)).replace('/dev/', '')
-
- # Get mounted devices
- devices = {}
- for p in psutil.disk_partitions(all=True):
- if not p.device.startswith('/dev/') or not p.mountpoint:
- continue
- if mountpoint is None:
- devices[_format_dname(p.device)] = p.mountpoint
- elif mountpoint == p.mountpoint:
- dn = _format_dname(p.device)
- devices[dn] = p.mountpoint
- result_dname = dn
- if len(devices) == 0:
- if mountpoint is not None:
- raise YunohostError('mountpoint_unknown')
- return result
-
- # Retrieve monitoring for unit(s)
- for u in units:
- if u == 'io':
- # Define setter
- if len(units) > 1:
- def _set(dn, dvalue):
- try:
- result[dn][u] = dvalue
- except KeyError:
- result[dn] = {u: dvalue}
- else:
- def _set(dn, dvalue):
- result[dn] = dvalue
-
- # Iterate over values
- devices_names = devices.keys()
- for d in json.loads(glances.getDiskIO()):
- dname = d.pop('disk_name')
- try:
- devices_names.remove(dname)
- except:
- continue
- else:
- _set(dname, d)
- for dname in devices_names:
- _set(dname, 'not-available')
- elif u == 'filesystem':
- # Define setter
- if len(units) > 1:
- def _set(dn, dvalue):
- try:
- result[dn][u] = dvalue
- except KeyError:
- result[dn] = {u: dvalue}
- else:
- def _set(dn, dvalue):
- result[dn] = dvalue
-
- # Iterate over values
- devices_names = devices.keys()
- for d in json.loads(glances.getFs()):
- dname = _format_dname(d.pop('device_name'))
- try:
- devices_names.remove(dname)
- except:
- continue
- else:
- d['avail'] = d['size'] - d['used']
- if human_readable:
- for i in ['used', 'avail', 'size']:
- d[i] = binary_to_human(d[i]) + 'B'
- _set(dname, d)
- for dname in devices_names:
- _set(dname, 'not-available')
- else:
- raise YunohostError('unit_unknown', unit=u)
-
- if result_dname is not None:
- return result[result_dname]
- return result
-
-
-def monitor_network(units=None, human_readable=False):
- """
- Monitor network interfaces
-
- Keyword argument:
- units -- Unit(s) to monitor
- human_readable -- Print sizes in human readable format
-
- """
- glances = _get_glances_api()
- result = {}
-
- if units is None:
- units = ['check', 'usage', 'infos']
-
- # Get network devices and their addresses
- # TODO / FIXME : use functions in utils/network.py to manage this
- devices = {}
- output = subprocess.check_output('ip addr show'.split())
- for d in re.split('^(?:[0-9]+: )', output, flags=re.MULTILINE):
- # Extract device name (1) and its addresses (2)
- m = re.match('([^\s@]+)(?:@[\S]+)?: (.*)', d, flags=re.DOTALL)
- if m:
- devices[m.group(1)] = m.group(2)
-
- # Retrieve monitoring for unit(s)
- for u in units:
- if u == 'check':
- result[u] = {}
- domain = _get_maindomain()
- cmd_check_smtp = os.system('/bin/nc -z -w1 yunohost.org 25')
- if cmd_check_smtp == 0:
- smtp_check = m18n.n('network_check_smtp_ok')
- else:
- smtp_check = m18n.n('network_check_smtp_ko')
-
- try:
- answers = dns.resolver.query(domain, 'MX')
- mx_check = {}
- i = 0
- for server in answers:
- mx_id = 'mx%s' % i
- mx_check[mx_id] = server
- i = i + 1
- except:
- mx_check = m18n.n('network_check_mx_ko')
- result[u] = {
- 'smtp_check': smtp_check,
- 'mx_check': mx_check
- }
- elif u == 'usage':
- result[u] = {}
- for i in json.loads(glances.getNetwork()):
- iname = i['interface_name']
- if iname in devices.keys():
- del i['interface_name']
- if human_readable:
- for k in i.keys():
- if k != 'time_since_update':
- i[k] = binary_to_human(i[k]) + 'B'
- result[u][iname] = i
- else:
- logger.debug('interface name %s was not found', iname)
- elif u == 'infos':
- p_ipv4 = get_public_ip() or 'unknown'
-
- # TODO / FIXME : use functions in utils/network.py to manage this
- l_ip = 'unknown'
- for name, addrs in devices.items():
- if name == 'lo':
- continue
- if not isinstance(l_ip, dict):
- l_ip = {}
- l_ip[name] = _extract_inet(addrs)
-
- gateway = 'unknown'
- output = subprocess.check_output('ip route show'.split())
- m = re.search('default via (.*) dev ([a-z]+[0-9]?)', output)
- if m:
- addr = _extract_inet(m.group(1), True)
- if len(addr) == 1:
- proto, gateway = addr.popitem()
-
- result[u] = {
- 'public_ip': p_ipv4,
- 'local_ip': l_ip,
- 'gateway': gateway,
- }
- else:
- raise YunohostError('unit_unknown', unit=u)
-
- if len(units) == 1:
- return result[units[0]]
- return result
-
-
-def monitor_system(units=None, human_readable=False):
- """
- Monitor system informations and usage
-
- Keyword argument:
- units -- Unit(s) to monitor
- human_readable -- Print sizes in human readable format
-
- """
- glances = _get_glances_api()
- result = {}
-
- if units is None:
- units = ['memory', 'cpu', 'process', 'uptime', 'infos']
-
- # Retrieve monitoring for unit(s)
- for u in units:
- if u == 'memory':
- ram = json.loads(glances.getMem())
- swap = json.loads(glances.getMemSwap())
- if human_readable:
- for i in ram.keys():
- if i != 'percent':
- ram[i] = binary_to_human(ram[i]) + 'B'
- for i in swap.keys():
- if i != 'percent':
- swap[i] = binary_to_human(swap[i]) + 'B'
- result[u] = {
- 'ram': ram,
- 'swap': swap
- }
- elif u == 'cpu':
- result[u] = {
- 'load': json.loads(glances.getLoad()),
- 'usage': json.loads(glances.getCpu())
- }
- elif u == 'process':
- result[u] = json.loads(glances.getProcessCount())
- elif u == 'uptime':
- result[u] = (str(datetime.now() - datetime.fromtimestamp(psutil.boot_time())).split('.')[0])
- elif u == 'infos':
- result[u] = json.loads(glances.getSystem())
- else:
- raise YunohostError('unit_unknown', unit=u)
-
- if len(units) == 1 and not isinstance(result[units[0]], str):
- return result[units[0]]
- return result
-
-
-def monitor_update_stats(period):
- """
- Update monitoring statistics
-
- Keyword argument:
- period -- Time period to update (day, week, month)
-
- """
- if period not in ['day', 'week', 'month']:
- raise YunohostError('monitor_period_invalid')
-
- stats = _retrieve_stats(period)
- if not stats:
- stats = {'disk': {}, 'network': {}, 'system': {}, 'timestamp': []}
-
- monitor = None
- # Get monitoring stats
- if period == 'day':
- monitor = _monitor_all('day')
- else:
- t = stats['timestamp']
- p = 'day' if period == 'week' else 'week'
- if len(t) > 0:
- monitor = _monitor_all(p, t[len(t) - 1])
- else:
- monitor = _monitor_all(p, 0)
- if not monitor:
- raise YunohostError('monitor_stats_no_update')
-
- stats['timestamp'].append(time.time())
-
- # Append disk stats
- for dname, units in monitor['disk'].items():
- disk = {}
- # Retrieve current stats for disk name
- if dname in stats['disk'].keys():
- disk = stats['disk'][dname]
-
- for unit, values in units.items():
- # Continue if unit doesn't contain stats
- if not isinstance(values, dict):
- continue
-
- # Retrieve current stats for unit and append new ones
- curr = disk[unit] if unit in disk.keys() else {}
- if unit == 'io':
- disk[unit] = _append_to_stats(curr, values, 'time_since_update')
- elif unit == 'filesystem':
- disk[unit] = _append_to_stats(curr, values, ['fs_type', 'mnt_point'])
- stats['disk'][dname] = disk
-
- # Append network stats
- net_usage = {}
- for iname, values in monitor['network']['usage'].items():
- # Continue if units doesn't contain stats
- if not isinstance(values, dict):
- continue
-
- # Retrieve current stats and append new ones
- curr = {}
- if 'usage' in stats['network'] and iname in stats['network']['usage']:
- curr = stats['network']['usage'][iname]
- net_usage[iname] = _append_to_stats(curr, values, 'time_since_update')
- stats['network'] = {'usage': net_usage, 'infos': monitor['network']['infos']}
-
- # Append system stats
- for unit, values in monitor['system'].items():
- # Continue if units doesn't contain stats
- if not isinstance(values, dict):
- continue
-
- # Set static infos unit
- if unit == 'infos':
- stats['system'][unit] = values
- continue
-
- # Retrieve current stats and append new ones
- curr = stats['system'][unit] if unit in stats['system'].keys() else {}
- stats['system'][unit] = _append_to_stats(curr, values)
-
- _save_stats(stats, period)
-
-
-def monitor_show_stats(period, date=None):
- """
- Show monitoring statistics
-
- Keyword argument:
- period -- Time period to show (day, week, month)
-
- """
- if period not in ['day', 'week', 'month']:
- raise YunohostError('monitor_period_invalid')
-
- result = _retrieve_stats(period, date)
- if result is False:
- raise YunohostError('monitor_stats_file_not_found')
- elif result is None:
- raise YunohostError('monitor_stats_period_unavailable')
- return result
-
-
-def monitor_enable(with_stats=False):
- """
- Enable server monitoring
-
- Keyword argument:
- with_stats -- Enable monitoring statistics
-
- """
- from yunohost.service import (service_status, service_enable,
- service_start)
-
- glances = service_status('glances')
- if glances['status'] != 'running':
- service_start('glances')
- if glances['loaded'] != 'enabled':
- service_enable('glances')
-
- # Install crontab
- if with_stats:
- # day: every 5 min # week: every 1 h # month: every 4 h #
- rules = ('*/5 * * * * root {cmd} day >> /dev/null\n'
- '3 * * * * root {cmd} week >> /dev/null\n'
- '6 */4 * * * root {cmd} month >> /dev/null').format(
- cmd='/usr/bin/yunohost --quiet monitor update-stats')
- with open(CRONTAB_PATH, 'w') as f:
- f.write(rules)
-
- logger.success(m18n.n('monitor_enabled'))
-
-
-def monitor_disable():
- """
- Disable server monitoring
-
- """
- from yunohost.service import (service_status, service_disable,
- service_stop)
-
- glances = service_status('glances')
- if glances['status'] != 'inactive':
- service_stop('glances')
- if glances['loaded'] != 'disabled':
- try:
- service_disable('glances')
- except YunohostError as e:
- logger.warning(e.strerror)
-
- # Remove crontab
- try:
- os.remove(CRONTAB_PATH)
- except:
- pass
-
- logger.success(m18n.n('monitor_disabled'))
-
-
-def _get_glances_api():
- """
- Retrieve Glances API running on the local server
-
- """
- try:
- p = xmlrpclib.ServerProxy(GLANCES_URI)
- p.system.methodHelp('getAll')
- except (xmlrpclib.ProtocolError, IOError):
- pass
- else:
- return p
-
- from yunohost.service import service_status
-
- if service_status('glances')['status'] != 'running':
- raise YunohostError('monitor_not_enabled')
- raise YunohostError('monitor_glances_con_failed')
-
-
-def _extract_inet(string, skip_netmask=False, skip_loopback=True):
- """
- Extract IP addresses (v4 and/or v6) from a string limited to one
- address by protocol
-
- Keyword argument:
- string -- String to search in
- skip_netmask -- True to skip subnet mask extraction
- skip_loopback -- False to include addresses reserved for the
- loopback interface
-
- Returns:
- A dict of {protocol: address} with protocol one of 'ipv4' or 'ipv6'
-
- """
- ip4_pattern = '((25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}'
- ip6_pattern = '(((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)::((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)'
- ip4_pattern += '/[0-9]{1,2})' if not skip_netmask else ')'
- ip6_pattern += '/[0-9]{1,3})' if not skip_netmask else ')'
- result = {}
-
- for m in re.finditer(ip4_pattern, string):
- addr = m.group(1)
- if skip_loopback and addr.startswith('127.'):
- continue
-
- # Limit to only one result
- result['ipv4'] = addr
- break
-
- for m in re.finditer(ip6_pattern, string):
- addr = m.group(1)
- if skip_loopback and addr == '::1':
- continue
-
- # Limit to only one result
- result['ipv6'] = addr
- break
-
- return result
-
-
-def binary_to_human(n, customary=False):
- """
- Convert bytes or bits into human readable format with binary prefix
-
- Keyword argument:
- n -- Number to convert
- customary -- Use customary symbol instead of IEC standard
-
- """
- symbols = ('Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi')
- if customary:
- symbols = ('K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y')
- prefix = {}
- for i, s in enumerate(symbols):
- prefix[s] = 1 << (i + 1) * 10
- for s in reversed(symbols):
- if n >= prefix[s]:
- value = float(n) / prefix[s]
- return '%.1f%s' % (value, s)
- return "%s" % n
-
-
-def _retrieve_stats(period, date=None):
- """
- Retrieve statistics from pickle file
-
- Keyword argument:
- period -- Time period to retrieve (day, week, month)
- date -- Date of stats to retrieve
-
- """
- pkl_file = None
-
- # Retrieve pickle file
- if date is not None:
- timestamp = calendar.timegm(date)
- pkl_file = '%s/%d_%s.pkl' % (STATS_PATH, timestamp, period)
- else:
- pkl_file = '%s/%s.pkl' % (STATS_PATH, period)
- if not os.path.isfile(pkl_file):
- return False
-
- # Read file and process its content
- with open(pkl_file, 'r') as f:
- result = pickle.load(f)
- if not isinstance(result, dict):
- return None
- return result
-
-
-def _save_stats(stats, period, date=None):
- """
- Save statistics to pickle file
-
- Keyword argument:
- stats -- Stats dict to save
- period -- Time period of stats (day, week, month)
- date -- Date of stats
-
- """
- pkl_file = None
-
- # Set pickle file name
- if date is not None:
- timestamp = calendar.timegm(date)
- pkl_file = '%s/%d_%s.pkl' % (STATS_PATH, timestamp, period)
- else:
- pkl_file = '%s/%s.pkl' % (STATS_PATH, period)
- if not os.path.isdir(STATS_PATH):
- os.makedirs(STATS_PATH)
-
- # Limit stats
- if date is None:
- t = stats['timestamp']
- limit = {'day': 86400, 'week': 604800, 'month': 2419200}
- if (t[len(t) - 1] - t[0]) > limit[period]:
- begin = t[len(t) - 1] - limit[period]
- stats = _filter_stats(stats, begin)
-
- # Write file content
- with open(pkl_file, 'w') as f:
- pickle.dump(stats, f)
- return True
-
-
-def _monitor_all(period=None, since=None):
- """
- Monitor all units (disk, network and system) for the given period
- If since is None, real-time monitoring is returned. Otherwise, the
- mean of stats since this timestamp is calculated and returned.
-
- Keyword argument:
- period -- Time period to monitor (day, week, month)
- since -- Timestamp of the stats beginning
-
- """
- result = {'disk': {}, 'network': {}, 'system': {}}
-
- # Real-time stats
- if period == 'day' and since is None:
- result['disk'] = monitor_disk()
- result['network'] = monitor_network()
- result['system'] = monitor_system()
- return result
-
- # Retrieve stats and calculate mean
- stats = _retrieve_stats(period)
- if not stats:
- return None
- stats = _filter_stats(stats, since)
- if not stats:
- return None
- result = _calculate_stats_mean(stats)
-
- return result
-
-
-def _filter_stats(stats, t_begin=None, t_end=None):
- """
- Filter statistics by beginning and/or ending timestamp
-
- Keyword argument:
- stats -- Dict stats to filter
- t_begin -- Beginning timestamp
- t_end -- Ending timestamp
-
- """
- if t_begin is None and t_end is None:
- return stats
-
- i_begin = i_end = None
- # Look for indexes of timestamp interval
- for i, t in enumerate(stats['timestamp']):
- if t_begin and i_begin is None and t >= t_begin:
- i_begin = i
- if t_end and i != 0 and i_end is None and t > t_end:
- i_end = i
- # Check indexes
- if i_begin is None:
- if t_begin and t_begin > stats['timestamp'][0]:
- return None
- i_begin = 0
- if i_end is None:
- if t_end and t_end < stats['timestamp'][0]:
- return None
- i_end = len(stats['timestamp'])
- if i_begin == 0 and i_end == len(stats['timestamp']):
- return stats
-
- # Filter function
- def _filter(s, i, j):
- for k, v in s.items():
- if isinstance(v, dict):
- s[k] = _filter(v, i, j)
- elif isinstance(v, list):
- s[k] = v[i:j]
- return s
-
- stats = _filter(stats, i_begin, i_end)
- return stats
-
-
-def _calculate_stats_mean(stats):
- """
- Calculate the weighted mean for each statistic
-
- Keyword argument:
- stats -- Stats dict to process
-
- """
- timestamp = stats['timestamp']
- t_sum = sum(timestamp)
- del stats['timestamp']
-
- # Weighted mean function
- def _mean(s, t, ts):
- for k, v in s.items():
- if isinstance(v, dict):
- s[k] = _mean(v, t, ts)
- elif isinstance(v, list):
- try:
- nums = [float(x * t[i]) for i, x in enumerate(v)]
- except:
- pass
- else:
- s[k] = sum(nums) / float(ts)
- return s
-
- stats = _mean(stats, timestamp, t_sum)
- return stats
-
-
-def _append_to_stats(stats, monitor, statics=[]):
- """
- Append monitoring statistics to current statistics
-
- Keyword argument:
- stats -- Current stats dict
- monitor -- Monitoring statistics
- statics -- List of stats static keys
-
- """
- if isinstance(statics, str):
- statics = [statics]
-
- # Appending function
- def _append(s, m, st):
- for k, v in m.items():
- if k in st:
- s[k] = v
- elif isinstance(v, dict):
- if k not in s:
- s[k] = {}
- s[k] = _append(s[k], v, st)
- else:
- if k not in s:
- s[k] = []
- if isinstance(v, list):
- s[k].extend(v)
- else:
- s[k].append(v)
- return s
-
- stats = _append(stats, monitor, statics)
- return stats
diff --git a/src/yunohost/permission.py b/src/yunohost/permission.py
index f7fa307da..67a0f57b0 100644
--- a/src/yunohost/permission.py
+++ b/src/yunohost/permission.py
@@ -24,6 +24,7 @@
Manage permissions
"""
+import copy
import grp
import random
@@ -35,309 +36,231 @@ from yunohost.log import is_unit_operation
logger = getActionLogger('yunohost.user')
+SYSTEM_PERMS = ["mail", "xmpp", "stfp"]
-def user_permission_list(app=None, permission=None, username=None, group=None):
+#
+#
+# The followings are the methods exposed through the "yunohost user permission" interface
+#
+#
+
+
+def user_permission_list(short=False, full=False, ignore_system_perms=False):
"""
- List permission for specific application
-
- Keyword argument:
- app -- an application OR sftp, xmpp (metronome), mail
- permission -- name of the permission ("main" by default)
- username -- Username to get informations
- group -- Groupname to get informations
-
+ List permissions and corresponding accesses
"""
- from yunohost.utils.ldap import _get_ldap_interface
+ # Fetch relevant informations
+
+ from yunohost.utils.ldap import _get_ldap_interface, _ldap_path_extract
ldap = _get_ldap_interface()
+ permissions_infos = ldap.search('ou=permission,dc=yunohost,dc=org',
+ '(objectclass=permissionYnh)',
+ ["cn", 'groupPermission', 'inheritPermission', 'URL'])
- permission_attrs = [
- 'cn',
- 'groupPermission',
- 'inheritPermission',
- 'URL',
- ]
-
- # Normally app is alway defined but it should be possible to set it
- if app and not isinstance(app, list):
- app = [app]
- if permission and not isinstance(permission, list):
- permission = [permission]
- if not isinstance(username, list):
- username = [username]
- if not isinstance(group, list):
- group = [group]
+ # Parse / organize information to be outputed
permissions = {}
+ for infos in permissions_infos:
- result = ldap.search('ou=permission,dc=yunohost,dc=org',
- '(objectclass=permissionYnh)', permission_attrs)
+ name = infos['cn'][0]
- for res in result:
- try:
- permission_name, app_name = res['cn'][0].split('.')
- except:
- logger.warning(m18n.n('permission_name_not_valid', permission=res['cn'][0]))
- group_name = []
- if 'groupPermission' in res:
- for g in res['groupPermission']:
- group_name.append(g.split("=")[1].split(",")[0])
- user_name = []
- if 'inheritPermission' in res:
- for u in res['inheritPermission']:
- user_name.append(u.split("=")[1].split(",")[0])
-
- # Don't show the result if the user defined a specific permission, user or group
- if app and app_name not in app:
- continue
- if permission and permission_name not in permission:
- continue
- if username[0] and not set(username) & set(user_name):
- continue
- if group[0] and not set(group) & set(group_name):
+ if ignore_system_perms and name.split(".")[0] in SYSTEM_PERMS:
continue
- if app_name not in permissions:
- permissions[app_name] = {}
+ permissions[name] = {}
+ permissions[name]["allowed"] = [_ldap_path_extract(p, "cn") for p in infos.get('groupPermission', [])]
- permissions[app_name][permission_name] = {'allowed_users': [], 'allowed_groups': []}
- for g in group_name:
- permissions[app_name][permission_name]['allowed_groups'].append(g)
- for u in user_name:
- permissions[app_name][permission_name]['allowed_users'].append(u)
- if 'URL' in res:
- permissions[app_name][permission_name]['URL'] = []
- for u in res['URL']:
- permissions[app_name][permission_name]['URL'].append(u)
+ if full:
+ permissions[name]["corresponding_users"] = [_ldap_path_extract(p, "uid") for p in infos.get('inheritPermission', [])]
+ permissions[name]["url"] = infos.get("URL", [None])[0]
+
+ if short:
+ permissions = permissions.keys()
return {'permissions': permissions}
-def user_permission_update(operation_logger, app=[], permission=None, add_username=None, add_group=None, del_username=None, del_group=None, sync_perm=True):
+@is_unit_operation()
+def user_permission_update(operation_logger, permission, add=None, remove=None, sync_perm=True):
"""
Allow or Disallow a user or group to a permission for a specific application
Keyword argument:
- app -- an application OR sftp, xmpp (metronome), mail
- permission -- name of the permission ("main" by default)
- add_username -- Username to allow
- add_group -- Groupname to allow
- del_username -- Username to disallow
- del_group -- Groupname to disallow
-
+ permission -- Name of the permission (e.g. mail or or wordpress or wordpress.editors)
+ add -- List of groups or usernames to add to this permission
+ remove -- List of groups or usernames to remove from to this permission
"""
- from yunohost.hook import hook_callback
from yunohost.user import user_group_list
- from yunohost.utils.ldap import _get_ldap_interface
- ldap = _get_ldap_interface()
- if permission:
- if not isinstance(permission, list):
- permission = [permission]
- else:
- permission = ["main"]
+ # By default, manipulate main permission
+ if "." not in permission:
+ permission = permission + ".main"
- if add_group:
- if not isinstance(add_group, list):
- add_group = [add_group]
- else:
- add_group = []
+ # Refuse to add "visitors" to mail, xmpp ... they require an account to make sense.
+ if add and "visitors" in add and permission.split(".")[0] in SYSTEM_PERMS:
+ raise YunohostError('permission_require_account', permission=permission)
- if add_username:
- if not isinstance(add_username, list):
- add_username = [add_username]
- else:
- add_username = []
+ # Fetch currently allowed groups for this permission
- if del_group:
- if not isinstance(del_group, list):
- del_group = [del_group]
- else:
- del_group = []
+ existing_permission = user_permission_list(full=True)["permissions"].get(permission, None)
+ if existing_permission is None:
+ raise YunohostError('permission_not_found', permission=permission)
- if del_username:
- if not isinstance(del_username, list):
- del_username = [del_username]
- else:
- del_username = []
+ current_allowed_groups = existing_permission["allowed"]
+ operation_logger.related_to.append(('app', permission.split(".")[0]))
- # Validate that the group exist
- for g in add_group:
- if g not in user_group_list(['cn'])['groups']:
- raise YunohostError('group_unknown', group=g)
- for u in add_username:
- if u not in user_list(['uid'])['users']:
- raise YunohostError('user_unknown', user=u)
- for g in del_group:
- if g not in user_group_list(['cn'])['groups']:
- raise YunohostError('group_unknown', group=g)
- for u in del_username:
- if u not in user_list(['uid'])['users']:
- raise YunohostError('user_unknown', user=u)
+ # Compute new allowed group list (and make sure what we're doing make sense)
- # Merge user and group (note that we consider all user as a group)
- add_group.extend(add_username)
- del_group.extend(del_username)
+ new_allowed_groups = copy.copy(current_allowed_groups)
+ all_existing_groups = user_group_list()['groups'].keys()
- if 'all_users' in add_group or 'all_users' in del_group:
- raise YunohostError('edit_permission_with_group_all_users_not_allowed')
+ if add:
+ groups_to_add = [add] if not isinstance(add, list) else add
+ for group in groups_to_add:
+ if group not in all_existing_groups:
+ raise YunohostError('group_unknown', group=group)
+ if group in current_allowed_groups:
+ logger.warning(m18n.n('permission_already_allowed', permission=permission, group=group))
+ else:
+ operation_logger.related_to.append(('group', group))
- # Populate permission informations
- permission_attrs = [
- 'cn',
- 'groupPermission',
- ]
- result = ldap.search('ou=permission,dc=yunohost,dc=org',
- '(objectclass=permissionYnh)', permission_attrs)
- result = {p['cn'][0]: p for p in result}
+ new_allowed_groups += groups_to_add
- new_per_dict = {}
+ if remove:
+ groups_to_remove = [remove] if not isinstance(remove, list) else remove
+ for group in groups_to_remove:
+ if group not in current_allowed_groups:
+ logger.warning(m18n.n('permission_already_disallowed', permission=permission, group=group))
+ else:
+ operation_logger.related_to.append(('group', group))
- for a in app:
- for per in permission:
- permission_name = per + '.' + a
- if permission_name not in result:
- raise YunohostError('permission_not_found', permission=per, app=a)
- new_per_dict[permission_name] = set()
- if 'groupPermission' in result[permission_name]:
- new_per_dict[permission_name] = set(result[permission_name]['groupPermission'])
+ new_allowed_groups = [g for g in new_allowed_groups if g not in groups_to_remove]
- for g in del_group:
- if 'cn=all_users,ou=groups,dc=yunohost,dc=org' in new_per_dict[permission_name]:
- raise YunohostError('need_define_permission_before')
- group_name = 'cn=' + g + ',ou=groups,dc=yunohost,dc=org'
- if group_name not in new_per_dict[permission_name]:
- logger.warning(m18n.n('group_already_disallowed', permission=per, app=a, group=g))
- else:
- new_per_dict[permission_name].remove(group_name)
+ # If we end up with something like allowed groups is ["all_users", "volunteers"]
+ # we shall warn the users that they should probably choose between one or
+ # the other, because the current situation is probably not what they expect
+ # / is temporary ? Note that it's fine to have ["all_users", "visitors"]
+ # though, but it's not fine to have ["all_users", "visitors", "volunteers"]
+ if "all_users" in new_allowed_groups and len(new_allowed_groups) >= 2:
+ if "visitors" not in new_allowed_groups or len(new_allowed_groups) >= 3:
+ logger.warning(m18n.n("permission_currently_allowed_for_all_users"))
- if 'cn=all_users,ou=groups,dc=yunohost,dc=org' in new_per_dict[permission_name]:
- new_per_dict[permission_name].remove('cn=all_users,ou=groups,dc=yunohost,dc=org')
- for g in add_group:
- group_name = 'cn=' + g + ',ou=groups,dc=yunohost,dc=org'
- if group_name in new_per_dict[permission_name]:
- logger.warning(m18n.n('group_already_allowed', permission=per, app=a, group=g))
- else:
- new_per_dict[permission_name].add(group_name)
+ # Don't update LDAP if we update exactly the same values
+ if set(new_allowed_groups) == set(current_allowed_groups):
+ logger.warning(m18n.n("permission_already_up_to_date"))
+ return existing_permission
+
+ # Commit the new allowed group list
operation_logger.start()
- for per, val in new_per_dict.items():
- # Don't update LDAP if we update exactly the same values
- if val == set(result[per]['groupPermission'] if 'groupPermission' in result[per] else []):
- continue
- if ldap.update('cn=%s,ou=permission' % per, {'groupPermission': val}):
- p = per.split('.')
- logger.debug(m18n.n('permission_updated', permission=p[0], app=p[1]))
- else:
- raise YunohostError('permission_update_failed')
+ new_permission = _update_ldap_group_permission(permission=permission, allowed=new_allowed_groups, sync_perm=sync_perm)
- if sync_perm:
- permission_sync_to_user()
+ logger.debug(m18n.n('permission_updated', permission=permission))
- for a in app:
- allowed_users = set()
- disallowed_users = set()
- group_list = user_group_list(['member'])['groups']
-
- for g in add_group:
- if 'members' in group_list[g]:
- allowed_users.union(group_list[g]['members'])
- for g in del_group:
- if 'members' in group_list[g]:
- disallowed_users.union(group_list[g]['members'])
-
- allowed_users = ','.join(allowed_users)
- disallowed_users = ','.join(disallowed_users)
- if add_group:
- hook_callback('post_app_addaccess', args=[app, allowed_users])
- if del_group:
- hook_callback('post_app_removeaccess', args=[app, disallowed_users])
-
- return user_permission_list(app, permission)
+ return new_permission
-def user_permission_clear(operation_logger, app=[], permission=None, sync_perm=True):
+@is_unit_operation()
+def user_permission_reset(operation_logger, permission, sync_perm=True):
"""
- Reset the permission for a specific application
+ Reset a given permission to just 'all_users'
Keyword argument:
- app -- an application OR sftp, xmpp (metronome), mail
- permission -- name of the permission ("main" by default)
- username -- Username to get informations (all by default)
- group -- Groupname to get informations (all by default)
-
+ permission -- Name of the permission (e.g. mail or nextcloud or wordpress.editors)
"""
- from yunohost.hook import hook_callback
- from yunohost.utils.ldap import _get_ldap_interface
- ldap = _get_ldap_interface()
- if permission:
- if not isinstance(permission, list):
- permission = [permission]
- else:
- permission = ["main"]
+ # By default, manipulate main permission
+ if "." not in permission:
+ permission = permission + ".main"
- default_permission = {'groupPermission': ['cn=all_users,ou=groups,dc=yunohost,dc=org']}
+ # Fetch existing permission
- # Populate permission informations
- permission_attrs = [
- 'cn',
- 'groupPermission',
- ]
- result = ldap.search('ou=permission,dc=yunohost,dc=org',
- '(objectclass=permissionYnh)', permission_attrs)
- result = {p['cn'][0]: p for p in result}
+ existing_permission = user_permission_list(full=True)["permissions"].get(permission, None)
+ if existing_permission is None:
+ raise YunohostError('permission_not_found', permission=permission)
- for a in app:
- for per in permission:
- permission_name = per + '.' + a
- if permission_name not in result:
- raise YunohostError('permission_not_found', permission=per, app=a)
- if 'groupPermission' in result[permission_name] and 'cn=all_users,ou=groups,dc=yunohost,dc=org' in result[permission_name]['groupPermission']:
- logger.warning(m18n.n('permission_already_clear', permission=per, app=a))
- continue
- if ldap.update('cn=%s,ou=permission' % permission_name, default_permission):
- logger.debug(m18n.n('permission_updated', permission=per, app=a))
- else:
- raise YunohostError('permission_update_failed')
+ if existing_permission["allowed"] == ["all_users"]:
+ logger.warning(m18n.n("permission_already_up_to_date"))
+ return
- permission_sync_to_user()
+ # Update permission with default (all_users)
- for a in app:
- permission_name = 'main.' + a
- result = ldap.search('ou=permission,dc=yunohost,dc=org',
- filter='cn=' + permission_name, attrs=['inheritPermission'])
- if result:
- allowed_users = result[0]['inheritPermission']
- new_user_list = ','.join(allowed_users)
- hook_callback('post_app_removeaccess', args=[app, new_user_list])
+ operation_logger.related_to.append(('app', permission.split(".")[0]))
+ operation_logger.start()
- return user_permission_list(app, permission)
+ new_permission = _update_ldap_group_permission(permission=permission, allowed="all_users", sync_perm=sync_perm)
+
+ logger.debug(m18n.n('permission_updated', permission=permission))
+
+ return new_permission
-@is_unit_operation(['permission', 'app'])
-def permission_add(operation_logger, app, permission, urls=None, default_allow=True, sync_perm=True):
+def user_permission_info(permission):
+ """
+ Return informations about a specific permission
+
+ Keyword argument:
+ permission -- Name of the permission (e.g. mail or nextcloud or wordpress.editors)
+ """
+
+ # By default, manipulate main permission
+ if "." not in permission:
+ permission = permission + ".main"
+
+ # Fetch existing permission
+
+ existing_permission = user_permission_list(full=True)["permissions"].get(permission, None)
+ if existing_permission is None:
+ raise YunohostError('permission_not_found', permission=permission)
+
+ return existing_permission
+
+
+#
+#
+# The followings methods are *not* directly exposed.
+# They are used to create/delete the permissions (e.g. during app install/remove)
+# and by some app helpers to possibly add additional permissions
+#
+#
+
+
+@is_unit_operation()
+def permission_create(operation_logger, permission, url=None, allowed=None, sync_perm=True):
"""
Create a new permission for a specific application
Keyword argument:
- app -- an application OR sftp, xmpp (metronome), mail
- permission -- name of the permission ("main" by default)
- urls -- list of urls to specify for the permission
+ permission -- Name of the permission (e.g. mail or nextcloud or wordpress.editors)
+ url -- (optional) URL for which access will be allowed/forbidden
+ allowed -- (optional) A list of group/user to allow for the permission
+ If provided, 'url' is assumed to be relative to the app domain/path if they
+ start with '/'. For example:
+ / -> domain.tld/app
+ /admin -> domain.tld/app/admin
+ domain.tld/app/api -> domain.tld/app/api
+
+ 'url' can be later treated as a regex if it starts with "re:".
+ For example:
+ re:/api/[A-Z]*$ -> domain.tld/app/api/[A-Z]*$
+ re:domain.tld/app/api/[A-Z]*$ -> domain.tld/app/api/[A-Z]*$
"""
- from yunohost.domain import _normalize_domain_path
+
from yunohost.utils.ldap import _get_ldap_interface
+ from yunohost.user import user_group_list
ldap = _get_ldap_interface()
+ # By default, manipulate main permission
+ if "." not in permission:
+ permission = permission + ".main"
+
# Validate uniqueness of permission in LDAP
- permission_name = str(permission + '.' + app) # str(...) Fix encoding issue
- conflict = ldap.get_conflict({
- 'cn': permission_name
- }, base_dn='ou=permission,dc=yunohost,dc=org')
- if conflict:
- raise YunohostError('permission_already_exist', permission=permission, app=app)
+ if ldap.get_conflict({'cn': permission},
+ base_dn='ou=permission,dc=yunohost,dc=org'):
+ raise YunohostError('permission_already_exist', permission=permission)
# Get random GID
all_gid = {x.gr_gid for x in grp.getgrall()}
@@ -349,180 +272,233 @@ def permission_add(operation_logger, app, permission, urls=None, default_allow=T
attr_dict = {
'objectClass': ['top', 'permissionYnh', 'posixGroup'],
- 'cn': permission_name,
+ 'cn': str(permission),
'gidNumber': gid,
}
- if default_allow:
- attr_dict['groupPermission'] = 'cn=all_users,ou=groups,dc=yunohost,dc=org'
- if urls:
- attr_dict['URL'] = []
- for url in urls:
- domain = url[:url.index('/')]
- path = url[url.index('/'):]
- domain, path = _normalize_domain_path(domain, path)
- attr_dict['URL'].append(domain + path)
+ if url:
+ attr_dict['URL'] = url
+ if allowed is not None:
+ if not isinstance(allowed, list):
+ allowed = [allowed]
+
+ # Validate that the groups to add actually exist
+ all_existing_groups = user_group_list()['groups'].keys()
+ for group in allowed or []:
+ if group not in all_existing_groups:
+ raise YunohostError('group_unknown', group=group)
+
+ operation_logger.related_to.append(('app', permission.split(".")[0]))
operation_logger.start()
- if ldap.add('cn=%s,ou=permission' % permission_name, attr_dict):
- if sync_perm:
- permission_sync_to_user()
- logger.debug(m18n.n('permission_created', permission=permission, app=app))
- return user_permission_list(app, permission)
- raise YunohostError('permission_creation_failed')
+ try:
+ ldap.add('cn=%s,ou=permission' % permission, attr_dict)
+ except Exception as e:
+ raise YunohostError('permission_creation_failed', permission=permission, error=e)
+
+ new_permission = _update_ldap_group_permission(permission=permission, allowed=allowed, sync_perm=sync_perm)
+
+ logger.debug(m18n.n('permission_created', permission=permission))
+ return new_permission
-@is_unit_operation(['permission', 'app'])
-def permission_update(operation_logger, app, permission, add_url=None, remove_url=None, sync_perm=True):
+@is_unit_operation()
+def permission_url(operation_logger, permission, url=None, sync_perm=True):
"""
- Update a permission for a specific application
+ Update urls related to a permission for a specific application
Keyword argument:
- app -- an application OR sftp, xmpp (metronome), mail
- permission -- name of the permission ("main" by default)
- add_url -- Add a new url for a permission
- remove_url -- Remove a url for a permission
-
+ permission -- Name of the permission (e.g. mail or nextcloud or wordpress.editors)
+ url -- (optional) URL for which access will be allowed/forbidden
"""
- from yunohost.domain import _normalize_domain_path
from yunohost.utils.ldap import _get_ldap_interface
ldap = _get_ldap_interface()
- permission_name = str(permission + '.' + app) # str(...) Fix encoding issue
+ # By default, manipulate main permission
+ if "." not in permission:
+ permission = permission + ".main"
- # Populate permission informations
- result = ldap.search(base='ou=permission,dc=yunohost,dc=org',
- filter='cn=' + permission_name, attrs=['URL'])
- if not result:
- raise YunohostError('permission_not_found', permission=permission, app=app)
- permission_obj = result[0]
+ # Fetch existing permission
- if 'URL' not in permission_obj:
- permission_obj['URL'] = []
+ existing_permission = user_permission_list(full=True)["permissions"].get(permission, None)
+ if not existing_permission:
+ raise YunohostError('permission_not_found', permission=permission)
- url = set(permission_obj['URL'])
+ # Compute new url list
+ old_url = existing_permission["url"]
- if add_url:
- for u in add_url:
- domain = u[:u.index('/')]
- path = u[u.index('/'):]
- domain, path = _normalize_domain_path(domain, path)
- url.add(domain + path)
- if remove_url:
- for u in remove_url:
- domain = u[:u.index('/')]
- path = u[u.index('/'):]
- domain, path = _normalize_domain_path(domain, path)
- url.discard(domain + path)
-
- if url == set(permission_obj['URL']):
+ if old_url == url:
logger.warning(m18n.n('permission_update_nothing_to_do'))
- return user_permission_list(app, permission)
+ return existing_permission
+ # Actually commit the change
+
+ operation_logger.related_to.append(('app', permission.split(".")[0]))
operation_logger.start()
- if ldap.update('cn=%s,ou=permission' % permission_name, {'cn': permission_name, 'URL': url}):
- if sync_perm:
- permission_sync_to_user()
- logger.debug(m18n.n('permission_updated', permission=permission, app=app))
- return user_permission_list(app, permission)
- raise YunohostError('premission_update_failed')
+ try:
+ ldap.update('cn=%s,ou=permission' % permission, {'URL': [url]})
+ except Exception as e:
+ raise YunohostError('permission_update_failed', permission=permission, error=e)
-
-@is_unit_operation(['permission', 'app'])
-def permission_remove(operation_logger, app, permission, force=False, sync_perm=True):
- """
- Remove a permission for a specific application
-
- Keyword argument:
- app -- an application OR sftp, xmpp (metronome), mail
- permission -- name of the permission ("main" by default)
-
- """
-
- if permission == "main" and not force:
- raise YunohostError('remove_main_permission_not_allowed')
-
- from yunohost.utils.ldap import _get_ldap_interface
- ldap = _get_ldap_interface()
-
- operation_logger.start()
- if not ldap.remove('cn=%s,ou=permission' % str(permission + '.' + app)):
- raise YunohostError('permission_deletion_failed', permission=permission, app=app)
if sync_perm:
permission_sync_to_user()
- logger.debug(m18n.n('permission_deleted', permission=permission, app=app))
+
+ logger.debug(m18n.n('permission_updated', permission=permission))
+ return user_permission_list(full=True)["permissions"][permission]
-def permission_sync_to_user(force=False):
+@is_unit_operation()
+def permission_delete(operation_logger, permission, force=False, sync_perm=True):
+ """
+ Delete a permission
+
+ Keyword argument:
+ permission -- Name of the permission (e.g. mail or nextcloud or wordpress.editors)
+ """
+
+ # By default, manipulate main permission
+ if "." not in permission:
+ permission = permission + ".main"
+
+ if permission.endswith(".main") and not force:
+ raise YunohostError('permission_cannot_remove_main')
+
+ from yunohost.utils.ldap import _get_ldap_interface
+ ldap = _get_ldap_interface()
+
+ # Make sure this permission exists
+
+ existing_permission = user_permission_list(full=True)["permissions"].get(permission, None)
+ if not existing_permission:
+ raise YunohostError('permission_not_found', permission=permission)
+
+ # Actually delete the permission
+
+ operation_logger.related_to.append(('app', permission.split(".")[0]))
+ operation_logger.start()
+
+ try:
+ ldap.remove('cn=%s,ou=permission' % permission)
+ except Exception as e:
+ raise YunohostError('permission_deletion_failed', permission=permission, error=e)
+
+ if sync_perm:
+ permission_sync_to_user()
+ logger.debug(m18n.n('permission_deleted', permission=permission))
+
+
+def permission_sync_to_user():
"""
Sychronise the inheritPermission attribut in the permission object from the
user<->group link and the group<->permission link
-
- Keyword argument:
- force -- Force to recreate all attributes. Used generally with the
- backup which uses "slapadd" which doesnt' use the memberOf overlay.
- Note that by removing all value and adding a new time, we force the
- overlay to update all attributes
"""
- # Note that a LDAP operation with the same value that is in LDAP crash SLAP.
- # So we need to check before each ldap operation that we really change something in LDAP
import os
from yunohost.app import app_ssowatconf
+ from yunohost.user import user_group_list
from yunohost.utils.ldap import _get_ldap_interface
ldap = _get_ldap_interface()
- permission_attrs = [
- 'cn',
- 'member',
- ]
- group_info = ldap.search('ou=groups,dc=yunohost,dc=org',
- '(objectclass=groupOfNamesYnh)', permission_attrs)
- group_info = {g['cn'][0]: g for g in group_info}
+ groups = user_group_list(full=True)["groups"]
+ permissions = user_permission_list(full=True)["permissions"]
- for per in ldap.search('ou=permission,dc=yunohost,dc=org',
- '(objectclass=permissionYnh)',
- ['cn', 'inheritPermission', 'groupPermission', 'memberUid']):
+ for permission_name, permission_infos in permissions.items():
- if 'groupPermission' not in per:
- per['groupPermission'] = []
- user_permission = set()
- for group in per['groupPermission']:
- group = group.split("=")[1].split(",")[0]
- if 'member' not in group_info[group]:
- continue
- for user in group_info[group]['member']:
- user_permission.add(user)
+ # These are the users currently allowed because there's an 'inheritPermission' object corresponding to it
+ currently_allowed_users = set(permission_infos["corresponding_users"])
- if 'inheritPermission' not in per:
- per['inheritPermission'] = []
- if 'memberUid' not in per:
- per['memberUid'] = []
+ # These are the users that should be allowed because they are member of a group that is allowed for this permission ...
+ should_be_allowed_users = set([user for group in permission_infos["allowed"] for user in groups[group]["members"]])
- uid_val = [v.split("=")[1].split(",")[0] for v in user_permission]
- if user_permission == set(per['inheritPermission']) and set(uid_val) == set(per['memberUid']) and not force:
+ # Note that a LDAP operation with the same value that is in LDAP crash SLAP.
+ # So we need to check before each ldap operation that we really change something in LDAP
+ if currently_allowed_users == should_be_allowed_users:
+ # We're all good, this permission is already correctly synchronized !
continue
- inheritPermission = {'inheritPermission': user_permission, 'memberUid': uid_val}
- if force:
- if per['groupPermission']:
- if not ldap.update('cn=%s,ou=permission' % per['cn'][0], {'groupPermission': []}):
- raise YunohostError('permission_update_failed_clear')
- if not ldap.update('cn=%s,ou=permission' % per['cn'][0], {'groupPermission': per['groupPermission']}):
- raise YunohostError('permission_update_failed_populate')
- if per['inheritPermission']:
- if not ldap.update('cn=%s,ou=permission' % per['cn'][0], {'inheritPermission': []}):
- raise YunohostError('permission_update_failed_clear')
- if user_permission:
- if not ldap.update('cn=%s,ou=permission' % per['cn'][0], inheritPermission):
- raise YunohostError('permission_update_failed')
- else:
- if not ldap.update('cn=%s,ou=permission' % per['cn'][0], inheritPermission):
- raise YunohostError('permission_update_failed')
- logger.debug(m18n.n('permission_generated'))
+
+ new_inherited_perms = {'inheritPermission': ["uid=%s,ou=users,dc=yunohost,dc=org" % u for u in should_be_allowed_users],
+ 'memberUid': should_be_allowed_users}
+
+ # Commit the change with the new inherited stuff
+ try:
+ ldap.update('cn=%s,ou=permission' % permission_name, new_inherited_perms)
+ except Exception as e:
+ raise YunohostError('permission_update_failed', permission=permission_name, error=e)
+
+ logger.debug("The permission database has been resynchronized")
app_ssowatconf()
# Reload unscd, otherwise the group ain't propagated to the LDAP database
os.system('nscd --invalidate=passwd')
os.system('nscd --invalidate=group')
+
+
+def _update_ldap_group_permission(permission, allowed, sync_perm=True):
+ """
+ Internal function that will rewrite user permission
+
+ permission -- Name of the permission (e.g. mail or nextcloud or wordpress.editors)
+ allowed -- A list of group/user to allow for the permission
+
+
+ Assumptions made, that should be checked before calling this function:
+ - the permission does currently exists ...
+ - the 'allowed' list argument is *different* from the current
+ permission state ... otherwise ldap will miserably fail in such
+ case...
+ - the 'allowed' list contains *existing* groups.
+ """
+
+ from yunohost.hook import hook_callback
+ from yunohost.utils.ldap import _get_ldap_interface
+ ldap = _get_ldap_interface()
+
+ # Fetch currently allowed groups for this permission
+ existing_permission = user_permission_list(full=True)["permissions"][permission]
+
+ if allowed is None:
+ return existing_permission
+
+ allowed = [allowed] if not isinstance(allowed, list) else allowed
+
+ # Guarantee uniqueness of values in allowed, which would otherwise make ldap.update angry.
+ allowed = set(allowed)
+
+ try:
+ ldap.update('cn=%s,ou=permission' % permission,
+ {'groupPermission': ['cn=' + g + ',ou=groups,dc=yunohost,dc=org' for g in allowed]})
+ except Exception as e:
+ raise YunohostError('permission_update_failed', permission=permission, error=e)
+
+ # Trigger permission sync if asked
+
+ if sync_perm:
+ permission_sync_to_user()
+
+ new_permission = user_permission_list(full=True)["permissions"][permission]
+
+ # Trigger app callbacks
+
+ app = permission.split(".")[0]
+ sub_permission = permission.split(".")[1]
+
+ old_corresponding_users = set(existing_permission["corresponding_users"])
+ new_corresponding_users = set(new_permission["corresponding_users"])
+
+ old_allowed_users = set(existing_permission["allowed"])
+ new_allowed_users = set(new_permission["allowed"])
+
+ effectively_added_users = new_corresponding_users - old_corresponding_users
+ effectively_removed_users = old_corresponding_users - new_corresponding_users
+
+ effectively_added_group = new_allowed_users - old_allowed_users - effectively_added_users
+ effectively_removed_group = old_allowed_users - new_allowed_users - effectively_removed_users
+
+ if effectively_added_users or effectively_added_group:
+ hook_callback('post_app_addaccess', args=[app, ','.join(effectively_added_users), sub_permission, ','.join(effectively_added_group)])
+ if effectively_removed_users or effectively_removed_group:
+ hook_callback('post_app_removeaccess', args=[app, ','.join(effectively_removed_users), sub_permission, ','.join(effectively_removed_group)])
+
+ return new_permission
diff --git a/src/yunohost/regenconf.py b/src/yunohost/regenconf.py
index 48129634a..4062628aa 100644
--- a/src/yunohost/regenconf.py
+++ b/src/yunohost/regenconf.py
@@ -70,7 +70,7 @@ def regen_conf(operation_logger, names=[], with_diff=False, force=False, dry_run
or not os.path.exists(REGEN_CONF_FILE)):
from yunohost.tools import _get_migration_by_name
migration = _get_migration_by_name("decouple_regenconf_from_services")
- migration.migrate()
+ migration.run()
result = {}
@@ -131,6 +131,16 @@ def regen_conf(operation_logger, names=[], with_diff=False, force=False, dry_run
show_info=False)['hooks']
names.remove('ssh')
+ # Dirty hack for legacy code : avoid attempting to regen the conf for
+ # glances because it got removed ... This is only needed *once*
+ # during the upgrade from 3.7 to 3.8 because Yunohost will attempt to
+ # regen glance's conf *before* it gets automatically removed from
+ # services.yml (which will happens only during the regen-conf of
+ # 'yunohost', so at the very end of the regen-conf cycle) Anyway,
+ # this can be safely removed once we're in >= 4.0
+ if "glances" in names:
+ names.remove("glances")
+
pre_result = hook_callback('conf_regen', names, pre_callback=_pre_call)
# Keep only the hook names with at least one success
@@ -154,10 +164,10 @@ def regen_conf(operation_logger, names=[], with_diff=False, force=False, dry_run
if not dry_run:
operation_logger.related_to.append(('configuration', category))
- logger.debug(m18n.n(
- 'regenconf_pending_applying' if not dry_run else
- 'regenconf_dry_pending_applying',
- category=category))
+ if dry_run:
+ logger.debug(m18n.n('regenconf_pending_applying', category=category))
+ else:
+ logger.debug(m18n.n('regenconf_dry_pending_applying', category=category))
conf_hashes = _get_conf_hashes(category)
succeed_regen = {}
@@ -271,10 +281,10 @@ def regen_conf(operation_logger, names=[], with_diff=False, force=False, dry_run
logger.debug(m18n.n('regenconf_up_to_date', category=category))
continue
elif not failed_regen:
- logger.success(m18n.n(
- 'regenconf_updated' if not dry_run else
- 'regenconf_would_be_updated',
- category=category))
+ if not dry_run:
+ logger.success(m18n.n('regenconf_updated', category=category))
+ else:
+ logger.success(m18n.n('regenconf_would_be_updated', category=category))
if succeed_regen and not dry_run:
_update_conf_hashes(category, conf_hashes)
@@ -463,6 +473,18 @@ def _update_conf_hashes(category, hashes):
_save_regenconf_infos(categories)
+def _force_clear_hashes(paths):
+
+ categories = _get_regenconf_infos()
+ for path in paths:
+ for category in categories.keys():
+ if path in categories[category]['conffiles']:
+ logger.debug("force-clearing old conf hash for %s in category %s" % (path, category))
+ del categories[category]['conffiles'][path]
+
+ _save_regenconf_infos(categories)
+
+
def _process_regen_conf(system_conf, new_conf=None, save=True):
"""Regenerate a given system configuration file
@@ -525,31 +547,32 @@ def _process_regen_conf(system_conf, new_conf=None, save=True):
def manually_modified_files():
- # We do this to have --quiet, i.e. don't throw a whole bunch of logs
- # just to fetch this...
- # Might be able to optimize this by looking at what the regen conf does
- # and only do the part that checks file hashes...
- cmd = "yunohost tools regen-conf --dry-run --output-as json --quiet"
- j = json.loads(subprocess.check_output(cmd.split()))
-
- # j is something like :
- # {"postfix": {"applied": {}, "pending": {"/etc/postfix/main.cf": {"status": "modified"}}}
-
output = []
- for app, actions in j.items():
- for action, files in actions.items():
- for filename, infos in files.items():
- if infos["status"] == "modified":
- output.append(filename)
+ regenconf_categories = _get_regenconf_infos()
+ for category, infos in regenconf_categories.items():
+ conffiles = infos["conffiles"]
+ for path, hash_ in conffiles.items():
+ if hash_ != _calculate_hash(path):
+ output.append(path)
return output
-def manually_modified_files_compared_to_debian_default():
+def manually_modified_files_compared_to_debian_default(ignore_handled_by_regenconf=False):
# from https://serverfault.com/a/90401
- r = subprocess.check_output("dpkg-query -W -f='${Conffiles}\n' '*' \
- | awk 'OFS=\" \"{print $2,$1}' \
- | md5sum -c 2>/dev/null \
- | awk -F': ' '$2 !~ /OK/{print $1}'", shell=True)
- return r.strip().split("\n")
+ files = subprocess.check_output("dpkg-query -W -f='${Conffiles}\n' '*' \
+ | awk 'OFS=\" \"{print $2,$1}' \
+ | md5sum -c 2>/dev/null \
+ | awk -F': ' '$2 !~ /OK/{print $1}'", shell=True)
+ files = files.strip().split("\n")
+
+ if ignore_handled_by_regenconf:
+ regenconf_categories = _get_regenconf_infos()
+ regenconf_files = []
+ for infos in regenconf_categories.values():
+ regenconf_files.extend(infos["conffiles"].keys())
+
+ files = [f for f in files if f not in regenconf_files]
+
+ return files
diff --git a/src/yunohost/service.py b/src/yunohost/service.py
index 17a3cc83e..b6c93b5ae 100644
--- a/src/yunohost/service.py
+++ b/src/yunohost/service.py
@@ -40,25 +40,24 @@ MOULINETTE_LOCK = "/var/run/moulinette_yunohost.lock"
logger = log.getActionLogger('yunohost.service')
-def service_add(name, status=None, log=None, runlevel=None, need_lock=False, description=None, log_type="file"):
+def service_add(name, description=None, log=None, log_type="file", test_status=None, test_conf=None, needs_exposed_ports=None, need_lock=False, status=None):
"""
Add a custom service
Keyword argument:
name -- Service name to add
- status -- Custom status command
- log -- Absolute path to log file to display
- runlevel -- Runlevel priority of the service
- need_lock -- Use this option to prevent deadlocks if the service does invoke yunohost commands.
description -- description of the service
- log_type -- Precise if the corresponding log is a file or a systemd log
+ log -- Absolute path to log file to display
+ log_type -- Specify if the corresponding log is a file or a systemd log
+ test_status -- Specify a custom bash command to check the status of the service. N.B. : it only makes sense to specify this if the corresponding systemd service does not return the proper information.
+ test_conf -- Specify a custom bash command to check if the configuration of the service is valid or broken, similar to nginx -t.
+ needs_exposed_ports -- A list of ports that needs to be publicly exposed for the service to work as intended.
+ need_lock -- Use this option to prevent deadlocks if the service does invoke yunohost commands.
+ status -- Deprecated, doesn't do anything anymore. Use test_status instead.
"""
services = _get_services()
- if not status:
- services[name] = {'status': 'service'}
- else:
- services[name] = {'status': status}
+ services[name] = {}
if log is not None:
if not isinstance(log, list):
@@ -77,15 +76,31 @@ def service_add(name, status=None, log=None, runlevel=None, need_lock=False, des
else:
raise YunohostError('service_add_failed', service=name)
-
- if runlevel is not None:
- services[name]['runlevel'] = runlevel
+ if description:
+ services[name]['description'] = description
+ else:
+ # Try to get the description from systemd service
+ out = subprocess.check_output("systemctl show %s | grep '^Description='" % name, shell=True).strip()
+ out = out.replace("Description=", "")
+ # If the service does not yet exists or if the description is empty,
+ # systemd will anyway return foo.service as default value, so we wanna
+ # make sure there's actually something here.
+ if out == name + ".service":
+ logger.warning("/!\\ Packager ! You added a custom service without specifying a description. Please add a proper Description in the systemd configuration, or use --description to explain what the service does in a similar fashion to existing services.")
+ else:
+ services[name]['description'] = out
if need_lock:
services[name]['need_lock'] = True
- if description is not None:
- services[name]['description'] = description
+ if test_status:
+ services[name]["test_status"] = test_status
+
+ if test_conf:
+ services[name]["test_conf"] = test_conf
+
+ if needs_exposed_ports:
+ services[name]["needs_exposed_ports"] = needs_exposed_ports
try:
_save_services(services)
@@ -277,26 +292,20 @@ def service_status(names=[]):
# the hack was to add fake services...
# we need to extract regenconf from service at some point, also because
# some app would really like to use it
- if "status" in services[name] and services[name]["status"] is None:
+ if services[name].get("status", "") is None:
continue
- status = _get_service_information_from_systemd(name)
-
- # try to get status using alternative version if they exists
- # this is for mariadb/mysql but is generic in case of
- alternates = services[name].get("alternates", [])
- while status is None and alternates:
- status = _get_service_information_from_systemd(alternates.pop())
+ systemd_service = services[name].get("actual_systemd_service", name)
+ status = _get_service_information_from_systemd(systemd_service)
if status is None:
- logger.error("Failed to get status information via dbus for service %s, systemctl didn't recognize this service ('NoSuchUnit')." % name)
+ logger.error("Failed to get status information via dbus for service %s, systemctl didn't recognize this service ('NoSuchUnit')." % systemd_service)
result[name] = {
'status': "unknown",
- 'loaded': "unknown",
- 'active': "unknown",
- 'active_at': "unknown",
+ 'start_on_boot': "unknown",
+ 'last_state_change': "unknown",
'description': "Error: failed to get information for this service, it doesn't exists for systemd",
- 'service_file_path': "unknown",
+ 'configuration': "unknown",
}
else:
@@ -314,21 +323,48 @@ def service_status(names=[]):
result[name] = {
'status': str(status.get("SubState", "unknown")),
- 'loaded': str(status.get("UnitFileState", "unknown")),
- 'active': str(status.get("ActiveState", "unknown")),
+ 'start_on_boot': str(status.get("UnitFileState", "unknown")),
+ 'last_state_change': "unknown",
'description': description,
- 'service_file_path': str(status.get("FragmentPath", "unknown")),
+ 'configuration': "unknown",
}
# Fun stuff™ : to obtain the enabled/disabled status for sysv services,
# gotta do this ... cf code of /lib/systemd/systemd-sysv-install
- if result[name]["loaded"] == "generated":
- result[name]["loaded"] = "enabled" if glob("/etc/rc[S5].d/S??"+name) else "disabled"
+ if result[name]["start_on_boot"] == "generated":
+ result[name]["start_on_boot"] = "enabled" if glob("/etc/rc[S5].d/S??"+name) else "disabled"
+ elif os.path.exists("/etc/systemd/system/multi-user.target.wants/%s.service" % name):
+ result[name]["start_on_boot"] = "enabled"
- if "ActiveEnterTimestamp" in status:
- result[name]['active_at'] = datetime.utcfromtimestamp(status["ActiveEnterTimestamp"] / 1000000)
- else:
- result[name]['active_at'] = "unknown"
+ if "StateChangeTimestamp" in status:
+ result[name]['last_state_change'] = datetime.utcfromtimestamp(status["StateChangeTimestamp"] / 1000000)
+
+ # 'test_status' is an optional field to test the status of the service using a custom command
+ if "test_status" in services[name]:
+ p = subprocess.Popen(services[name]["test_status"],
+ shell=True,
+ executable='/bin/bash',
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT)
+
+ p.communicate()
+
+ result[name]["status"] = "running" if p.returncode == 0 else "failed"
+
+ # 'test_status' is an optional field to test the status of the service using a custom command
+ if "test_conf" in services[name]:
+ p = subprocess.Popen(services[name]["test_conf"],
+ shell=True,
+ executable='/bin/bash',
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT)
+
+ out, _ = p.communicate()
+ if p.returncode == 0:
+ result[name]["configuration"] = "valid"
+ else:
+ result[name]["configuration"] = "broken"
+ result[name]["configuration-details"] = out.strip().split("\n")
if len(names) == 1:
return result[names[0]]
@@ -369,14 +405,12 @@ def service_log(name, number=50):
"""
services = _get_services()
+ number = int(number)
if name not in services.keys():
raise YunohostError('service_unknown', service=name)
- if 'log' not in services[name]:
- raise YunohostError('service_no_log', service=name)
-
- log_list = services[name]['log']
+ log_list = services[name].get('log', [])
log_type_list = services[name].get('log_type', [])
if not isinstance(log_list, list):
@@ -386,13 +420,16 @@ def service_log(name, number=50):
result = {}
+ # First we always add the logs from journalctl / systemd
+ result["journalctl"] = _get_journalctl_logs(name, number).splitlines()
+
for index, log_path in enumerate(log_list):
log_type = log_type_list[index]
if log_type == "file":
# log is a file, read it
if not os.path.isdir(log_path):
- result[log_path] = _tail(log_path, int(number)) if os.path.exists(log_path) else []
+ result[log_path] = _tail(log_path, number) if os.path.exists(log_path) else []
continue
for log_file in os.listdir(log_path):
@@ -404,10 +441,11 @@ def service_log(name, number=50):
if not log_file.endswith(".log"):
continue
- result[log_file_path] = _tail(log_file_path, int(number)) if os.path.exists(log_file_path) else []
+ result[log_file_path] = _tail(log_file_path, number) if os.path.exists(log_file_path) else []
else:
+ # N.B. : this is legacy code that can probably be removed ... to be confirmed
# get log with journalctl
- result[log_path] = _get_journalctl_logs(log_path, int(number)).splitlines()
+ result[log_path] = _get_journalctl_logs(log_path, number).splitlines()
return result
@@ -529,14 +567,22 @@ def _get_services():
services = yaml.load(f)
except:
return {}
- else:
- # some services are marked as None to remove them from YunoHost
- # filter this
- for key, value in services.items():
- if value is None:
- del services[key]
- return services
+ # some services are marked as None to remove them from YunoHost
+ # filter this
+ for key, value in services.items():
+ if value is None:
+ del services[key]
+
+ # Stupid hack for postgresql which ain't an official service ... Can't
+ # really inject that info otherwise. Real service we want to check for
+ # status and log is in fact postgresql@x.y-main (x.y being the version)
+ if "postgresql" in services:
+ if "description" in services["postgresql"]:
+ del services["postgresql"]["description"]
+ services["postgresql"]["actual_systemd_service"] = "postgresql@9.6-main"
+
+ return services
def _save_services(services):
@@ -631,8 +677,10 @@ def _find_previous_log_file(file):
def _get_journalctl_logs(service, number="all"):
+ services = _get_services()
+ systemd_service = services.get(service, {}).get("actual_systemd_service", service)
try:
- return subprocess.check_output("journalctl -xn -u {0} -n{1}".format(service, number), shell=True)
+ return subprocess.check_output("journalctl -xn -u {0} -n{1}".format(systemd_service, number), shell=True)
except:
import traceback
return "error while get services logs from journalctl:\n%s" % traceback.format_exc()
diff --git a/src/yunohost/settings.py b/src/yunohost/settings.py
index a24ecb637..c1edadb93 100644
--- a/src/yunohost/settings.py
+++ b/src/yunohost/settings.py
@@ -1,5 +1,6 @@
import os
import json
+import subprocess
from datetime import datetime
from collections import OrderedDict
@@ -14,6 +15,28 @@ logger = getActionLogger('yunohost.settings')
SETTINGS_PATH = "/etc/yunohost/settings.json"
SETTINGS_PATH_OTHER_LOCATION = "/etc/yunohost/settings-%s.json"
+def is_boolean(value):
+ """
+ Ensure a string value is intended as a boolean
+
+ Keyword arguments:
+ arg -- The string to check
+
+ Returns:
+ (is_boolean, boolean_value)
+
+ """
+ if isinstance(value, bool):
+ return True, value
+ elif isinstance(value, basestring):
+ if str(value).lower() in ['true', 'on', 'yes', 'false', 'off', 'no']:
+ return True, str(value).lower() in ['true', 'on', 'yes']
+ else:
+ return False, None
+ else:
+ return False, None
+
+
# a settings entry is in the form of:
# namespace.subnamespace.name: {type, value, default, description, [choices]}
# choices is only for enum
@@ -27,7 +50,7 @@ SETTINGS_PATH_OTHER_LOCATION = "/etc/yunohost/settings-%s.json"
# * bool
# * int
# * string
-# * enum (in form a python list)
+# * enum (in the form of a python list)
DEFAULTS = OrderedDict([
("example.bool", {"type": "bool", "default": True}),
@@ -46,6 +69,8 @@ DEFAULTS = OrderedDict([
"choices": ["intermediate", "modern"]}),
("security.postfix.compatibility", {"type": "enum", "default": "intermediate",
"choices": ["intermediate", "modern"]}),
+ ("pop3.enabled", {"type": "bool", "default": False}),
+ ("smtp.allow_ipv6", {"type": "bool", "default": True}),
])
@@ -93,7 +118,10 @@ def settings_set(key, value):
key_type = settings[key]["type"]
if key_type == "bool":
- if not isinstance(value, bool):
+ boolean_value = is_boolean(value)
+ if boolean_value[0]:
+ value = boolean_value[1]
+ else:
raise YunohostError('global_settings_bad_type_for_setting', setting=key,
received_type=type(value).__name__, expected_type=key_type)
elif key_type == "int":
@@ -126,8 +154,6 @@ def settings_set(key, value):
settings[key]["value"] = value
_save_settings(settings)
- # TODO : whatdo if the old value is the same as
- # the new value...
try:
trigger_post_change_hook(key, old_value, value)
except Exception as e:
@@ -295,7 +321,34 @@ def reconfigure_ssh(setting_name, old_value, new_value):
if old_value != new_value:
service_regen_conf(names=['ssh'])
+@post_change_hook("smtp.allow_ipv6")
@post_change_hook("security.postfix.compatibility")
-def reconfigure_ssh(setting_name, old_value, new_value):
+def reconfigure_postfix(setting_name, old_value, new_value):
if old_value != new_value:
service_regen_conf(names=['postfix'])
+
+@post_change_hook("pop3.enabled")
+def reconfigure_dovecot(setting_name, old_value, new_value):
+ dovecot_package = 'dovecot-pop3d'
+
+ environment = os.environ.copy()
+ environment.update({'DEBIAN_FRONTEND': 'noninteractive'})
+
+ if new_value == "True":
+ command = [
+ 'apt-get',
+ '-y',
+ '--no-remove',
+ '-o Dpkg::Options::=--force-confdef',
+ '-o Dpkg::Options::=--force-confold',
+ 'install',
+ dovecot_package,
+ ]
+ subprocess.call(command, env=environment)
+ if old_value != new_value:
+ service_regen_conf(names=['dovecot'])
+ else:
+ if old_value != new_value:
+ service_regen_conf(names=['dovecot'])
+ command = ['apt-get', '-y', 'remove', dovecot_package]
+ subprocess.call(command, env=environment)
diff --git a/src/yunohost/tests/conftest.py b/src/yunohost/tests/conftest.py
index ce321933c..bd1702571 100644
--- a/src/yunohost/tests/conftest.py
+++ b/src/yunohost/tests/conftest.py
@@ -1,9 +1,32 @@
+import pytest
import sys
import moulinette
+from moulinette import m18n
+from yunohost.utils.error import YunohostError
+from contextlib import contextmanager
+
sys.path.append("..")
+
+@contextmanager
+def message(mocker, key, **kwargs):
+ mocker.spy(m18n, "n")
+ yield
+ m18n.n.assert_any_call(key, **kwargs)
+
+
+@contextmanager
+def raiseYunohostError(mocker, key, **kwargs):
+ with pytest.raises(YunohostError) as e_info:
+ yield
+ assert e_info._excinfo[1].key == key
+ if kwargs:
+ assert e_info._excinfo[1].kwargs == kwargs
+
+
+
def pytest_addoption(parser):
parser.addoption("--yunodebug", action="store_true", default=False)
diff --git a/src/yunohost/tests/test_apps.py b/src/yunohost/tests/test_apps.py
index 9c85df1e9..7c0861aa1 100644
--- a/src/yunohost/tests/test_apps.py
+++ b/src/yunohost/tests/test_apps.py
@@ -4,13 +4,16 @@ import pytest
import shutil
import requests
+from conftest import message, raiseYunohostError
+
from moulinette import m18n
from moulinette.utils.filesystem import mkdir
-from yunohost.app import app_install, app_remove, app_ssowatconf, _is_installed, app_upgrade
+from yunohost.app import app_install, app_remove, app_ssowatconf, _is_installed, app_upgrade, app_map
from yunohost.domain import _get_maindomain, domain_add, domain_remove, domain_list
from yunohost.utils.error import YunohostError
from yunohost.tests.test_permission import check_LDAP_db_integrity, check_permission_for_apps
+from yunohost.permission import user_permission_list, permission_delete
def setup_function(function):
@@ -27,31 +30,30 @@ def clean():
os.system("mkdir -p /etc/ssowat/")
app_ssowatconf()
- # Gotta first remove break yo system
- # because some remaining stuff might
- # make the other app_remove crashs ;P
- if _is_installed("break_yo_system"):
- app_remove("break_yo_system")
+ test_apps = ["break_yo_system", "legacy_app", "legacy_app__2", "full_domain_app"]
- if _is_installed("legacy_app"):
- app_remove("legacy_app")
+ for test_app in test_apps:
- to_remove = []
- to_remove += glob.glob("/etc/nginx/conf.d/*.d/*legacy*")
- to_remove += glob.glob("/etc/nginx/conf.d/*.d/*break_yo_system*")
- for filepath in to_remove:
- os.remove(filepath)
+ if _is_installed(test_app):
+ app_remove(test_app)
- to_remove = []
- to_remove += glob.glob("/etc/yunohost/apps/*legacy_app*")
- to_remove += glob.glob("/etc/yunohost/apps/*break_yo_system*")
- to_remove += glob.glob("/var/www/*legacy*")
- for folderpath in to_remove:
- shutil.rmtree(folderpath, ignore_errors=True)
+ for filepath in glob.glob("/etc/nginx/conf.d/*.d/*%s*" % test_app):
+ os.remove(filepath)
+ for folderpath in glob.glob("/etc/yunohost/apps/*%s*" % test_app):
+ shutil.rmtree(folderpath, ignore_errors=True)
+ for folderpath in glob.glob("/var/www/*%s*" % test_app):
+ shutil.rmtree(folderpath, ignore_errors=True)
+
+ os.system("bash -c \"mysql -u root --password=$(cat /etc/yunohost/mysql) 2>/dev/null <<< 'DROP DATABASE %s' \"" % test_app)
+ os.system("bash -c \"mysql -u root --password=$(cat /etc/yunohost/mysql) 2>/dev/null <<< 'DROP USER %s@localhost'\"" % test_app)
os.system("systemctl reset-failed nginx") # Reset failed quota for service to avoid running into start-limit rate ?
os.system("systemctl start nginx")
+ # Clean permissions
+ for permission_name in user_permission_list(short=True)["permissions"]:
+ if any(test_app in permission_name for test_app in test_apps):
+ permission_delete(permission_name, force=True)
@pytest.fixture(autouse=True)
def check_LDAP_db_integrity_call():
@@ -66,7 +68,7 @@ def check_permission_for_apps_call():
yield
check_permission_for_apps()
-@pytest.fixture(scope="session")
+@pytest.fixture(scope="module")
def secondary_domain(request):
if "example.test" not in domain_list()["domains"]:
@@ -107,16 +109,23 @@ def app_is_not_installed(domain, app):
def app_is_exposed_on_http(domain, path, message_in_page):
try:
- r = requests.get("http://127.0.0.1" + path + "/", headers={"Host": domain}, timeout=10)
+ r = requests.get("http://127.0.0.1" + path + "/", headers={"Host": domain}, timeout=10, verify=False)
return r.status_code == 200 and message_in_page in r.text
- except Exception:
+ except Exception as e:
return False
-def install_legacy_app(domain, path):
+def install_legacy_app(domain, path, public=True):
app_install("./tests/apps/legacy_app_ynh",
- args="domain=%s&path=%s" % (domain, path),
+ args="domain=%s&path=%s&is_public=%s" % (domain, path, 1 if public else 0),
+ force=True)
+
+
+def install_full_domain_app(domain):
+
+ app_install("./tests/apps/full_domain_app_ynh",
+ args="domain=%s" % domain,
force=True)
@@ -133,6 +142,12 @@ def test_legacy_app_install_main_domain():
install_legacy_app(main_domain, "/legacy")
+ app_map_ = app_map(raw=True)
+ assert main_domain in app_map_
+ assert '/legacy' in app_map_[main_domain]
+ assert 'id' in app_map_[main_domain]['/legacy']
+ assert app_map_[main_domain]['/legacy']['id'] == 'legacy_app'
+
assert app_is_installed(main_domain, "legacy_app")
assert app_is_exposed_on_http(main_domain, "/legacy", "This is a dummy app")
@@ -157,6 +172,12 @@ def test_legacy_app_install_secondary_domain_on_root(secondary_domain):
install_legacy_app(secondary_domain, "/")
+ app_map_ = app_map(raw=True)
+ assert secondary_domain in app_map_
+ assert '/' in app_map_[secondary_domain]
+ assert 'id' in app_map_[secondary_domain]['/']
+ assert app_map_[secondary_domain]['/']['id'] == 'legacy_app'
+
assert app_is_installed(secondary_domain, "legacy_app")
assert app_is_exposed_on_http(secondary_domain, "/", "This is a dummy app")
@@ -167,13 +188,7 @@ def test_legacy_app_install_secondary_domain_on_root(secondary_domain):
def test_legacy_app_install_private(secondary_domain):
- install_legacy_app(secondary_domain, "/legacy")
-
- settings = open("/etc/yunohost/apps/legacy_app/settings.yml", "r").read()
- new_settings = settings.replace("\nunprotected_uris: /", "")
- assert new_settings != settings
- open("/etc/yunohost/apps/legacy_app/settings.yml", "w").write(new_settings)
- app_ssowatconf()
+ install_legacy_app(secondary_domain, "/legacy", public=False)
assert app_is_installed(secondary_domain, "legacy_app")
assert not app_is_exposed_on_http(secondary_domain, "/legacy", "This is a dummy app")
@@ -183,11 +198,11 @@ def test_legacy_app_install_private(secondary_domain):
assert app_is_not_installed(secondary_domain, "legacy_app")
-def test_legacy_app_install_unknown_domain():
+def test_legacy_app_install_unknown_domain(mocker):
with pytest.raises(YunohostError):
- install_legacy_app("whatever.nope", "/legacy")
- # TODO check error message
+ with message(mocker, "app_argument_invalid"):
+ install_legacy_app("whatever.nope", "/legacy")
assert app_is_not_installed("whatever.nope", "legacy_app")
@@ -214,55 +229,51 @@ def test_legacy_app_install_multiple_instances(secondary_domain):
assert app_is_not_installed(secondary_domain, "legacy_app__2")
-def test_legacy_app_install_path_unavailable(secondary_domain):
+def test_legacy_app_install_path_unavailable(mocker, secondary_domain):
# These will be removed in teardown
install_legacy_app(secondary_domain, "/legacy")
with pytest.raises(YunohostError):
- install_legacy_app(secondary_domain, "/")
- # TODO check error message
+ with message(mocker, "app_location_unavailable"):
+ install_legacy_app(secondary_domain, "/")
assert app_is_installed(secondary_domain, "legacy_app")
assert app_is_not_installed(secondary_domain, "legacy_app__2")
-def test_legacy_app_install_bad_args():
-
- with pytest.raises(YunohostError):
- install_legacy_app("this.domain.does.not.exists", "/legacy")
-
-
-def test_legacy_app_install_with_nginx_down(secondary_domain):
+def test_legacy_app_install_with_nginx_down(mocker, secondary_domain):
os.system("systemctl stop nginx")
- with pytest.raises(YunohostError):
+ with raiseYunohostError(mocker, "app_action_cannot_be_ran_because_required_services_down"):
install_legacy_app(secondary_domain, "/legacy")
-def test_legacy_app_failed_install(secondary_domain):
+def test_legacy_app_failed_install(mocker, secondary_domain):
# This will conflict with the folder that the app
# attempts to create, making the install fail
mkdir("/var/www/legacy_app/", 0o750)
with pytest.raises(YunohostError):
- install_legacy_app(secondary_domain, "/legacy")
- # TODO check error message
+ with message(mocker, 'app_install_script_failed'):
+ install_legacy_app(secondary_domain, "/legacy")
assert app_is_not_installed(secondary_domain, "legacy_app")
-def test_legacy_app_failed_remove(secondary_domain):
+def test_legacy_app_failed_remove(mocker, secondary_domain):
install_legacy_app(secondary_domain, "/legacy")
# The remove script runs with set -eu and attempt to remove this
# file without -f, so will fail if it's not there ;)
os.remove("/etc/nginx/conf.d/%s.d/%s.conf" % (secondary_domain, "legacy_app"))
- with pytest.raises(YunohostError):
- app_remove("legacy")
+
+ # TODO / FIXME : can't easily validate that 'app_not_properly_removed'
+ # is triggered for weird reasons ...
+ app_remove("legacy_app")
#
# Well here, we hit the classical issue where if an app removal script
@@ -272,50 +283,69 @@ def test_legacy_app_failed_remove(secondary_domain):
assert app_is_not_installed(secondary_domain, "legacy")
-def test_systemfuckedup_during_app_install(secondary_domain):
+def test_full_domain_app(secondary_domain):
+
+ install_full_domain_app(secondary_domain)
+
+ assert app_is_exposed_on_http(secondary_domain, "/", "This is a dummy app")
+
+
+def test_full_domain_app_with_conflicts(mocker, secondary_domain):
+
+ install_legacy_app(secondary_domain, "/legacy")
+
+ with raiseYunohostError(mocker, "app_full_domain_unavailable"):
+ install_full_domain_app(secondary_domain)
+
+
+def test_systemfuckedup_during_app_install(mocker, secondary_domain):
with pytest.raises(YunohostError):
- install_break_yo_system(secondary_domain, breakwhat="install")
- os.system("nginx -t")
- os.system("systemctl status nginx")
+ with message(mocker, "app_install_failed"):
+ with message(mocker, 'app_action_broke_system'):
+ install_break_yo_system(secondary_domain, breakwhat="install")
assert app_is_not_installed(secondary_domain, "break_yo_system")
-def test_systemfuckedup_during_app_remove(secondary_domain):
+def test_systemfuckedup_during_app_remove(mocker, secondary_domain):
install_break_yo_system(secondary_domain, breakwhat="remove")
with pytest.raises(YunohostError):
- app_remove("break_yo_system")
- os.system("nginx -t")
- os.system("systemctl status nginx")
+ with message(mocker, 'app_action_broke_system'):
+ with message(mocker, 'app_removed'):
+ app_remove("break_yo_system")
assert app_is_not_installed(secondary_domain, "break_yo_system")
-def test_systemfuckedup_during_app_install_and_remove(secondary_domain):
+def test_systemfuckedup_during_app_install_and_remove(mocker, secondary_domain):
with pytest.raises(YunohostError):
- install_break_yo_system(secondary_domain, breakwhat="everything")
+ with message(mocker, "app_install_failed"):
+ with message(mocker, 'app_action_broke_system'):
+ install_break_yo_system(secondary_domain, breakwhat="everything")
assert app_is_not_installed(secondary_domain, "break_yo_system")
-def test_systemfuckedup_during_app_upgrade(secondary_domain):
+def test_systemfuckedup_during_app_upgrade(mocker, secondary_domain):
install_break_yo_system(secondary_domain, breakwhat="upgrade")
with pytest.raises(YunohostError):
- app_upgrade("break_yo_system", file="./tests/apps/break_yo_system_ynh")
+ with message(mocker, 'app_action_broke_system'):
+ app_upgrade("break_yo_system", file="./tests/apps/break_yo_system_ynh")
-def test_failed_multiple_app_upgrade(secondary_domain):
+def test_failed_multiple_app_upgrade(mocker, secondary_domain):
install_legacy_app(secondary_domain, "/legacy")
install_break_yo_system(secondary_domain, breakwhat="upgrade")
with pytest.raises(YunohostError):
- app_upgrade(["break_yo_system", "legacy_app"],
- file={"break_yo_system": "./tests/apps/break_yo_system_ynh",
- "legacy": "./tests/apps/legacy_app_ynh"})
+ with message(mocker, "app_not_upgraded"):
+ app_upgrade(["break_yo_system", "legacy_app"],
+ file={"break_yo_system": "./tests/apps/break_yo_system_ynh",
+ "legacy": "./tests/apps/legacy_app_ynh"})
diff --git a/src/yunohost/tests/test_appscatalog.py b/src/yunohost/tests/test_appscatalog.py
new file mode 100644
index 000000000..c3ece7907
--- /dev/null
+++ b/src/yunohost/tests/test_appscatalog.py
@@ -0,0 +1,372 @@
+import os
+import pytest
+import requests
+import requests_mock
+import glob
+import shutil
+
+from moulinette import m18n
+from moulinette.utils.filesystem import read_json, write_to_json, write_to_yaml, mkdir
+
+from yunohost.utils.error import YunohostError
+from yunohost.app import (_initialize_apps_catalog_system,
+ _read_apps_catalog_list,
+ _update_apps_catalog,
+ _actual_apps_catalog_api_url,
+ _load_apps_catalog,
+ app_catalog,
+ logger,
+ APPS_CATALOG_CACHE,
+ APPS_CATALOG_CONF,
+ APPS_CATALOG_CRON_PATH,
+ APPS_CATALOG_API_VERSION,
+ APPS_CATALOG_DEFAULT_URL)
+
+APPS_CATALOG_DEFAULT_URL_FULL = _actual_apps_catalog_api_url(APPS_CATALOG_DEFAULT_URL)
+CRON_FOLDER, CRON_NAME = APPS_CATALOG_CRON_PATH.rsplit("/", 1)
+
+DUMMY_APP_CATALOG = """{
+ "apps": {
+ "foo": {"id": "foo", "level": 4, "category": "yolo", "manifest":{"description": "Foo"}},
+ "bar": {"id": "bar", "level": 7, "category": "swag", "manifest":{"description": "Bar"}}
+ },
+ "categories": [
+ {"id": "yolo", "description": "YoLo", "title": {"en": "Yolo"}},
+ {"id": "swag", "description": "sWaG", "title": {"en": "Swag"}}
+ ]
+}
+"""
+
+class AnyStringWith(str):
+ def __eq__(self, other):
+ return self in other
+
+def setup_function(function):
+
+ # Clear apps catalog cache
+ shutil.rmtree(APPS_CATALOG_CACHE, ignore_errors=True)
+
+ # Clear apps_catalog cron
+ if os.path.exists(APPS_CATALOG_CRON_PATH):
+ os.remove(APPS_CATALOG_CRON_PATH)
+
+ # Clear apps_catalog conf
+ if os.path.exists(APPS_CATALOG_CONF):
+ os.remove(APPS_CATALOG_CONF)
+
+
+def teardown_function(function):
+
+ # Clear apps catalog cache
+ # Otherwise when using apps stuff after running the test,
+ # we'll still have the dummy unusable list
+ shutil.rmtree(APPS_CATALOG_CACHE, ignore_errors=True)
+
+
+def cron_job_is_there():
+ r = os.system("run-parts -v --test %s | grep %s" % (CRON_FOLDER, CRON_NAME))
+ return r == 0
+
+#
+# ################################################
+#
+
+
+def test_apps_catalog_init(mocker):
+
+ # Cache is empty
+ assert not glob.glob(APPS_CATALOG_CACHE + "/*")
+ # Conf doesn't exist yet
+ assert not os.path.exists(APPS_CATALOG_CONF)
+ # Conf doesn't exist yet
+ assert not os.path.exists(APPS_CATALOG_CRON_PATH)
+
+ # Initialize ...
+ mocker.spy(m18n, "n")
+ _initialize_apps_catalog_system()
+ m18n.n.assert_any_call('apps_catalog_init_success')
+
+ # Then there's a cron enabled
+ assert cron_job_is_there()
+
+ # And a conf with at least one list
+ assert os.path.exists(APPS_CATALOG_CONF)
+ apps_catalog_list = _read_apps_catalog_list()
+ assert len(apps_catalog_list)
+
+ # Cache is expected to still be empty though
+ # (if we did update the apps_catalog during init,
+ # we couldn't differentiate easily exceptions
+ # related to lack of network connectivity)
+ assert not glob.glob(APPS_CATALOG_CACHE + "/*")
+
+
+def test_apps_catalog_emptylist():
+
+ # Initialize ...
+ _initialize_apps_catalog_system()
+
+ # Let's imagine somebody removed the default apps catalog because uh idk they dont want to use our default apps catalog
+ os.system("rm %s" % APPS_CATALOG_CONF)
+ os.system("touch %s" % APPS_CATALOG_CONF)
+
+ apps_catalog_list = _read_apps_catalog_list()
+ assert not len(apps_catalog_list)
+
+
+def test_apps_catalog_update_nominal(mocker):
+
+ # Initialize ...
+ _initialize_apps_catalog_system()
+
+ # Cache is empty
+ assert not glob.glob(APPS_CATALOG_CACHE + "/*")
+
+ # Update
+ with requests_mock.Mocker() as m:
+
+ _actual_apps_catalog_api_url,
+ # Mock the server response with a dummy apps catalog
+ m.register_uri("GET", APPS_CATALOG_DEFAULT_URL_FULL, text=DUMMY_APP_CATALOG)
+
+ mocker.spy(m18n, "n")
+ _update_apps_catalog()
+ m18n.n.assert_any_call("apps_catalog_updating")
+ m18n.n.assert_any_call("apps_catalog_update_success")
+
+ # Cache shouldn't be empty anymore empty
+ assert glob.glob(APPS_CATALOG_CACHE + "/*")
+
+ # And if we load the catalog, we sould find
+ # - foo and bar as apps (unordered),
+ # - yolo and swag as categories (ordered)
+ catalog = app_catalog(with_categories=True)
+
+ assert "apps" in catalog
+ assert set(catalog["apps"].keys()) == set(["foo", "bar"])
+
+ assert "categories" in catalog
+ assert [c["id"] for c in catalog["categories"]] == ["yolo", "swag"]
+
+
+def test_apps_catalog_update_404(mocker):
+
+ # Initialize ...
+ _initialize_apps_catalog_system()
+
+ with requests_mock.Mocker() as m:
+
+ # 404 error
+ m.register_uri("GET", APPS_CATALOG_DEFAULT_URL_FULL,
+ status_code=404)
+
+ with pytest.raises(YunohostError):
+ mocker.spy(m18n, "n")
+ _update_apps_catalog()
+ m18n.n.assert_any_call("apps_catalog_failed_to_download")
+
+def test_apps_catalog_update_timeout(mocker):
+
+ # Initialize ...
+ _initialize_apps_catalog_system()
+
+ with requests_mock.Mocker() as m:
+
+ # Timeout
+ m.register_uri("GET", APPS_CATALOG_DEFAULT_URL_FULL,
+ exc=requests.exceptions.ConnectTimeout)
+
+ with pytest.raises(YunohostError):
+ mocker.spy(m18n, "n")
+ _update_apps_catalog()
+ m18n.n.assert_any_call("apps_catalog_failed_to_download")
+
+
+def test_apps_catalog_update_sslerror(mocker):
+
+ # Initialize ...
+ _initialize_apps_catalog_system()
+
+ with requests_mock.Mocker() as m:
+
+ # SSL error
+ m.register_uri("GET", APPS_CATALOG_DEFAULT_URL_FULL,
+ exc=requests.exceptions.SSLError)
+
+ with pytest.raises(YunohostError):
+ mocker.spy(m18n, "n")
+ _update_apps_catalog()
+ m18n.n.assert_any_call("apps_catalog_failed_to_download")
+
+
+def test_apps_catalog_update_corrupted(mocker):
+
+ # Initialize ...
+ _initialize_apps_catalog_system()
+
+ with requests_mock.Mocker() as m:
+
+ # Corrupted json
+ m.register_uri("GET", APPS_CATALOG_DEFAULT_URL_FULL,
+ text=DUMMY_APP_CATALOG[:-2])
+
+ with pytest.raises(YunohostError):
+ mocker.spy(m18n, "n")
+ _update_apps_catalog()
+ m18n.n.assert_any_call("apps_catalog_failed_to_download")
+
+
+def test_apps_catalog_load_with_empty_cache(mocker):
+
+ # Initialize ...
+ _initialize_apps_catalog_system()
+
+ # Cache is empty
+ assert not glob.glob(APPS_CATALOG_CACHE + "/*")
+
+ # Update
+ with requests_mock.Mocker() as m:
+
+ # Mock the server response with a dummy apps catalog
+ m.register_uri("GET", APPS_CATALOG_DEFAULT_URL_FULL, text=DUMMY_APP_CATALOG)
+
+ # Try to load the apps catalog
+ # This should implicitly trigger an update in the background
+ mocker.spy(m18n, "n")
+ app_dict = _load_apps_catalog()["apps"]
+ m18n.n.assert_any_call("apps_catalog_obsolete_cache")
+ m18n.n.assert_any_call("apps_catalog_update_success")
+
+
+ # Cache shouldn't be empty anymore empty
+ assert glob.glob(APPS_CATALOG_CACHE + "/*")
+
+ assert "foo" in app_dict.keys()
+ assert "bar" in app_dict.keys()
+
+
+def test_apps_catalog_load_with_conflicts_between_lists(mocker):
+
+ # Initialize ...
+ _initialize_apps_catalog_system()
+
+ conf = [{"id": "default", "url": APPS_CATALOG_DEFAULT_URL},
+ {"id": "default2", "url": APPS_CATALOG_DEFAULT_URL.replace("yunohost.org", "yolohost.org")}]
+
+ write_to_yaml(APPS_CATALOG_CONF, conf)
+
+ # Update
+ with requests_mock.Mocker() as m:
+
+ # Mock the server response with a dummy apps catalog
+ # + the same apps catalog for the second list
+ m.register_uri("GET", APPS_CATALOG_DEFAULT_URL_FULL, text=DUMMY_APP_CATALOG)
+ m.register_uri("GET", APPS_CATALOG_DEFAULT_URL_FULL.replace("yunohost.org", "yolohost.org"), text=DUMMY_APP_CATALOG)
+
+ # Try to load the apps catalog
+ # This should implicitly trigger an update in the background
+ mocker.spy(logger, "warning")
+ app_dict = _load_apps_catalog()["apps"]
+ logger.warning.assert_any_call(AnyStringWith("Duplicate"))
+
+ # Cache shouldn't be empty anymore empty
+ assert glob.glob(APPS_CATALOG_CACHE + "/*")
+
+ assert "foo" in app_dict.keys()
+ assert "bar" in app_dict.keys()
+
+
+def test_apps_catalog_load_with_oudated_api_version(mocker):
+
+ # Initialize ...
+ _initialize_apps_catalog_system()
+
+ # Update
+ with requests_mock.Mocker() as m:
+
+ mocker.spy(m18n, "n")
+ m.register_uri("GET", APPS_CATALOG_DEFAULT_URL_FULL, text=DUMMY_APP_CATALOG)
+ _update_apps_catalog()
+
+ # Cache shouldn't be empty anymore empty
+ assert glob.glob(APPS_CATALOG_CACHE + "/*")
+
+ # Tweak the cache to replace the from_api_version with a different one
+ for cache_file in glob.glob(APPS_CATALOG_CACHE + "/*"):
+ cache_json = read_json(cache_file)
+ assert cache_json["from_api_version"] == APPS_CATALOG_API_VERSION
+ cache_json["from_api_version"] = 0
+ write_to_json(cache_file, cache_json)
+
+ # Update
+ with requests_mock.Mocker() as m:
+
+ # Mock the server response with a dummy apps catalog
+ m.register_uri("GET", APPS_CATALOG_DEFAULT_URL_FULL, text=DUMMY_APP_CATALOG)
+
+ mocker.spy(m18n, "n")
+ app_dict = _load_apps_catalog()["apps"]
+ m18n.n.assert_any_call("apps_catalog_update_success")
+
+ assert "foo" in app_dict.keys()
+ assert "bar" in app_dict.keys()
+
+ # Check that we indeed have the new api number in cache
+ for cache_file in glob.glob(APPS_CATALOG_CACHE + "/*"):
+ cache_json = read_json(cache_file)
+ assert cache_json["from_api_version"] == APPS_CATALOG_API_VERSION
+
+
+
+def test_apps_catalog_migrate_legacy_explicitly():
+
+ open("/etc/yunohost/appslists.json", "w").write('{"yunohost": {"yolo":"swag"}}')
+ mkdir(APPS_CATALOG_CACHE, 0o750, parents=True)
+ open(APPS_CATALOG_CACHE+"/yunohost_old.json", "w").write('{"foo":{}, "bar": {}}')
+ open(APPS_CATALOG_CRON_PATH, "w").write("# Some old cron")
+
+ from yunohost.tools import _get_migration_by_name
+ migration = _get_migration_by_name("futureproof_apps_catalog_system")
+
+ with requests_mock.Mocker() as m:
+
+ # Mock the server response with a dummy apps catalog
+ m.register_uri("GET", APPS_CATALOG_DEFAULT_URL_FULL, text=DUMMY_APP_CATALOG)
+ migration.run()
+
+ # Old conf shouldnt be there anymore (got renamed to .old)
+ assert not os.path.exists("/etc/yunohost/appslists.json")
+ # Old cache should have been removed
+ assert not os.path.exists(APPS_CATALOG_CACHE+"/yunohost_old.json")
+ # Cron should have been changed
+ assert "/bin/bash" in open(APPS_CATALOG_CRON_PATH, "r").read()
+ assert cron_job_is_there()
+
+ # Reading the apps_catalog should work
+ app_dict = _load_apps_catalog()["apps"]
+ assert "foo" in app_dict.keys()
+ assert "bar" in app_dict.keys()
+
+
+def test_apps_catalog_migrate_legacy_implicitly():
+
+ open("/etc/yunohost/appslists.json", "w").write('{"yunohost": {"yolo":"swag"}}')
+ mkdir(APPS_CATALOG_CACHE, 0o750, parents=True)
+ open(APPS_CATALOG_CACHE+"/yunohost_old.json", "w").write('{"old_foo":{}, "old_bar": {}}')
+ open(APPS_CATALOG_CRON_PATH, "w").write("# Some old cron")
+
+ with requests_mock.Mocker() as m:
+ m.register_uri("GET", APPS_CATALOG_DEFAULT_URL_FULL, text=DUMMY_APP_CATALOG)
+ app_dict = _load_apps_catalog()["apps"]
+
+ assert "foo" in app_dict.keys()
+ assert "bar" in app_dict.keys()
+
+ # Old conf shouldnt be there anymore (got renamed to .old)
+ assert not os.path.exists("/etc/yunohost/appslists.json")
+ # Old cache should have been removed
+ assert not os.path.exists(APPS_CATALOG_CACHE+"/yunohost_old.json")
+ # Cron should have been changed
+ assert "/bin/bash" in open(APPS_CATALOG_CRON_PATH, "r").read()
+ assert cron_job_is_there()
+
diff --git a/src/yunohost/tests/test_appslist.py b/src/yunohost/tests/test_appslist.py
deleted file mode 100644
index 817807ed9..000000000
--- a/src/yunohost/tests/test_appslist.py
+++ /dev/null
@@ -1,389 +0,0 @@
-import os
-import pytest
-import requests
-import requests_mock
-import glob
-import time
-
-from yunohost.utils.error import YunohostError
-
-from yunohost.app import app_fetchlist, app_removelist, app_listlists, _using_legacy_appslist_system, _migrate_appslist_system, _register_new_appslist
-
-URL_OFFICIAL_APP_LIST = "https://app.yunohost.org/official.json"
-REPO_PATH = '/var/cache/yunohost/repo'
-APPSLISTS_JSON = '/etc/yunohost/appslists.json'
-
-
-def setup_function(function):
-
- # Clear all appslist
- files = glob.glob(REPO_PATH + "/*")
- for f in files:
- os.remove(f)
-
- # Clear appslist crons
- files = glob.glob("/etc/cron.d/yunohost-applist-*")
- for f in files:
- os.remove(f)
-
- if os.path.exists("/etc/cron.daily/yunohost-fetch-appslists"):
- os.remove("/etc/cron.daily/yunohost-fetch-appslists")
-
- if os.path.exists(APPSLISTS_JSON):
- os.remove(APPSLISTS_JSON)
-
-
-def teardown_function(function):
- pass
-
-
-def cron_job_is_there():
- r = os.system("run-parts -v --test /etc/cron.daily/ | grep yunohost-fetch-appslists")
- return r == 0
-
-
-#
-# Test listing of appslists and registering of appslists #
-#
-
-
-def test_appslist_list_empty():
- """
- Calling app_listlists() with no registered list should return empty dict
- """
-
- assert app_listlists() == {}
-
-
-def test_appslist_list_register():
- """
- Register a new list
- """
-
- # Assume we're starting with an empty app list
- assert app_listlists() == {}
-
- # Register a new dummy list
- _register_new_appslist("https://lol.com/appslist.json", "dummy")
-
- appslist_dict = app_listlists()
- assert "dummy" in appslist_dict.keys()
- assert appslist_dict["dummy"]["url"] == "https://lol.com/appslist.json"
-
- assert cron_job_is_there()
-
-
-def test_appslist_list_register_conflict_name():
- """
- Attempt to register a new list with conflicting name
- """
-
- _register_new_appslist("https://lol.com/appslist.json", "dummy")
- with pytest.raises(YunohostError):
- _register_new_appslist("https://lol.com/appslist2.json", "dummy")
-
- appslist_dict = app_listlists()
-
- assert "dummy" in appslist_dict.keys()
- assert "dummy2" not in appslist_dict.keys()
-
-
-def test_appslist_list_register_conflict_url():
- """
- Attempt to register a new list with conflicting url
- """
-
- _register_new_appslist("https://lol.com/appslist.json", "dummy")
- with pytest.raises(YunohostError):
- _register_new_appslist("https://lol.com/appslist.json", "plopette")
-
- appslist_dict = app_listlists()
-
- assert "dummy" in appslist_dict.keys()
- assert "plopette" not in appslist_dict.keys()
-
-
-#
-# Test fetching of appslists #
-#
-
-
-def test_appslist_fetch():
- """
- Do a fetchlist and test the .json got updated.
- """
- assert app_listlists() == {}
-
- _register_new_appslist(URL_OFFICIAL_APP_LIST, "yunohost")
-
- with requests_mock.Mocker() as m:
-
- # Mock the server response with a valid (well, empty, yep) json
- m.register_uri("GET", URL_OFFICIAL_APP_LIST, text='{ }')
-
- official_lastUpdate = app_listlists()["yunohost"]["lastUpdate"]
- app_fetchlist()
- new_official_lastUpdate = app_listlists()["yunohost"]["lastUpdate"]
-
- assert new_official_lastUpdate > official_lastUpdate
-
-
-def test_appslist_fetch_single_appslist():
- """
- Register several lists but only fetch one. Check only one got updated.
- """
-
- assert app_listlists() == {}
- _register_new_appslist(URL_OFFICIAL_APP_LIST, "yunohost")
- _register_new_appslist("https://lol.com/appslist.json", "dummy")
-
- time.sleep(1)
-
- with requests_mock.Mocker() as m:
-
- # Mock the server response with a valid (well, empty, yep) json
- m.register_uri("GET", URL_OFFICIAL_APP_LIST, text='{ }')
-
- official_lastUpdate = app_listlists()["yunohost"]["lastUpdate"]
- dummy_lastUpdate = app_listlists()["dummy"]["lastUpdate"]
- app_fetchlist(name="yunohost")
- new_official_lastUpdate = app_listlists()["yunohost"]["lastUpdate"]
- new_dummy_lastUpdate = app_listlists()["dummy"]["lastUpdate"]
-
- assert new_official_lastUpdate > official_lastUpdate
- assert new_dummy_lastUpdate == dummy_lastUpdate
-
-
-def test_appslist_fetch_unknownlist():
- """
- Attempt to fetch an unknown list
- """
-
- assert app_listlists() == {}
-
- with pytest.raises(YunohostError):
- app_fetchlist(name="swag")
-
-
-def test_appslist_fetch_url_but_no_name():
- """
- Do a fetchlist with url given, but no name given
- """
-
- with pytest.raises(YunohostError):
- app_fetchlist(url=URL_OFFICIAL_APP_LIST)
-
-
-def test_appslist_fetch_badurl():
- """
- Do a fetchlist with a bad url
- """
-
- app_fetchlist(url="https://not.a.valid.url/plop.json", name="plop")
-
-
-def test_appslist_fetch_badfile():
- """
- Do a fetchlist and mock a response with a bad json
- """
- assert app_listlists() == {}
-
- _register_new_appslist(URL_OFFICIAL_APP_LIST, "yunohost")
-
- with requests_mock.Mocker() as m:
-
- m.register_uri("GET", URL_OFFICIAL_APP_LIST, text='{ not json lol }')
-
- app_fetchlist()
-
-
-def test_appslist_fetch_404():
- """
- Do a fetchlist and mock a 404 response
- """
- assert app_listlists() == {}
-
- _register_new_appslist(URL_OFFICIAL_APP_LIST, "yunohost")
-
- with requests_mock.Mocker() as m:
-
- m.register_uri("GET", URL_OFFICIAL_APP_LIST, status_code=404)
-
- app_fetchlist()
-
-
-def test_appslist_fetch_sslerror():
- """
- Do a fetchlist and mock an SSL error
- """
- assert app_listlists() == {}
-
- _register_new_appslist(URL_OFFICIAL_APP_LIST, "yunohost")
-
- with requests_mock.Mocker() as m:
-
- m.register_uri("GET", URL_OFFICIAL_APP_LIST,
- exc=requests.exceptions.SSLError)
-
- app_fetchlist()
-
-
-def test_appslist_fetch_timeout():
- """
- Do a fetchlist and mock a timeout
- """
- assert app_listlists() == {}
-
- _register_new_appslist(URL_OFFICIAL_APP_LIST, "yunohost")
-
- with requests_mock.Mocker() as m:
-
- m.register_uri("GET", URL_OFFICIAL_APP_LIST,
- exc=requests.exceptions.ConnectTimeout)
-
- app_fetchlist()
-
-
-#
-# Test remove of appslist #
-#
-
-
-def test_appslist_remove():
- """
- Register a new appslist, then remove it
- """
-
- # Assume we're starting with an empty app list
- assert app_listlists() == {}
-
- # Register a new dummy list
- _register_new_appslist("https://lol.com/appslist.json", "dummy")
- app_removelist("dummy")
-
- # Should end up with no list registered
- assert app_listlists() == {}
-
-
-def test_appslist_remove_unknown():
- """
- Attempt to remove an unknown list
- """
-
- with pytest.raises(YunohostError):
- app_removelist("dummy")
-
-
-#
-# Test migration from legacy appslist system #
-#
-
-
-def add_legacy_cron(name, url):
- with open("/etc/cron.d/yunohost-applist-%s" % name, "w") as f:
- f.write('00 00 * * * root yunohost app fetchlist -u %s -n %s > /dev/null 2>&1\n' % (url, name))
-
-
-def test_appslist_check_using_legacy_system_testFalse():
- """
- If no legacy cron job is there, the check should return False
- """
- assert glob.glob("/etc/cron.d/yunohost-applist-*") == []
- assert _using_legacy_appslist_system() is False
-
-
-def test_appslist_check_using_legacy_system_testTrue():
- """
- If there's a legacy cron job, the check should return True
- """
- assert glob.glob("/etc/cron.d/yunohost-applist-*") == []
- add_legacy_cron("yunohost", "https://app.yunohost.org/official.json")
- assert _using_legacy_appslist_system() is True
-
-
-def test_appslist_system_migration():
- """
- Test that legacy cron jobs get migrated correctly when calling app_listlists
- """
-
- # Start with no legacy cron, no appslist registered
- assert glob.glob("/etc/cron.d/yunohost-applist-*") == []
- assert app_listlists() == {}
- assert not os.path.exists("/etc/cron.daily/yunohost-fetch-appslists")
-
- # Add a few legacy crons
- add_legacy_cron("yunohost", "https://app.yunohost.org/official.json")
- add_legacy_cron("dummy", "https://swiggitty.swaggy.lol/yolo.json")
-
- # Migrate
- assert _using_legacy_appslist_system() is True
- _migrate_appslist_system()
- assert _using_legacy_appslist_system() is False
-
- # No legacy cron job should remain
- assert glob.glob("/etc/cron.d/yunohost-applist-*") == []
-
- # Check they are in app_listlists anyway
- appslist_dict = app_listlists()
- assert "yunohost" in appslist_dict.keys()
- assert appslist_dict["yunohost"]["url"] == "https://app.yunohost.org/official.json"
- assert "dummy" in appslist_dict.keys()
- assert appslist_dict["dummy"]["url"] == "https://swiggitty.swaggy.lol/yolo.json"
-
- assert cron_job_is_there()
-
-
-def test_appslist_system_migration_badcron():
- """
- Test the migration on a bad legacy cron (no url found inside cron job)
- """
-
- # Start with no legacy cron, no appslist registered
- assert glob.glob("/etc/cron.d/yunohost-applist-*") == []
- assert app_listlists() == {}
- assert not os.path.exists("/etc/cron.daily/yunohost-fetch-appslists")
-
- # Add a "bad" legacy cron
- add_legacy_cron("wtflist", "ftp://the.fuck.is.this")
-
- # Migrate
- assert _using_legacy_appslist_system() is True
- _migrate_appslist_system()
- assert _using_legacy_appslist_system() is False
-
- # No legacy cron should remain, but it should be backuped in /etc/yunohost
- assert glob.glob("/etc/cron.d/yunohost-applist-*") == []
- assert os.path.exists("/etc/yunohost/wtflist.oldlist.bkp")
-
- # Appslist should still be empty
- assert app_listlists() == {}
-
-
-def test_appslist_system_migration_conflict():
- """
- Test migration of conflicting cron job (in terms of url)
- """
-
- # Start with no legacy cron, no appslist registered
- assert glob.glob("/etc/cron.d/yunohost-applist-*") == []
- assert app_listlists() == {}
- assert not os.path.exists("/etc/cron.daily/yunohost-fetch-appslists")
-
- # Add a few legacy crons
- add_legacy_cron("yunohost", "https://app.yunohost.org/official.json")
- add_legacy_cron("dummy", "https://app.yunohost.org/official.json")
-
- # Migrate
- assert _using_legacy_appslist_system() is True
- _migrate_appslist_system()
- assert _using_legacy_appslist_system() is False
-
- # No legacy cron job should remain
- assert glob.glob("/etc/cron.d/yunohost-applist-*") == []
-
- # Only one among "dummy" and "yunohost" should be listed
- appslist_dict = app_listlists()
- assert (len(appslist_dict.keys()) == 1)
- assert ("dummy" in appslist_dict.keys()) or ("yunohost" in appslist_dict.keys())
-
- assert cron_job_is_there()
diff --git a/src/yunohost/tests/test_backuprestore.py b/src/yunohost/tests/test_backuprestore.py
index 7d384a46a..c7a4f9016 100644
--- a/src/yunohost/tests/test_backuprestore.py
+++ b/src/yunohost/tests/test_backuprestore.py
@@ -2,15 +2,14 @@ import pytest
import os
import shutil
import subprocess
-from mock import ANY
-from moulinette import m18n
+from conftest import message, raiseYunohostError
+
from yunohost.app import app_install, app_remove, app_ssowatconf
from yunohost.app import _is_installed
from yunohost.backup import backup_create, backup_restore, backup_list, backup_info, backup_delete, _recursive_umount
from yunohost.domain import _get_maindomain
-from yunohost.utils.error import YunohostError
-from yunohost.user import user_permission_list
+from yunohost.user import user_permission_list, user_create, user_list, user_delete
from yunohost.tests.test_permission import check_LDAP_db_integrity, check_permission_for_apps
# Get main domain
@@ -38,10 +37,10 @@ def setup_function(function):
add_archive_wordpress_from_2p4()
assert len(backup_list()["archives"]) == 1
- if "with_backup_legacy_app_installed" in markers:
- assert not app_is_installed("backup_legacy_app")
- install_app("backup_legacy_app_ynh", "/yolo")
- assert app_is_installed("backup_legacy_app")
+ if "with_legacy_app_installed" in markers:
+ assert not app_is_installed("legacy_app")
+ install_app("legacy_app_ynh", "/yolo")
+ assert app_is_installed("legacy_app")
if "with_backup_recommended_app_installed" in markers:
assert not app_is_installed("backup_recommended_app")
@@ -59,6 +58,13 @@ def setup_function(function):
add_archive_system_from_2p4()
assert len(backup_list()["archives"]) == 1
+ if "with_permission_app_installed" in markers:
+ assert not app_is_installed("permissions_app")
+ user_create("alice", "Alice", "White", "alice@" + maindomain, "test123Ynh")
+ install_app("permissions_app_ynh", "/urlpermissionapp"
+ "&admin=alice")
+ assert app_is_installed("permissions_app")
+
def teardown_function(function):
@@ -73,6 +79,9 @@ def teardown_function(function):
if "clean_opt_dir" in markers:
shutil.rmtree("/opt/test_backup_output_directory")
+ if "alice" in user_list()["users"]:
+ user_delete("alice")
+
@pytest.fixture(autouse=True)
def check_LDAP_db_integrity_call():
@@ -92,6 +101,9 @@ def check_permission_for_apps_call():
def app_is_installed(app):
+ if app == "permissions_app":
+ return _is_installed(app)
+
# These are files we know should be installed by the app
app_files = []
app_files.append("/etc/nginx/conf.d/%s.d/%s.conf" % (maindomain, app))
@@ -105,7 +117,7 @@ def backup_test_dependencies_are_met():
# Dummy test apps (or backup archives)
assert os.path.exists("./tests/apps/backup_wordpress_from_2p4")
- assert os.path.exists("./tests/apps/backup_legacy_app_ynh")
+ assert os.path.exists("./tests/apps/legacy_app_ynh")
assert os.path.exists("./tests/apps/backup_recommended_app_ynh")
return True
@@ -155,14 +167,9 @@ def delete_all_backups():
def uninstall_test_apps_if_needed():
- if _is_installed("backup_legacy_app"):
- app_remove("backup_legacy_app")
-
- if _is_installed("backup_recommended_app"):
- app_remove("backup_recommended_app")
-
- if _is_installed("wordpress"):
- app_remove("wordpress")
+ for app in ["legacy_app", "backup_recommended_app", "wordpress", "permissions_app"]:
+ if _is_installed(app):
+ app_remove(app)
def install_app(app, path, additionnal_args=""):
@@ -198,10 +205,11 @@ def add_archive_system_from_2p4():
#
-def test_backup_only_ldap():
+def test_backup_only_ldap(mocker):
# Create the backup
- backup_create(system=["conf_ldap"], apps=None)
+ with message(mocker, "backup_created"):
+ backup_create(system=["conf_ldap"], apps=None)
archives = backup_list()["archives"]
assert len(archives) == 1
@@ -214,24 +222,22 @@ def test_backup_only_ldap():
def test_backup_system_part_that_does_not_exists(mocker):
- mocker.spy(m18n, "n")
-
# Create the backup
- with pytest.raises(YunohostError):
- backup_create(system=["yolol"], apps=None)
+ with message(mocker, 'backup_hook_unknown', hook="doesnt_exist"):
+ with raiseYunohostError(mocker, "backup_nothings_done"):
+ backup_create(system=["doesnt_exist"], apps=None)
- m18n.n.assert_any_call('backup_hook_unknown', hook="yolol")
- m18n.n.assert_any_call('backup_nothings_done')
#
# System backup and restore #
#
-def test_backup_and_restore_all_sys():
+def test_backup_and_restore_all_sys(mocker):
# Create the backup
- backup_create(system=[], apps=None)
+ with message(mocker, "backup_created"):
+ backup_create(system=[], apps=None)
archives = backup_list()["archives"]
assert len(archives) == 1
@@ -247,8 +253,9 @@ def test_backup_and_restore_all_sys():
assert not os.path.exists("/etc/ssowat/conf.json")
# Restore the backup
- backup_restore(name=archives[0], force=True,
- system=[], apps=None)
+ with message(mocker, "restore_complete"):
+ backup_restore(name=archives[0], force=True,
+ system=[], apps=None)
# Check ssowat conf is back
assert os.path.exists("/etc/ssowat/conf.json")
@@ -262,16 +269,18 @@ def test_backup_and_restore_all_sys():
def test_restore_system_from_Ynh2p4(monkeypatch, mocker):
# Backup current system
- backup_create(system=[], apps=None)
+ with message(mocker, "backup_created"):
+ backup_create(system=[], apps=None)
archives = backup_list()["archives"]
assert len(archives) == 2
# Restore system archive from 2.4
try:
- backup_restore(name=backup_list()["archives"][1],
- system=[],
- apps=None,
- force=True)
+ with message(mocker, "restore_complete"):
+ backup_restore(name=backup_list()["archives"][1],
+ system=[],
+ apps=None,
+ force=True)
finally:
# Restore system as it was
backup_restore(name=backup_list()["archives"][0],
@@ -298,12 +307,10 @@ def test_backup_script_failure_handling(monkeypatch, mocker):
# call with monkeypatch). We also patch m18n to check later it's been called
# with the expected error message key
monkeypatch.setattr("yunohost.backup.hook_exec", custom_hook_exec)
- mocker.spy(m18n, "n")
- with pytest.raises(YunohostError):
- backup_create(system=None, apps=["backup_recommended_app"])
-
- m18n.n.assert_any_call('backup_app_failed', app='backup_recommended_app')
+ with message(mocker, 'backup_app_failed', app='backup_recommended_app'):
+ with raiseYunohostError(mocker, 'backup_nothings_done'):
+ backup_create(system=None, apps=["backup_recommended_app"])
@pytest.mark.with_backup_recommended_app_installed
@@ -319,25 +326,17 @@ def test_backup_not_enough_free_space(monkeypatch, mocker):
monkeypatch.setattr("yunohost.backup.free_space_in_directory",
custom_free_space_in_directory)
- mocker.spy(m18n, "n")
-
- with pytest.raises(YunohostError):
+ with raiseYunohostError(mocker, 'not_enough_disk_space'):
backup_create(system=None, apps=["backup_recommended_app"])
- m18n.n.assert_any_call('not_enough_disk_space', path=ANY)
-
def test_backup_app_not_installed(mocker):
assert not _is_installed("wordpress")
- mocker.spy(m18n, "n")
-
- with pytest.raises(YunohostError):
- backup_create(system=None, apps=["wordpress"])
-
- m18n.n.assert_any_call("unbackup_app", app="wordpress")
- m18n.n.assert_any_call('backup_nothings_done')
+ with message(mocker, "unbackup_app", app="wordpress"):
+ with raiseYunohostError(mocker, 'backup_nothings_done'):
+ backup_create(system=None, apps=["wordpress"])
@pytest.mark.with_backup_recommended_app_installed
@@ -347,13 +346,9 @@ def test_backup_app_with_no_backup_script(mocker):
os.system("rm %s" % backup_script)
assert not os.path.exists(backup_script)
- mocker.spy(m18n, "n")
-
- with pytest.raises(YunohostError):
- backup_create(system=None, apps=["backup_recommended_app"])
-
- m18n.n.assert_any_call("backup_with_no_backup_script_for_app", app="backup_recommended_app")
- m18n.n.assert_any_call('backup_nothings_done')
+ with message(mocker, "backup_with_no_backup_script_for_app", app="backup_recommended_app"):
+ with raiseYunohostError(mocker, 'backup_nothings_done'):
+ backup_create(system=None, apps=["backup_recommended_app"])
@pytest.mark.with_backup_recommended_app_installed
@@ -363,23 +358,21 @@ def test_backup_app_with_no_restore_script(mocker):
os.system("rm %s" % restore_script)
assert not os.path.exists(restore_script)
- mocker.spy(m18n, "n")
-
# Backuping an app with no restore script will only display a warning to the
# user...
- backup_create(system=None, apps=["backup_recommended_app"])
-
- m18n.n.assert_any_call("backup_with_no_restore_script_for_app", app="backup_recommended_app")
+ with message(mocker, "backup_with_no_restore_script_for_app", app="backup_recommended_app"):
+ backup_create(system=None, apps=["backup_recommended_app"])
@pytest.mark.clean_opt_dir
-def test_backup_with_different_output_directory():
+def test_backup_with_different_output_directory(mocker):
# Create the backup
- backup_create(system=["conf_ssh"], apps=None,
- output_directory="/opt/test_backup_output_directory",
- name="backup")
+ with message(mocker, "backup_created"):
+ backup_create(system=["conf_ssh"], apps=None,
+ output_directory="/opt/test_backup_output_directory",
+ name="backup")
assert os.path.exists("/opt/test_backup_output_directory/backup.tar.gz")
@@ -393,12 +386,14 @@ def test_backup_with_different_output_directory():
@pytest.mark.clean_opt_dir
-def test_backup_with_no_compress():
+def test_backup_with_no_compress(mocker):
+
# Create the backup
- backup_create(system=["conf_nginx"], apps=None,
- output_directory="/opt/test_backup_output_directory",
- no_compress=True,
- name="backup")
+ with message(mocker, "backup_created"):
+ backup_create(system=["conf_nginx"], apps=None,
+ output_directory="/opt/test_backup_output_directory",
+ no_compress=True,
+ name="backup")
assert os.path.exists("/opt/test_backup_output_directory/info.json")
@@ -408,10 +403,11 @@ def test_backup_with_no_compress():
#
@pytest.mark.with_wordpress_archive_from_2p4
-def test_restore_app_wordpress_from_Ynh2p4():
+def test_restore_app_wordpress_from_Ynh2p4(mocker):
- backup_restore(system=None, name=backup_list()["archives"][0],
- apps=["wordpress"])
+ with message(mocker, "restore_complete"):
+ backup_restore(system=None, name=backup_list()["archives"][0],
+ apps=["wordpress"])
@pytest.mark.with_wordpress_archive_from_2p4
@@ -423,16 +419,14 @@ def test_restore_app_script_failure_handling(monkeypatch, mocker):
raise Exception
monkeypatch.setattr("yunohost.backup.hook_exec", custom_hook_exec)
- mocker.spy(m18n, "n")
assert not _is_installed("wordpress")
- with pytest.raises(YunohostError):
- backup_restore(system=None, name=backup_list()["archives"][0],
- apps=["wordpress"])
+ with message(mocker, 'restore_app_failed', app='wordpress'):
+ with raiseYunohostError(mocker, 'restore_nothings_done'):
+ backup_restore(system=None, name=backup_list()["archives"][0],
+ apps=["wordpress"])
- m18n.n.assert_any_call('restore_app_failed', app='wordpress')
- m18n.n.assert_any_call('restore_nothings_done')
assert not _is_installed("wordpress")
@@ -444,18 +438,13 @@ def test_restore_app_not_enough_free_space(monkeypatch, mocker):
monkeypatch.setattr("yunohost.backup.free_space_in_directory",
custom_free_space_in_directory)
- mocker.spy(m18n, "n")
assert not _is_installed("wordpress")
- with pytest.raises(YunohostError):
+ with raiseYunohostError(mocker, 'restore_not_enough_disk_space'):
backup_restore(system=None, name=backup_list()["archives"][0],
apps=["wordpress"])
- m18n.n.assert_any_call('restore_not_enough_disk_space',
- free_space=0,
- margin=ANY,
- needed_space=ANY)
assert not _is_installed("wordpress")
@@ -465,13 +454,11 @@ def test_restore_app_not_in_backup(mocker):
assert not _is_installed("wordpress")
assert not _is_installed("yoloswag")
- mocker.spy(m18n, "n")
+ with message(mocker, 'backup_archive_app_not_found', app="yoloswag"):
+ with raiseYunohostError(mocker, 'restore_nothings_done'):
+ backup_restore(system=None, name=backup_list()["archives"][0],
+ apps=["yoloswag"])
- with pytest.raises(YunohostError):
- backup_restore(system=None, name=backup_list()["archives"][0],
- apps=["yoloswag"])
-
- m18n.n.assert_any_call('backup_archive_app_not_found', app="yoloswag")
assert not _is_installed("wordpress")
assert not _is_installed("yoloswag")
@@ -481,44 +468,74 @@ def test_restore_app_already_installed(mocker):
assert not _is_installed("wordpress")
- backup_restore(system=None, name=backup_list()["archives"][0],
- apps=["wordpress"])
-
- assert _is_installed("wordpress")
-
- mocker.spy(m18n, "n")
- with pytest.raises(YunohostError):
+ with message(mocker, "restore_complete"):
backup_restore(system=None, name=backup_list()["archives"][0],
apps=["wordpress"])
- m18n.n.assert_any_call('restore_already_installed_app', app="wordpress")
- m18n.n.assert_any_call('restore_nothings_done')
+ assert _is_installed("wordpress")
+
+ with message(mocker, 'restore_already_installed_app', app="wordpress"):
+ with raiseYunohostError(mocker, 'restore_nothings_done'):
+ backup_restore(system=None, name=backup_list()["archives"][0],
+ apps=["wordpress"])
assert _is_installed("wordpress")
-@pytest.mark.with_backup_legacy_app_installed
-def test_backup_and_restore_legacy_app():
+@pytest.mark.with_legacy_app_installed
+def test_backup_and_restore_legacy_app(mocker):
- _test_backup_and_restore_app("backup_legacy_app")
+ _test_backup_and_restore_app(mocker, "legacy_app")
@pytest.mark.with_backup_recommended_app_installed
-def test_backup_and_restore_recommended_app():
+def test_backup_and_restore_recommended_app(mocker):
- _test_backup_and_restore_app("backup_recommended_app")
+ _test_backup_and_restore_app(mocker, "backup_recommended_app")
@pytest.mark.with_backup_recommended_app_installed_with_ynh_restore
-def test_backup_and_restore_with_ynh_restore():
+def test_backup_and_restore_with_ynh_restore(mocker):
- _test_backup_and_restore_app("backup_recommended_app")
+ _test_backup_and_restore_app(mocker, "backup_recommended_app")
+
+@pytest.mark.with_permission_app_installed
+def test_backup_and_restore_permission_app(mocker):
+
+ res = user_permission_list(full=True)['permissions']
+ assert "permissions_app.main" in res
+ assert "permissions_app.admin" in res
+ assert "permissions_app.dev" in res
+ assert res['permissions_app.main']['url'] == "/"
+ assert res['permissions_app.admin']['url'] == "/admin"
+ assert res['permissions_app.dev']['url'] == "/dev"
+
+ assert "visitors" in res['permissions_app.main']['allowed']
+ assert "all_users" in res['permissions_app.main']['allowed']
+ assert res['permissions_app.admin']['allowed'] == ["alice"]
+ assert res['permissions_app.dev']['allowed'] == []
+
+ _test_backup_and_restore_app(mocker, "permissions_app")
+
+ res = user_permission_list(full=True)['permissions']
+ assert "permissions_app.main" in res
+ assert "permissions_app.admin" in res
+ assert "permissions_app.dev" in res
+ assert res['permissions_app.main']['url'] == "/"
+ assert res['permissions_app.admin']['url'] == "/admin"
+ assert res['permissions_app.dev']['url'] == "/dev"
+
+ assert "visitors" in res['permissions_app.main']['allowed']
+ assert "all_users" in res['permissions_app.main']['allowed']
+ assert res['permissions_app.admin']['allowed'] == ["alice"]
+ assert res['permissions_app.dev']['allowed'] == []
-def _test_backup_and_restore_app(app):
+def _test_backup_and_restore_app(mocker, app):
# Create a backup of this app
- backup_create(system=None, apps=[app])
+ with message(mocker, "backup_created"):
+ backup_create(system=None, apps=[app])
archives = backup_list()["archives"]
assert len(archives) == 1
@@ -531,18 +548,18 @@ def _test_backup_and_restore_app(app):
# Uninstall the app
app_remove(app)
assert not app_is_installed(app)
- assert app not in user_permission_list()['permissions']
+ assert app+".main" not in user_permission_list()['permissions']
# Restore the app
- backup_restore(system=None, name=archives[0],
- apps=[app])
+ with message(mocker, "restore_complete"):
+ backup_restore(system=None, name=archives[0],
+ apps=[app])
assert app_is_installed(app)
# Check permission
per_list = user_permission_list()['permissions']
- assert app in per_list
- assert "main" in per_list[app]
+ assert app+".main" in per_list
#
# Some edge cases #
@@ -557,13 +574,24 @@ def test_restore_archive_with_no_json(mocker):
assert "badbackup" in backup_list()["archives"]
- mocker.spy(m18n, "n")
- with pytest.raises(YunohostError):
+ with raiseYunohostError(mocker, 'backup_archive_cant_retrieve_info_json'):
backup_restore(name="badbackup", force=True)
- m18n.n.assert_any_call('backup_invalid_archive')
+
+@pytest.mark.with_wordpress_archive_from_2p4
+def test_restore_archive_with_bad_archive(mocker):
+
+ # Break the archive
+ os.system("head -n 1000 /home/yunohost.backup/archives/backup_wordpress_from_2p4.tar.gz > /home/yunohost.backup/archives/backup_wordpress_from_2p4.tar.gz")
+
+ assert "backup_wordpress_from_2p4" in backup_list()["archives"]
+
+ with raiseYunohostError(mocker, 'backup_archive_open_failed'):
+ backup_restore(name="backup_wordpress_from_2p4", force=True)
+
+ clean_tmp_backup_directory()
-def test_backup_binds_are_readonly(monkeypatch):
+def test_backup_binds_are_readonly(mocker, monkeypatch):
def custom_mount_and_backup(self, backup_manager):
self.manager = backup_manager
@@ -584,4 +612,5 @@ def test_backup_binds_are_readonly(monkeypatch):
custom_mount_and_backup)
# Create the backup
- backup_create(system=[])
+ with message(mocker, "backup_created"):
+ backup_create(system=[])
diff --git a/src/yunohost/tests/test_changeurl.py b/src/yunohost/tests/test_changeurl.py
index cb9b5d290..8888dd9e9 100644
--- a/src/yunohost/tests/test_changeurl.py
+++ b/src/yunohost/tests/test_changeurl.py
@@ -8,11 +8,11 @@ from yunohost.domain import _get_maindomain
from yunohost.utils.error import YunohostError
# Get main domain
-maindomain = _get_maindomain()
-
+maindomain = ""
def setup_function(function):
- pass
+ global maindomain
+ maindomain = _get_maindomain()
def teardown_function(function):
diff --git a/src/yunohost/tests/test_permission.py b/src/yunohost/tests/test_permission.py
index d309a8211..b6b7d9577 100644
--- a/src/yunohost/tests/test_permission.py
+++ b/src/yunohost/tests/test_permission.py
@@ -1,37 +1,63 @@
+import requests
import pytest
-from moulinette.core import MoulinetteError
-from yunohost.app import app_install, app_remove, app_change_url, app_list
-from yunohost.user import user_list, user_create, user_permission_list, user_delete, user_group_list, user_group_delete, user_permission_add, user_permission_remove, user_permission_clear
-from yunohost.permission import permission_add, permission_update, permission_remove
+from conftest import message, raiseYunohostError
+
+from yunohost.app import app_install, app_remove, app_change_url, app_list, app_map, _installed_apps
+from yunohost.user import user_list, user_create, user_delete, \
+ user_group_list, user_group_delete
+from yunohost.permission import user_permission_update, user_permission_list, user_permission_reset, \
+ permission_create, permission_delete, permission_url
from yunohost.domain import _get_maindomain
-from yunohost.utils.error import YunohostError
# Get main domain
-maindomain = _get_maindomain()
+maindomain = ""
+dummy_password = "test123Ynh"
+
+# Dirty patch of DNS resolution. Force the DNS to 127.0.0.1 address even if dnsmasq have the public address.
+# Mainly used for 'can_access_webpage' function
+import socket
+
+prv_getaddrinfo = socket.getaddrinfo
def clean_user_groups_permission():
for u in user_list()['users']:
user_delete(u)
for g in user_group_list()['groups']:
- if g != "all_users":
+ if g not in ["all_users", "visitors"]:
user_group_delete(g)
- for a, per in user_permission_list()['permissions'].items():
- if a in ['wiki', 'blog', 'site']:
- for p in per:
- permission_remove(a, p, force=True, sync_perm=False)
+ for p in user_permission_list()['permissions']:
+ if any(p.startswith(name) for name in ["wiki", "blog", "site", "permissions_app"]):
+ permission_delete(p, force=True, sync_perm=False)
+ socket.getaddrinfo = prv_getaddrinfo
+
def setup_function(function):
clean_user_groups_permission()
- user_create("alice", "Alice", "White", "alice@" + maindomain, "test123Ynh")
- user_create("bob", "Bob", "Snow", "bob@" + maindomain, "test123Ynh")
- permission_add("wiki", "main", [maindomain + "/wiki"], sync_perm=False)
- permission_add("blog", "main", sync_perm=False)
+ global maindomain
+ maindomain = _get_maindomain()
+
+ # Dirty patch of DNS resolution. Force the DNS to 127.0.0.1 address even if dnsmasq have the public address.
+ # Mainly used for 'can_access_webpage' function
+ dns_cache = {(maindomain, 443, 0, 1): [(2, 1, 6, '', ('127.0.0.1', 443))]}
+ def new_getaddrinfo(*args):
+ try:
+ return dns_cache[args]
+ except KeyError:
+ res = prv_getaddrinfo(*args)
+ dns_cache[args] = res
+ return res
+ socket.getaddrinfo = new_getaddrinfo
+
+ user_create("alice", "Alice", "White", "alice@" + maindomain, dummy_password)
+ user_create("bob", "Bob", "Snow", "bob@" + maindomain, dummy_password)
+ permission_create("wiki.main", url="/", allowed=["all_users"] , sync_perm=False)
+ permission_create("blog.main", allowed=["all_users"], sync_perm=False)
+ user_permission_update("blog.main", remove="all_users", add="alice")
- user_permission_add(["blog"], "main", group="alice")
def teardown_function(function):
clean_user_groups_permission()
@@ -39,6 +65,11 @@ def teardown_function(function):
app_remove("permissions_app")
except:
pass
+ try:
+ app_remove("legacy_app")
+ except:
+ pass
+
@pytest.fixture(autouse=True)
def check_LDAP_db_integrity_call():
@@ -46,6 +77,7 @@ def check_LDAP_db_integrity_call():
yield
check_LDAP_db_integrity()
+
def check_LDAP_db_integrity():
# Here we check that all attributes in all object are sychronized.
# Here is the list of attributes per object:
@@ -57,7 +89,7 @@ def check_LDAP_db_integrity():
# One part should be done automatically by the "memberOf" overlay of LDAP.
# The other part is done by the the "permission_sync_to_user" function of the permission module
- from yunohost.utils.ldap import _get_ldap_interface
+ from yunohost.utils.ldap import _get_ldap_interface, _ldap_path_extract
ldap = _get_ldap_interface()
user_search = ldap.search('ou=users,dc=yunohost,dc=org',
@@ -76,161 +108,195 @@ def check_LDAP_db_integrity():
for user in user_search:
user_dn = 'uid=' + user['uid'][0] + ',ou=users,dc=yunohost,dc=org'
- group_list = [m.split("=")[1].split(",")[0] for m in user['memberOf']]
- permission_list = []
- if 'permission' in user:
- permission_list = [m.split("=")[1].split(",")[0] for m in user['permission']]
+ group_list = [_ldap_path_extract(m, "cn") for m in user['memberOf']]
+ permission_list = [_ldap_path_extract(m, "cn") for m in user.get('permission', [])]
+ # This user's DN sould be found in all groups it is a member of
for group in group_list:
assert user_dn in group_map[group]['member']
+
+ # This user's DN should be found in all perms it has access to
for permission in permission_list:
assert user_dn in permission_map[permission]['inheritPermission']
for permission in permission_search:
permission_dn = 'cn=' + permission['cn'][0] + ',ou=permission,dc=yunohost,dc=org'
- user_list = []
- group_list = []
- if 'inheritPermission' in permission:
- user_list = [m.split("=")[1].split(",")[0] for m in permission['inheritPermission']]
- assert set(user_list) == set(permission['memberUid'])
- if 'groupPermission' in permission:
- group_list = [m.split("=")[1].split(",")[0] for m in permission['groupPermission']]
+ # inheritPermission uid's should match memberUids
+ user_list = [_ldap_path_extract(m, "uid") for m in permission.get('inheritPermission', [])]
+ assert set(user_list) == set(permission.get('memberUid', []))
+
+ # This perm's DN should be found on all related users it is related to
for user in user_list:
assert permission_dn in user_map[user]['permission']
+
+ # Same for groups : we should find the permission's DN for all related groups
+ group_list = [_ldap_path_extract(m, "cn") for m in permission.get('groupPermission', [])]
for group in group_list:
assert permission_dn in group_map[group]['permission']
- if 'member' in group_map[group]:
- user_list_in_group = [m.split("=")[1].split(",")[0] for m in group_map[group]['member']]
- assert set(user_list_in_group) <= set(user_list)
+
+ # The list of user in the group should be a subset of all users related to the current permission
+ users_in_group = [_ldap_path_extract(m, "uid") for m in group_map[group].get("member", [])]
+ assert set(users_in_group) <= set(user_list)
for group in group_search:
group_dn = 'cn=' + group['cn'][0] + ',ou=groups,dc=yunohost,dc=org'
- user_list = []
- permission_list = []
- if 'member' in group:
- user_list = [m.split("=")[1].split(",")[0] for m in group['member']]
- if group['cn'][0] in user_list:
- # If it's the main group of the user it's normal that it is not in the memberUid
- g_list = list(user_list)
- g_list.remove(group['cn'][0])
- if 'memberUid' in group:
- assert set(g_list) == set(group['memberUid'])
- else:
- assert g_list == []
- else:
- assert set(user_list) == set(group['memberUid'])
- if 'permission' in group:
- permission_list = [m.split("=")[1].split(",")[0] for m in group['permission']]
+ user_list = [_ldap_path_extract(m, "uid") for m in group.get("member", [])]
+ # For primary groups, we should find that :
+ # - len(user_list) is 1 (a primary group has only 1 member)
+ # - the group name should be an existing yunohost user
+ # - memberUid is empty (meaning no other member than the corresponding user)
+ if group['cn'][0] in user_list:
+ assert len(user_list) == 1
+ assert group["cn"][0] in user_map
+ assert group.get('memberUid', []) == []
+ # Otherwise, user_list and memberUid should have the same content
+ else:
+ assert set(user_list) == set(group.get('memberUid', []))
+
+ # For all users members, this group should be in the "memberOf" on the other side
for user in user_list:
assert group_dn in user_map[user]['memberOf']
+
+ # For all the permissions of this group, the group should be among the "groupPermission" on the other side
+ permission_list = [_ldap_path_extract(m, "cn") for m in group.get('permission', [])]
for permission in permission_list:
assert group_dn in permission_map[permission]['groupPermission']
- if 'inheritPermission' in permission_map:
- allowed_user_list = [m.split("=")[1].split(",")[0] for m in permission_map[permission]['inheritPermission']]
- assert set(user_list) <= set(allowed_user_list)
+
+ # And the list of user of this group (user_list) should be a subset of all allowed users for this perm...
+ allowed_user_list = [_ldap_path_extract(m, "uid") for m in permission_map[permission].get('inheritPermission', [])]
+ assert set(user_list) <= set(allowed_user_list)
def check_permission_for_apps():
# We check that the for each installed apps we have at last the "main" permission
# and we don't have any permission linked to no apps. The only exception who is not liked to an app
- # is mail, metronome, and sftp
+ # is mail, xmpp, and sftp
- from yunohost.utils.ldap import _get_ldap_interface
- ldap = _get_ldap_interface()
- permission_search = ldap.search('ou=permission,dc=yunohost,dc=org',
- '(objectclass=permissionYnh)',
- ['cn', 'groupPermission', 'inheritPermission', 'memberUid'])
+ app_perms = user_permission_list(ignore_system_perms=True)["permissions"].keys()
- installed_apps = {app['id'] for app in app_list(installed=True)['apps']}
- permission_list_set = {permission['cn'][0].split(".")[1] for permission in permission_search}
+ # Keep only the prefix so that
+ # ["foo.main", "foo.pwet", "bar.main"]
+ # becomes
+ # {"bar", "foo"}
+ # and compare this to the list of installed apps ...
+
+ app_perms_prefix = set(p.split(".")[0] for p in app_perms)
+
+ assert set(_installed_apps()) == app_perms_prefix
+
+
+def can_access_webpage(webpath, logged_as=None):
+
+ webpath = webpath.rstrip("/")
+ sso_url = "https://" + maindomain + "/yunohost/sso/"
+
+ # Anonymous access
+ if not logged_as:
+ r = requests.get(webpath, verify=False)
+ # Login as a user using dummy password
+ else:
+ with requests.Session() as session:
+ session.post(sso_url,
+ data={"user": logged_as,
+ "password": dummy_password},
+ headers={"Referer": sso_url,
+ "Content-Type": "application/x-www-form-urlencoded"},
+ verify=False)
+ # We should have some cookies related to authentication now
+ assert session.cookies
+ r = session.get(webpath, verify=False)
+
+ # If we can't access it, we got redirected to the SSO
+ return not r.url.startswith(sso_url)
- extra_service_permission = set(['mail', 'metronome'])
- if 'sftp' in permission_list_set:
- extra_service_permission.add('sftp')
- assert installed_apps == permission_list_set - extra_service_permission
#
# List functions
#
-def test_list_permission():
- res = user_permission_list()['permissions']
+def test_permission_list():
+ res = user_permission_list(full=True)['permissions']
- assert "wiki" in res
- assert "main" in res['wiki']
- assert "blog" in res
- assert "main" in res['blog']
- assert "mail" in res
- assert "main" in res['mail']
- assert "metronome" in res
- assert "main" in res['metronome']
- assert ["all_users"] == res['wiki']['main']['allowed_groups']
- assert ["alice"] == res['blog']['main']['allowed_groups']
- assert set(["alice", "bob"]) == set(res['wiki']['main']['allowed_users'])
- assert ["alice"] == res['blog']['main']['allowed_users']
- assert [maindomain + "/wiki"] == res['wiki']['main']['URL']
+ assert "wiki.main" in res
+ assert "blog.main" in res
+ assert "mail.main" in res
+ assert "xmpp.main" in res
+ assert res['wiki.main']['allowed'] == ["all_users"]
+ assert res['blog.main']['allowed'] == ["alice"]
+ assert set(res['wiki.main']['corresponding_users']) == set(["alice", "bob"])
+ assert res['blog.main']['corresponding_users'] == ["alice"]
+ assert res['wiki.main']['url'] == "/"
#
# Create - Remove functions
#
-def test_add_permission_1():
- permission_add("site", "test")
+
+def test_permission_create_main(mocker):
+ with message(mocker, "permission_created", permission="site.main"):
+ permission_create("site.main", allowed=["all_users"])
+
+ res = user_permission_list(full=True)['permissions']
+ assert "site.main" in res
+ assert res['site.main']['allowed'] == ["all_users"]
+ assert set(res['site.main']['corresponding_users']) == set(["alice", "bob"])
+
+
+def test_permission_create_extra(mocker):
+ with message(mocker, "permission_created", permission="site.test"):
+ permission_create("site.test")
+
+ res = user_permission_list(full=True)['permissions']
+ assert "site.test" in res
+ # all_users is only enabled by default on .main perms
+ assert "all_users" not in res['site.test']['allowed']
+ assert res['site.test']['corresponding_users'] == []
+
+
+def test_permission_create_with_specific_user():
+ permission_create("site.test", allowed=["alice"])
+
+ res = user_permission_list(full=True)['permissions']
+ assert "site.test" in res
+ assert res['site.test']['allowed'] == ["alice"]
+
+
+def test_permission_delete(mocker):
+ with message(mocker, "permission_deleted", permission="wiki.main"):
+ permission_delete("wiki.main", force=True)
res = user_permission_list()['permissions']
- assert "site" in res
- assert "test" in res['site']
- assert "all_users" in res['site']['test']['allowed_groups']
- assert set(["alice", "bob"]) == set(res['site']['test']['allowed_users'])
-
-def test_add_permission_2():
- permission_add("site", "main", default_allow=False)
-
- res = user_permission_list()['permissions']
- assert "site" in res
- assert "main" in res['site']
- assert [] == res['site']['main']['allowed_groups']
- assert [] == res['site']['main']['allowed_users']
-
-def test_remove_permission():
- permission_remove("wiki", "main", force=True)
-
- res = user_permission_list()['permissions']
- assert "wiki" not in res
+ assert "wiki.main" not in res
#
# Error on create - remove function
#
-def test_add_bad_permission():
- # Create permission with same name
- with pytest.raises(YunohostError):
- permission_add("wiki", "main")
-def test_remove_bad_permission():
- # Remove not existant permission
- with pytest.raises(MoulinetteError):
- permission_remove("non_exit", "main", force=True)
+def test_permission_create_already_existing(mocker):
+ with raiseYunohostError(mocker, "permission_already_exist"):
+ permission_create("wiki.main")
+
+
+def test_permission_delete_doesnt_existing(mocker):
+ with raiseYunohostError(mocker, "permission_not_found"):
+ permission_delete("doesnt.exist", force=True)
res = user_permission_list()['permissions']
- assert "wiki" in res
- assert "main" in res['wiki']
- assert "blog" in res
- assert "main" in res['blog']
- assert "mail" in res
- assert "main" in res ['mail']
- assert "metronome" in res
- assert "main" in res['metronome']
+ assert "wiki.main" in res
+ assert "blog.main" in res
+ assert "mail.main" in res
+ assert "xmpp.main" in res
-def test_remove_main_permission():
- with pytest.raises(YunohostError):
- permission_remove("blog", "main")
+
+def test_permission_delete_main_without_force(mocker):
+ with raiseYunohostError(mocker, "permission_cannot_remove_main"):
+ permission_delete("blog.main")
res = user_permission_list()['permissions']
- assert "mail" in res
- assert "main" in res['mail']
+ assert "blog.main" in res
#
# Update functions
@@ -238,182 +304,224 @@ def test_remove_main_permission():
# user side functions
-def test_allow_first_group():
- # Remove permission to all_users and define per users
- user_permission_add(["wiki"], "main", group="alice")
- res = user_permission_list()['permissions']
- assert ['alice'] == res['wiki']['main']['allowed_users']
- assert ['alice'] == res['wiki']['main']['allowed_groups']
+def test_permission_add_group(mocker):
+ with message(mocker, "permission_updated", permission="wiki.main"):
+ user_permission_update("wiki.main", add="alice")
-def test_allow_other_group():
- # Allow new user in a permission
- user_permission_add(["blog"], "main", group="bob")
+ res = user_permission_list(full=True)['permissions']
+ assert set(res['wiki.main']['allowed']) == set(["all_users", "alice"])
+ assert set(res['wiki.main']['corresponding_users']) == set(["alice", "bob"])
- res = user_permission_list()['permissions']
- assert set(["alice", "bob"]) == set(res['blog']['main']['allowed_users'])
- assert set(["alice", "bob"]) == set(res['blog']['main']['allowed_groups'])
-def test_disallow_group_1():
- # Disallow a user in a permission
- user_permission_remove(["blog"], "main", group="alice")
+def test_permission_remove_group(mocker):
+ with message(mocker, "permission_updated", permission="blog.main"):
+ user_permission_update("blog.main", remove="alice")
- res = user_permission_list()['permissions']
- assert [] == res['blog']['main']['allowed_users']
- assert [] == res['blog']['main']['allowed_groups']
+ res = user_permission_list(full=True)['permissions']
+ assert res['blog.main']['allowed'] == []
+ assert res['blog.main']['corresponding_users'] == []
-def test_allow_group_1():
- # Allow a user when he is already allowed
- user_permission_add(["blog"], "main", group="alice")
- res = user_permission_list()['permissions']
- assert ["alice"] == res['blog']['main']['allowed_users']
- assert ["alice"] == res['blog']['main']['allowed_groups']
+def test_permission_add_and_remove_group(mocker):
+ with message(mocker, "permission_updated", permission="wiki.main"):
+ user_permission_update("wiki.main", add="alice", remove="all_users")
-def test_disallow_group_1():
- # Disallow a user when he is already disallowed
- user_permission_remove(["blog"], "main", group="bob")
+ res = user_permission_list(full=True)['permissions']
+ assert res['wiki.main']['allowed'] == ["alice"]
+ assert res['wiki.main']['corresponding_users'] == ["alice"]
- res = user_permission_list()['permissions']
- assert ["alice"] == res['blog']['main']['allowed_users']
- assert ["alice"] == res['blog']['main']['allowed_groups']
-def test_reset_permission():
+def test_permission_add_group_already_allowed(mocker):
+ with message(mocker, "permission_already_allowed", permission="blog.main", group="alice"):
+ user_permission_update("blog.main", add="alice")
+
+ res = user_permission_list(full=True)['permissions']
+ assert res['blog.main']['allowed'] == ["alice"]
+ assert res['blog.main']['corresponding_users'] == ["alice"]
+
+
+def test_permission_remove_group_already_not_allowed(mocker):
+ with message(mocker, "permission_already_disallowed", permission="blog.main", group="bob"):
+ user_permission_update("blog.main", remove="bob")
+
+ res = user_permission_list(full=True)['permissions']
+ assert res['blog.main']['allowed'] == ["alice"]
+ assert res['blog.main']['corresponding_users'] == ["alice"]
+
+
+def test_permission_reset(mocker):
+ with message(mocker, "permission_updated", permission="blog.main"):
+ user_permission_reset("blog.main")
+
+ res = user_permission_list(full=True)['permissions']
+ assert res['blog.main']['allowed'] == ["all_users"]
+ assert set(res['blog.main']['corresponding_users']) == set(["alice", "bob"])
+
+
+def test_permission_reset_idempotency():
# Reset permission
- user_permission_clear(["blog"], "main")
+ user_permission_reset("blog.main")
+ user_permission_reset("blog.main")
- res = user_permission_list()['permissions']
- assert set(["alice", "bob"]) == set(res['blog']['main']['allowed_users'])
- assert ["all_users"] == res['blog']['main']['allowed_groups']
+ res = user_permission_list(full=True)['permissions']
+ assert res['blog.main']['allowed'] == ["all_users"]
+ assert set(res['blog.main']['corresponding_users']) == set(["alice", "bob"])
-# internal functions
-def test_add_url_1():
- # Add URL in permission which hasn't any URL defined
- permission_update("blog", "main", add_url=[maindomain + "/testA"])
+def test_permission_reset_idempotency():
+ # Reset permission
+ user_permission_reset("blog.main")
+ user_permission_reset("blog.main")
- res = user_permission_list()['permissions']
- assert [maindomain + "/testA"] == res['blog']['main']['URL']
+ res = user_permission_list(full=True)['permissions']
+ assert res['blog.main']['allowed'] == ["all_users"]
+ assert set(res['blog.main']['corresponding_users']) == set(["alice", "bob"])
-def test_add_url_2():
- # Add a second URL in a permission
- permission_update("wiki", "main", add_url=[maindomain + "/testA"])
-
- res = user_permission_list()['permissions']
- assert set([maindomain + "/testA", maindomain + "/wiki"]) == set(res['wiki']['main']['URL'])
-
-def test_remove_url_1():
- permission_update("wiki", "main", remove_url=[maindomain + "/wiki"])
-
- res = user_permission_list()['permissions']
- assert 'URL' not in res['wiki']['main']
-
-def test_add_url_3():
- # Add a url already added
- permission_update("wiki", "main", add_url=[maindomain + "/wiki"])
-
- res = user_permission_list()['permissions']
- assert [maindomain + "/wiki"] == res['wiki']['main']['URL']
-
-def test_remove_url_2():
- # Remove a url not added (with a permission which contain some URL)
- permission_update("wiki", "main", remove_url=[maindomain + "/not_exist"])
-
- res = user_permission_list()['permissions']
- assert [maindomain + "/wiki"] == res['wiki']['main']['URL']
-
-def test_remove_url_2():
- # Remove a url not added (with a permission which contain no URL)
- permission_update("blog", "main", remove_url=[maindomain + "/not_exist"])
-
- res = user_permission_list()['permissions']
- assert 'URL' not in res['blog']['main']
#
# Error on update function
#
-def test_disallow_bad_group_1():
- # Disallow a group when the group all_users is allowed
- with pytest.raises(YunohostError):
- user_permission_remove("wiki", "main", group="alice")
- res = user_permission_list()['permissions']
- assert ["all_users"] == res['wiki']['main']['allowed_groups']
- assert set(["alice", "bob"]) == set(res['wiki']['main']['allowed_users'])
+def test_permission_add_group_that_doesnt_exist(mocker):
+ with raiseYunohostError(mocker, "group_unknown"):
+ user_permission_update("blog.main", add="doesnt_exist")
-def test_allow_bad_user():
- # Allow a non existant group
- with pytest.raises(YunohostError):
- user_permission_add(["blog"], "main", group="not_exist")
+ res = user_permission_list(full=True)['permissions']
+ assert res['blog.main']['allowed'] == ["alice"]
+ assert res['blog.main']['corresponding_users'] == ["alice"]
- res = user_permission_list()['permissions']
- assert ["alice"] == res['blog']['main']['allowed_groups']
- assert ["alice"] == res['blog']['main']['allowed_users']
-def test_disallow_bad_group_2():
- # Disallow a non existant group
- with pytest.raises(YunohostError):
- user_permission_remove(["blog"], "main", group="not_exist")
+def test_permission_update_permission_that_doesnt_exist(mocker):
+ with raiseYunohostError(mocker, "permission_not_found"):
+ user_permission_update("doesnt.exist", add="alice")
- res = user_permission_list()['permissions']
- assert ["alice"] == res['blog']['main']['allowed_groups']
- assert ["alice"] == res['blog']['main']['allowed_users']
-def test_allow_bad_permission_1():
- # Allow a user to a non existant permission
- with pytest.raises(YunohostError):
- user_permission_add(["wiki"], "not_exit", group="alice")
+# Permission url management
-def test_allow_bad_permission_2():
- # Allow a user to a non existant permission
- with pytest.raises(YunohostError):
- user_permission_add(["not_exit"], "main", group="alice")
+def test_permission_redefine_url():
+ permission_url("blog.main", url="/pwet")
+
+ res = user_permission_list(full=True)['permissions']
+ assert res["blog.main"]["url"] == "/pwet"
+
+def test_permission_remove_url():
+ permission_url("blog.main", url=None)
+
+ res = user_permission_list(full=True)['permissions']
+ assert res["blog.main"]["url"] is None
#
# Application interaction
#
-def test_install_app():
+
+def test_permission_app_install():
app_install("./tests/apps/permissions_app_ynh",
- args="domain=%s&path=%s&admin=%s" % (maindomain, "/urlpermissionapp", "alice"), force=True)
+ args="domain=%s&path=%s&is_public=0&admin=%s" % (maindomain, "/urlpermissionapp", "alice"), force=True)
- res = user_permission_list()['permissions']
- assert "permissions_app" in res
- assert "main" in res['permissions_app']
- assert [maindomain + "/urlpermissionapp"] == res['permissions_app']['main']['URL']
- assert [maindomain + "/urlpermissionapp/admin"] == res['permissions_app']['admin']['URL']
- assert [maindomain + "/urlpermissionapp/dev"] == res['permissions_app']['dev']['URL']
+ res = user_permission_list(full=True)['permissions']
+ assert "permissions_app.main" in res
+ assert "permissions_app.admin" in res
+ assert "permissions_app.dev" in res
+ assert res['permissions_app.main']['url'] == "/"
+ assert res['permissions_app.admin']['url'] == "/admin"
+ assert res['permissions_app.dev']['url'] == "/dev"
- assert ["all_users"] == res['permissions_app']['main']['allowed_groups']
- assert set(["alice", "bob"]) == set(res['permissions_app']['main']['allowed_users'])
+ assert res['permissions_app.main']['allowed'] == ["all_users"]
+ assert set(res['permissions_app.main']['corresponding_users']) == set(["alice", "bob"])
- assert ["alice"] == res['permissions_app']['admin']['allowed_groups']
- assert ["alice"] == res['permissions_app']['admin']['allowed_users']
+ assert res['permissions_app.admin']['allowed'] == ["alice"]
+ assert res['permissions_app.admin']['corresponding_users'] == ["alice"]
- assert ["all_users"] == res['permissions_app']['dev']['allowed_groups']
- assert set(["alice", "bob"]) == set(res['permissions_app']['dev']['allowed_users'])
+ assert res['permissions_app.dev']['allowed'] == []
+ assert set(res['permissions_app.dev']['corresponding_users']) == set()
-def test_remove_app():
+ # Check that we get the right stuff in app_map, which is used to generate the ssowatconf
+ assert maindomain + "/urlpermissionapp" in app_map(user="alice").keys()
+ user_permission_update("permissions_app.main", remove="all_users", add="bob")
+ assert maindomain + "/urlpermissionapp" not in app_map(user="alice").keys()
+ assert maindomain + "/urlpermissionapp" in app_map(user="bob").keys()
+
+
+def test_permission_app_remove():
app_install("./tests/apps/permissions_app_ynh",
- args="domain=%s&path=%s&admin=%s" % (maindomain, "/urlpermissionapp", "alice"), force=True)
+ args="domain=%s&path=%s&is_public=0&admin=%s" % (maindomain, "/urlpermissionapp", "alice"), force=True)
app_remove("permissions_app")
- res = user_permission_list()['permissions']
- assert "permissions_app" not in res
+ # Check all permissions for this app got deleted
+ res = user_permission_list(full=True)['permissions']
+ assert not any(p.startswith("permissions_app.") for p in res.keys())
-def test_change_url():
+
+def test_permission_app_change_url():
app_install("./tests/apps/permissions_app_ynh",
args="domain=%s&path=%s&admin=%s" % (maindomain, "/urlpermissionapp", "alice"), force=True)
- res = user_permission_list()['permissions']
- assert [maindomain + "/urlpermissionapp"] == res['permissions_app']['main']['URL']
- assert [maindomain + "/urlpermissionapp/admin"] == res['permissions_app']['admin']['URL']
- assert [maindomain + "/urlpermissionapp/dev"] == res['permissions_app']['dev']['URL']
+ # FIXME : should rework this test to look for differences in the generated app map / app tiles ...
+ res = user_permission_list(full=True)['permissions']
+ assert res['permissions_app.main']['url'] == "/"
+ assert res['permissions_app.admin']['url'] == "/admin"
+ assert res['permissions_app.dev']['url'] == "/dev"
app_change_url("permissions_app", maindomain, "/newchangeurl")
- res = user_permission_list()['permissions']
- assert [maindomain + "/newchangeurl"] == res['permissions_app']['main']['URL']
- assert [maindomain + "/newchangeurl/admin"] == res['permissions_app']['admin']['URL']
- assert [maindomain + "/newchangeurl/dev"] == res['permissions_app']['dev']['URL']
+ res = user_permission_list(full=True)['permissions']
+ assert res['permissions_app.main']['url'] == "/"
+ assert res['permissions_app.admin']['url'] == "/admin"
+ assert res['permissions_app.dev']['url'] == "/dev"
+
+
+def test_permission_app_propagation_on_ssowat():
+
+ app_install("./tests/apps/permissions_app_ynh",
+ args="domain=%s&path=%s&is_public=1&admin=%s" % (maindomain, "/urlpermissionapp", "alice"), force=True)
+
+ res = user_permission_list(full=True)['permissions']
+ assert "visitors" in res['permissions_app.main']['allowed']
+ assert "all_users" in res['permissions_app.main']['allowed']
+
+ app_webroot = "https://%s/urlpermissionapp" % maindomain
+ assert can_access_webpage(app_webroot, logged_as=None)
+ assert can_access_webpage(app_webroot, logged_as="alice")
+
+ user_permission_update("permissions_app.main", remove=["visitors", "all_users"], add="bob")
+ res = user_permission_list(full=True)['permissions']
+
+ assert not can_access_webpage(app_webroot, logged_as=None)
+ assert not can_access_webpage(app_webroot, logged_as="alice")
+ assert can_access_webpage(app_webroot, logged_as="bob")
+
+ # Test admin access, as configured during install, only alice should be able to access it
+
+ # alice gotta be allowed on the main permission to access the admin tho
+ user_permission_update("permissions_app.main", remove="bob", add="all_users")
+
+ assert not can_access_webpage(app_webroot+"/admin", logged_as=None)
+ assert can_access_webpage(app_webroot+"/admin", logged_as="alice")
+ assert not can_access_webpage(app_webroot+"/admin", logged_as="bob")
+
+
+def test_permission_legacy_app_propagation_on_ssowat():
+
+ app_install("./tests/apps/legacy_app_ynh",
+ args="domain=%s&path=%s" % (maindomain, "/legacy"), force=True)
+
+ # App is configured as public by default using the legacy unprotected_uri mechanics
+ # It should automatically be migrated during the install
+ res = user_permission_list(full=True)['permissions']
+ assert "visitors" in res['legacy_app.main']['allowed']
+ assert "all_users" in res['legacy_app.main']['allowed']
+
+ app_webroot = "https://%s/legacy" % maindomain
+
+ assert can_access_webpage(app_webroot, logged_as=None)
+ assert can_access_webpage(app_webroot, logged_as="alice")
+
+ # Try to update the permission and check that permissions are still consistent
+ user_permission_update("legacy_app.main", remove=["visitors", "all_users"], add="bob")
+
+ assert not can_access_webpage(app_webroot, logged_as=None)
+ assert not can_access_webpage(app_webroot, logged_as="alice")
+ assert can_access_webpage(app_webroot, logged_as="bob")
diff --git a/src/yunohost/tests/test_regenconf.py b/src/yunohost/tests/test_regenconf.py
new file mode 100644
index 000000000..357f96c88
--- /dev/null
+++ b/src/yunohost/tests/test_regenconf.py
@@ -0,0 +1,80 @@
+import glob
+import os
+import pytest
+import shutil
+import requests
+
+from conftest import message, raiseYunohostError
+
+from moulinette import m18n
+from moulinette.utils.filesystem import mkdir
+
+from yunohost.domain import _get_maindomain, domain_add, domain_remove, domain_list
+from yunohost.utils.error import YunohostError
+from yunohost.regenconf import manually_modified_files, _get_conf_hashes, _force_clear_hashes
+
+TEST_DOMAIN = "secondarydomain.test"
+TEST_DOMAIN_NGINX_CONFIG = "/etc/nginx/conf.d/secondarydomain.test.conf"
+
+def setup_function(function):
+
+ _force_clear_hashes([TEST_DOMAIN_NGINX_CONFIG])
+ clean()
+
+def teardown_function(function):
+
+ clean()
+ _force_clear_hashes([TEST_DOMAIN_NGINX_CONFIG])
+
+def clean():
+
+ assert os.system("pgrep slapd >/dev/null") == 0
+ assert os.system("pgrep nginx >/dev/null") == 0
+
+ if TEST_DOMAIN in domain_list()["domains"]:
+ domain_remove(TEST_DOMAIN)
+ assert not os.path.exists(TEST_DOMAIN_NGINX_CONFIG)
+
+ os.system("rm -f %s" % TEST_DOMAIN_NGINX_CONFIG)
+
+ assert os.system("nginx -t 2>/dev/null") == 0
+
+ assert not os.path.exists(TEST_DOMAIN_NGINX_CONFIG)
+ assert TEST_DOMAIN_NGINX_CONFIG not in _get_conf_hashes("nginx")
+ assert TEST_DOMAIN_NGINX_CONFIG not in manually_modified_files()
+
+
+def test_add_domain():
+
+ domain_add(TEST_DOMAIN)
+
+ assert TEST_DOMAIN in domain_list()["domains"]
+
+ assert os.path.exists(TEST_DOMAIN_NGINX_CONFIG)
+
+ assert TEST_DOMAIN_NGINX_CONFIG in _get_conf_hashes("nginx")
+ assert TEST_DOMAIN_NGINX_CONFIG not in manually_modified_files()
+
+
+def test_add_and_edit_domain_conf():
+
+ domain_add(TEST_DOMAIN)
+
+ assert os.path.exists(TEST_DOMAIN_NGINX_CONFIG)
+ assert TEST_DOMAIN_NGINX_CONFIG in _get_conf_hashes("nginx")
+ assert TEST_DOMAIN_NGINX_CONFIG not in manually_modified_files()
+
+ os.system("echo ' ' >> %s" % TEST_DOMAIN_NGINX_CONFIG)
+
+ assert TEST_DOMAIN_NGINX_CONFIG in manually_modified_files()
+
+
+def test_add_domain_conf_already_exists():
+
+ os.system("echo ' ' >> %s" % TEST_DOMAIN_NGINX_CONFIG)
+
+ domain_add(TEST_DOMAIN)
+
+ assert os.path.exists(TEST_DOMAIN_NGINX_CONFIG)
+ assert TEST_DOMAIN_NGINX_CONFIG in _get_conf_hashes("nginx")
+ assert TEST_DOMAIN_NGINX_CONFIG not in manually_modified_files()
diff --git a/src/yunohost/tests/test_settings.py b/src/yunohost/tests/test_settings.py
index 0da12597f..ea6c6922c 100644
--- a/src/yunohost/tests/test_settings.py
+++ b/src/yunohost/tests/test_settings.py
@@ -62,6 +62,8 @@ def test_settings_set():
settings_set("example.bool", False)
assert settings_get("example.bool") == False
+ settings_set("example.bool", "on")
+ assert settings_get("example.bool") == True
def test_settings_set_int():
settings_set("example.int", 21)
diff --git a/src/yunohost/tests/test_user-group.py b/src/yunohost/tests/test_user-group.py
index 3973f0c7d..f1eae9c4e 100644
--- a/src/yunohost/tests/test_user-group.py
+++ b/src/yunohost/tests/test_user-group.py
@@ -1,37 +1,45 @@
import pytest
-from moulinette.core import MoulinetteError
-from yunohost.user import user_list, user_info, user_group_list, user_create, user_delete, user_update, user_group_add, user_group_delete, user_group_update, user_group_info
+from conftest import message, raiseYunohostError
+
+from yunohost.user import user_list, user_info, user_create, user_delete, user_update, \
+ user_group_list, user_group_create, user_group_delete, user_group_update
from yunohost.domain import _get_maindomain
-from yunohost.utils.error import YunohostError
from yunohost.tests.test_permission import check_LDAP_db_integrity
# Get main domain
-maindomain = _get_maindomain()
+maindomain = ""
+
def clean_user_groups():
for u in user_list()['users']:
user_delete(u)
for g in user_group_list()['groups']:
- if g != "all_users":
+ if g not in ["all_users", "visitors"]:
user_group_delete(g)
+
def setup_function(function):
clean_user_groups()
+ global maindomain
+ maindomain = _get_maindomain()
+
user_create("alice", "Alice", "White", "alice@" + maindomain, "test123Ynh")
user_create("bob", "Bob", "Snow", "bob@" + maindomain, "test123Ynh")
user_create("jack", "Jack", "Black", "jack@" + maindomain, "test123Ynh")
- user_group_add("dev")
- user_group_add("apps")
- user_group_update("dev", add_user=["alice"])
- user_group_update("apps", add_user=["bob"])
+ user_group_create("dev")
+ user_group_create("apps")
+ user_group_update("dev", add=["alice"])
+ user_group_update("apps", add=["bob"])
+
def teardown_function(function):
clean_user_groups()
+
@pytest.fixture(autouse=True)
def check_LDAP_db_integrity_call():
check_LDAP_db_integrity()
@@ -42,6 +50,7 @@ def check_LDAP_db_integrity_call():
# List functions
#
+
def test_list_users():
res = user_list()['users']
@@ -49,6 +58,7 @@ def test_list_users():
assert "bob" in res
assert "jack" in res
+
def test_list_groups():
res = user_group_list()['groups']
@@ -65,8 +75,11 @@ def test_list_groups():
# Create - Remove functions
#
-def test_create_user():
- user_create("albert", "Albert", "Good", "alber@" + maindomain, "test123Ynh")
+
+def test_create_user(mocker):
+
+ with message(mocker, "user_created"):
+ user_create("albert", "Albert", "Good", "alber@" + maindomain, "test123Ynh")
group_res = user_group_list()['groups']
assert "albert" in user_list()['users']
@@ -74,23 +87,33 @@ def test_create_user():
assert "albert" in group_res['albert']['members']
assert "albert" in group_res['all_users']['members']
-def test_del_user():
- user_delete("alice")
+
+def test_del_user(mocker):
+
+ with message(mocker, "user_deleted"):
+ user_delete("alice")
group_res = user_group_list()['groups']
assert "alice" not in user_list()
assert "alice" not in group_res
assert "alice" not in group_res['all_users']['members']
-def test_add_group():
- user_group_add("adminsys")
+
+def test_create_group(mocker):
+
+ with message(mocker, "group_created", group="adminsys"):
+ user_group_create("adminsys")
group_res = user_group_list()['groups']
assert "adminsys" in group_res
- assert "members" not in group_res['adminsys']
+ assert "members" in group_res['adminsys'].keys()
+ assert group_res["adminsys"]["members"] == []
-def test_del_group():
- user_group_delete("dev")
+
+def test_del_group(mocker):
+
+ with message(mocker, "group_deleted", group="dev"):
+ user_group_delete("dev")
group_res = user_group_list()['groups']
assert "dev" not in group_res
@@ -99,112 +122,129 @@ def test_del_group():
# Error on create / remove function
#
-def test_add_bad_user_1():
- # Check email already exist
- with pytest.raises(MoulinetteError):
+
+def test_create_user_with_mail_address_already_taken(mocker):
+ with raiseYunohostError(mocker, "user_creation_failed"):
user_create("alice2", "Alice", "White", "alice@" + maindomain, "test123Ynh")
-def test_add_bad_user_2():
- # Check to short password
- with pytest.raises(MoulinetteError):
+
+def test_create_user_with_password_too_simple(mocker):
+ with raiseYunohostError(mocker, "password_listed"):
user_create("other", "Alice", "White", "other@" + maindomain, "12")
-def test_add_bad_user_3():
- # Check user already exist
- with pytest.raises(MoulinetteError):
+
+def test_create_user_already_exists(mocker):
+ with raiseYunohostError(mocker, "user_already_exists"):
user_create("alice", "Alice", "White", "other@" + maindomain, "test123Ynh")
-def test_del_bad_user_1():
- # Check user not found
- with pytest.raises(MoulinetteError):
- user_delete("not_exit")
-def test_add_bad_group_1():
+def test_update_user_with_mail_address_already_taken(mocker):
+ with raiseYunohostError(mocker, "user_update_failed"):
+ user_update("bob", add_mailalias="alice@" + maindomain)
+
+
+def test_del_user_that_does_not_exist(mocker):
+ with raiseYunohostError(mocker, "user_unknown"):
+ user_delete("doesnt_exist")
+
+
+def test_create_group_all_users(mocker):
# Check groups already exist with special group "all_users"
- with pytest.raises(YunohostError):
- user_group_add("all_users")
+ with raiseYunohostError(mocker, "group_already_exist"):
+ user_group_create("all_users")
-def test_add_bad_group_2():
- # Check groups already exist (for standard groups)
- with pytest.raises(MoulinetteError):
- user_group_add("dev")
-def test_del_bad_group_1():
- # Check not allowed to remove this groups
- with pytest.raises(YunohostError):
+def test_create_group_already_exists(mocker):
+ # Check groups already exist (regular groups)
+ with raiseYunohostError(mocker, "group_already_exist"):
+ user_group_create("dev")
+
+
+def test_del_group_all_users(mocker):
+ with raiseYunohostError(mocker, "group_cannot_be_deleted"):
user_group_delete("all_users")
-def test_del_bad_group_2():
- # Check groups not found
- with pytest.raises(MoulinetteError):
- user_group_delete("not_exit")
+
+def test_del_group_that_does_not_exist(mocker):
+ with raiseYunohostError(mocker, "group_unknown"):
+ user_group_delete("doesnt_exist")
#
# Update function
#
-def test_update_user_1():
- user_update("alice", firstname="NewName", lastname="NewLast")
+
+def test_update_user(mocker):
+ with message(mocker, "user_updated"):
+ user_update("alice", firstname="NewName", lastname="NewLast")
info = user_info("alice")
- assert "NewName" == info['firstname']
- assert "NewLast" == info['lastname']
+ assert info['firstname'] == "NewName"
+ assert info['lastname'] == "NewLast"
-def test_update_group_1():
- user_group_update("dev", add_user=["bob"])
+
+def test_update_group_add_user(mocker):
+ with message(mocker, "group_updated", group="dev"):
+ user_group_update("dev", add=["bob"])
group_res = user_group_list()['groups']
- assert set(["alice", "bob"]) == set(group_res['dev']['members'])
+ assert set(group_res['dev']['members']) == set(["alice", "bob"])
-def test_update_group_2():
- # Try to add a user in a group when the user is already in
- user_group_update("apps", add_user=["bob"])
+
+def test_update_group_add_user_already_in(mocker):
+ with message(mocker, "group_user_already_in_group", user="bob", group="apps"):
+ user_group_update("apps", add=["bob"])
group_res = user_group_list()['groups']
- assert ["bob"] == group_res['apps']['members']
+ assert group_res['apps']['members'] == ["bob"]
-def test_update_group_3():
- # Try to remove a user in a group
- user_group_update("apps", remove_user=["bob"])
+
+def test_update_group_remove_user(mocker):
+ with message(mocker, "group_updated", group="apps"):
+ user_group_update("apps", remove=["bob"])
group_res = user_group_list()['groups']
- assert "members" not in group_res['apps']
+ assert group_res['apps']['members'] == []
-def test_update_group_4():
- # Try to remove a user in a group when it is not already in
- user_group_update("apps", remove_user=["jack"])
+
+def test_update_group_remove_user_not_already_in(mocker):
+ with message(mocker, "group_user_not_in_group", user="jack", group="apps"):
+ user_group_update("apps", remove=["jack"])
group_res = user_group_list()['groups']
- assert ["bob"] == group_res['apps']['members']
+ assert group_res['apps']['members'] == ["bob"]
#
# Error on update functions
#
-def test_bad_update_user_1():
- # Check user not found
- with pytest.raises(YunohostError):
- user_update("not_exit", firstname="NewName", lastname="NewLast")
+
+def test_update_user_that_doesnt_exist(mocker):
+ with raiseYunohostError(mocker, "user_unknown"):
+ user_update("doesnt_exist", firstname="NewName", lastname="NewLast")
-def bad_update_group_1():
- # Check groups not found
- with pytest.raises(YunohostError):
- user_group_update("not_exit", add_user=["alice"])
+def test_update_group_that_doesnt_exist(mocker):
+ with raiseYunohostError(mocker, "group_unknown"):
+ user_group_update("doesnt_exist", add=["alice"])
-def test_bad_update_group_2():
- # Check remove user in groups "all_users" not allowed
- with pytest.raises(YunohostError):
- user_group_update("all_users", remove_user=["alice"])
-def test_bad_update_group_3():
- # Check remove user in it own group not allowed
- with pytest.raises(YunohostError):
- user_group_update("alice", remove_user=["alice"])
+def test_update_group_all_users_manually(mocker):
+ with raiseYunohostError(mocker, "group_cannot_edit_all_users"):
+ user_group_update("all_users", remove=["alice"])
-def test_bad_update_group_1():
- # Check add bad user in group
- with pytest.raises(YunohostError):
- user_group_update("dev", add_user=["not_exist"])
+ assert "alice" in user_group_list()["groups"]["all_users"]["members"]
- assert "not_exist" not in user_group_list()["groups"]["dev"]
+
+def test_update_group_primary_manually(mocker):
+ with raiseYunohostError(mocker, "group_cannot_edit_primary_group"):
+ user_group_update("alice", remove=["alice"])
+
+ assert "alice" in user_group_list()["groups"]["alice"]["members"]
+
+
+def test_update_group_add_user_that_doesnt_exist(mocker):
+ with raiseYunohostError(mocker, "user_unknown"):
+ user_group_update("dev", add=["doesnt_exist"])
+
+ assert "doesnt_exist" not in user_group_list()["groups"]["dev"]["members"]
diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py
index 64689fe0c..3208bda60 100644
--- a/src/yunohost/tools.py
+++ b/src/yunohost/tools.py
@@ -30,23 +30,20 @@ import json
import subprocess
import pwd
import socket
-from xmlrpclib import Fault
from importlib import import_module
-from collections import OrderedDict
from moulinette import msignals, m18n
from moulinette.utils.log import getActionLogger
from moulinette.utils.process import check_output, call_async_output
from moulinette.utils.filesystem import read_json, write_to_json, read_yaml, write_to_yaml
-from yunohost.app import app_fetchlist, app_info, app_upgrade, app_ssowatconf, app_list, _install_appslist_fetch_cron
-from yunohost.domain import domain_add, domain_list, _get_maindomain, _set_maindomain
+
+from yunohost.app import _update_apps_catalog, app_info, app_upgrade, app_ssowatconf, app_list, _initialize_apps_catalog_system
+from yunohost.domain import domain_add, domain_list
from yunohost.dyndns import _dyndns_available, _dyndns_provides
from yunohost.firewall import firewall_upnp
-from yunohost.service import service_status, service_start, service_enable
+from yunohost.service import service_start, service_enable
from yunohost.regenconf import regen_conf
-from yunohost.monitor import monitor_disk, monitor_system
-from yunohost.utils.packages import ynh_packages_version, _dump_sources_list, _list_upgradable_apt_packages
-from yunohost.utils.network import get_public_ip
+from yunohost.utils.packages import _dump_sources_list, _list_upgradable_apt_packages, ynh_packages_version
from yunohost.utils.error import YunohostError
from yunohost.log import is_unit_operation, OperationLogger
@@ -56,6 +53,8 @@ MIGRATIONS_STATE_PATH = "/etc/yunohost/migrations.yaml"
logger = getActionLogger('yunohost.tools')
+def tools_versions():
+ return ynh_packages_version()
def tools_ldapinit():
"""
@@ -164,60 +163,10 @@ def tools_adminpw(new_password, check_strength=True):
logger.success(m18n.n('admin_password_changed'))
-@is_unit_operation()
-def tools_maindomain(operation_logger, new_domain=None):
- """
- Check the current main domain, or change it
-
- Keyword argument:
- new_domain -- The new domain to be set as the main domain
-
- """
-
- # If no new domain specified, we return the current main domain
- if not new_domain:
- return {'current_main_domain': _get_maindomain()}
-
- # Check domain exists
- if new_domain not in domain_list()['domains']:
- raise YunohostError('domain_unknown')
-
- operation_logger.related_to.append(('domain', new_domain))
- operation_logger.start()
-
- # Apply changes to ssl certs
- ssl_key = "/etc/ssl/private/yunohost_key.pem"
- ssl_crt = "/etc/ssl/private/yunohost_crt.pem"
- new_ssl_key = "/etc/yunohost/certs/%s/key.pem" % new_domain
- new_ssl_crt = "/etc/yunohost/certs/%s/crt.pem" % new_domain
-
- try:
- if os.path.exists(ssl_key) or os.path.lexists(ssl_key):
- os.remove(ssl_key)
- if os.path.exists(ssl_crt) or os.path.lexists(ssl_crt):
- os.remove(ssl_crt)
-
- os.symlink(new_ssl_key, ssl_key)
- os.symlink(new_ssl_crt, ssl_crt)
-
- _set_maindomain(new_domain)
- except Exception as e:
- logger.warning("%s" % e, exc_info=1)
- raise YunohostError('maindomain_change_failed')
-
- _set_hostname(new_domain)
-
- # Generate SSOwat configuration file
- app_ssowatconf()
-
- # Regen configurations
- try:
- with open('/etc/yunohost/installed', 'r'):
- regen_conf()
- except IOError:
- pass
-
- logger.success(m18n.n('maindomain_changed'))
+def tools_maindomain(new_main_domain=None):
+ from yunohost.domain import domain_main_domain
+ logger.warning(m18n.g("deprecated_command_alias", prog="yunohost", old="tools maindomain", new="domain main-domain"))
+ return domain_main_domain(new_main_domain=new_main_domain)
def _set_hostname(hostname, pretty_hostname=None):
@@ -233,9 +182,9 @@ def _set_hostname(hostname, pretty_hostname=None):
# Then call hostnamectl
commands = [
- "sudo hostnamectl --static set-hostname".split() + [hostname],
- "sudo hostnamectl --transient set-hostname".split() + [hostname],
- "sudo hostnamectl --pretty set-hostname".split() + [pretty_hostname]
+ "hostnamectl --static set-hostname".split() + [hostname],
+ "hostnamectl --transient set-hostname".split() + [hostname],
+ "hostnamectl --pretty set-hostname".split() + [pretty_hostname]
]
for command in commands:
@@ -281,6 +230,7 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False,
"""
from yunohost.utils.password import assert_password_is_strong_enough
+ from yunohost.domain import domain_main_domain
dyndns_provider = "dyndns.yunohost.org"
@@ -350,25 +300,12 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False,
os.system('hostname yunohost.yunohost.org')
# Add a temporary SSOwat rule to redirect SSO to admin page
- try:
- with open('/etc/ssowat/conf.json.persistent') as json_conf:
- ssowat_conf = json.loads(str(json_conf.read()))
- except ValueError as e:
- raise YunohostError('ssowat_persistent_conf_read_error', error=str(e))
- except IOError:
+ if not os.path.exists('/etc/ssowat/conf.json.persistent'):
ssowat_conf = {}
+ else:
+ ssowat_conf = read_json('/etc/ssowat/conf.json.persistent')
- if 'redirected_urls' not in ssowat_conf:
- ssowat_conf['redirected_urls'] = {}
-
- ssowat_conf['redirected_urls']['/'] = domain + '/yunohost/admin'
-
- try:
- with open('/etc/ssowat/conf.json.persistent', 'w+') as f:
- json.dump(ssowat_conf, f, sort_keys=True, indent=4)
- except IOError as e:
- raise YunohostError('ssowat_persistent_conf_write_error', error=str(e))
-
+ write_to_json('/etc/ssowat/conf.json.persistent', ssowat_conf)
os.system('chmod 644 /etc/ssowat/conf.json.persistent')
# Create SSL CA
@@ -381,7 +318,7 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False,
'touch %s/index.txt' % ssl_dir,
'cp %s/openssl.cnf %s/openssl.ca.cnf' % (ssl_dir, ssl_dir),
'sed -i s/yunohost.org/%s/g %s/openssl.ca.cnf ' % (domain, ssl_dir),
- 'openssl req -x509 -new -config %s/openssl.ca.cnf -days 3650 -out %s/ca/cacert.pem -keyout %s/ca/cakey.pem -nodes -batch' % (ssl_dir, ssl_dir, ssl_dir),
+ 'openssl req -x509 -new -config %s/openssl.ca.cnf -days 3650 -out %s/ca/cacert.pem -keyout %s/ca/cakey.pem -nodes -batch -subj /CN=%s/O=%s' % (ssl_dir, ssl_dir, ssl_dir, domain, os.path.splitext(domain)[0]),
'cp %s/ca/cacert.pem /etc/ssl/certs/ca-yunohost_crt.pem' % ssl_dir,
'update-ca-certificates'
]
@@ -403,7 +340,7 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False,
# New domain config
regen_conf(['nsswitch'], force=True)
domain_add(domain, dyndns)
- tools_maindomain(domain)
+ domain_main_domain(domain)
# Change LDAP admin password
tools_adminpw(password, check_strength=not force_password)
@@ -411,15 +348,17 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False,
# Enable UPnP silently and reload firewall
firewall_upnp('enable', no_refresh=True)
- # Setup the default apps list with cron job
+ # Initialize the apps catalog system
+ _initialize_apps_catalog_system()
+
+ # Try to update the apps catalog ...
+ # we don't fail miserably if this fails,
+ # because that could be for example an offline installation...
try:
- app_fetchlist(name="yunohost",
- url="https://app.yunohost.org/apps.json")
+ _update_apps_catalog()
except Exception as e:
logger.warning(str(e))
- _install_appslist_fetch_cron()
-
# Init migrations (skip them, no need to run them on a fresh system)
_skip_all_migrations()
@@ -450,7 +389,7 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False,
logger.success(m18n.n('yunohost_configured'))
- logger.warning(m18n.n('recommend_to_add_first_user'))
+ logger.warning(m18n.n('yunohost_postinstall_end_tip'))
def tools_regen_conf(names=[], with_diff=False, force=False, dry_run=False,
@@ -510,11 +449,10 @@ def tools_update(apps=False, system=False):
upgradable_apps = []
if apps:
- logger.info(m18n.n('updating_app_lists'))
try:
- app_fetchlist()
+ _update_apps_catalog()
except YunohostError as e:
- logger.error(m18n.n('tools_update_failed_to_app_fetchlist'), error=e)
+ logger.error(str(e))
upgradable_apps = list(_list_upgradable_apps())
@@ -529,14 +467,17 @@ def _list_upgradable_apps():
app_list_installed = os.listdir(APPS_SETTING_PATH)
for app_id in app_list_installed:
- app_dict = app_info(app_id, raw=True)
+ app_dict = app_info(app_id, full=True)
if app_dict["upgradable"] == "yes":
- current_version = app_dict.get("version", "?")
- current_commit = app_dict.get("status", {}).get("remote", {}).get("revision", "?")[:7]
- new_version = app_dict.get("manifest",{}).get("version","?")
- new_commit = app_dict.get("git", {}).get("revision", "?")[:7]
+ # FIXME : would make more sense for these infos to be computed
+ # directly in app_info and used to check the upgradability of
+ # the app...
+ current_version = app_dict.get("manifest", {}).get("version", "?")
+ current_commit = app_dict.get("settings", {}).get("current_revision", "?")[:7]
+ new_version = app_dict.get("from_catalog", {}).get("manifest", {}).get("version", "?")
+ new_commit = app_dict.get("from_catalog", {}).get("git", {}).get("revision", "?")[:7]
if current_version == new_version:
current_version += " (" + current_commit + ")"
@@ -616,8 +557,8 @@ def tools_upgrade(operation_logger, apps=None, system=False):
# randomly from yunohost itself... upgrading them is likely to
critical_packages = ("moulinette", "yunohost", "yunohost-admin", "ssowat", "python")
- critical_packages_upgradable = [p for p in upgradables if p["name"] in critical_packages]
- noncritical_packages_upgradable = [p for p in upgradables if p["name"] not in critical_packages]
+ critical_packages_upgradable = [p["name"] for p in upgradables if p["name"] in critical_packages]
+ noncritical_packages_upgradable = [p["name"] for p in upgradables if p["name"] not in critical_packages]
# Prepare dist-upgrade command
dist_upgrade = "DEBIAN_FRONTEND=noninteractive"
@@ -648,8 +589,11 @@ def tools_upgrade(operation_logger, apps=None, system=False):
logger.debug("Running apt command :\n{}".format(dist_upgrade))
+ def is_relevant(l):
+ return "Reading database ..." not in l.rstrip()
+
callbacks = (
- lambda l: logger.info("+" + l.rstrip() + "\r"),
+ lambda l: logger.info("+ " + l.rstrip() + "\r") if is_relevant(l) else logger.debug(l.rstrip() + "\r"),
lambda l: logger.warning(l.rstrip()),
)
returncode = call_async_output(dist_upgrade, callbacks, shell=True)
@@ -726,203 +670,6 @@ def tools_upgrade(operation_logger, apps=None, system=False):
operation_logger.success()
-def tools_diagnosis(private=False):
- """
- Return global info about current yunohost instance to help debugging
-
- """
- diagnosis = OrderedDict()
-
- # Debian release
- try:
- with open('/etc/debian_version', 'r') as f:
- debian_version = f.read().rstrip()
- except IOError as e:
- logger.warning(m18n.n('diagnosis_debian_version_error', error=format(e)), exc_info=1)
- else:
- diagnosis['host'] = "Debian %s" % debian_version
-
- # Kernel version
- try:
- with open('/proc/sys/kernel/osrelease', 'r') as f:
- kernel_version = f.read().rstrip()
- except IOError as e:
- logger.warning(m18n.n('diagnosis_kernel_version_error', error=format(e)), exc_info=1)
- else:
- diagnosis['kernel'] = kernel_version
-
- # Packages version
- diagnosis['packages'] = ynh_packages_version()
-
- diagnosis["backports"] = check_output("dpkg -l |awk '/^ii/ && $3 ~ /bpo[6-8]/ {print $2}'").split()
-
- # Server basic monitoring
- diagnosis['system'] = OrderedDict()
- try:
- disks = monitor_disk(units=['filesystem'], human_readable=True)
- except (YunohostError, Fault) as e:
- logger.warning(m18n.n('diagnosis_monitor_disk_error', error=format(e)), exc_info=1)
- else:
- diagnosis['system']['disks'] = {}
- for disk in disks:
- if isinstance(disks[disk], str):
- diagnosis['system']['disks'][disk] = disks[disk]
- else:
- diagnosis['system']['disks'][disk] = 'Mounted on %s, %s (%s free)' % (
- disks[disk]['mnt_point'],
- disks[disk]['size'],
- disks[disk]['avail']
- )
-
- try:
- system = monitor_system(units=['cpu', 'memory'], human_readable=True)
- except YunohostError as e:
- logger.warning(m18n.n('diagnosis_monitor_system_error', error=format(e)), exc_info=1)
- else:
- diagnosis['system']['memory'] = {
- 'ram': '%s (%s free)' % (system['memory']['ram']['total'], system['memory']['ram']['free']),
- 'swap': '%s (%s free)' % (system['memory']['swap']['total'], system['memory']['swap']['free']),
- }
-
- # nginx -t
- p = subprocess.Popen("nginx -t".split(),
- stdout=subprocess.PIPE,
- stderr=subprocess.STDOUT)
- out, _ = p.communicate()
- diagnosis["nginx"] = out.strip().split("\n")
- if p.returncode != 0:
- logger.error(out)
-
- # Services status
- services = service_status()
- diagnosis['services'] = {}
-
- for service in services:
- diagnosis['services'][service] = "%s (%s)" % (services[service]['status'], services[service]['loaded'])
-
- # YNH Applications
- try:
- applications = app_list()['apps']
- except YunohostError as e:
- diagnosis['applications'] = m18n.n('diagnosis_no_apps')
- else:
- diagnosis['applications'] = {}
- for application in applications:
- if application['installed']:
- diagnosis['applications'][application['id']] = application['label'] if application['label'] else application['name']
-
- # Private data
- if private:
- diagnosis['private'] = OrderedDict()
-
- # Public IP
- diagnosis['private']['public_ip'] = {}
- diagnosis['private']['public_ip']['IPv4'] = get_public_ip(4)
- diagnosis['private']['public_ip']['IPv6'] = get_public_ip(6)
-
- # Domains
- diagnosis['private']['domains'] = domain_list()['domains']
-
- diagnosis['private']['regen_conf'] = regen_conf(with_diff=True, dry_run=True)
-
- try:
- diagnosis['security'] = {
- "CVE-2017-5754": {
- "name": "meltdown",
- "vulnerable": _check_if_vulnerable_to_meltdown(),
- }
- }
- except Exception as e:
- import traceback
- traceback.print_exc()
- logger.warning("Unable to check for meltdown vulnerability: %s" % e)
-
- return diagnosis
-
-
-def _check_if_vulnerable_to_meltdown():
- # meltdown CVE: https://security-tracker.debian.org/tracker/CVE-2017-5754
-
- # We use a cache file to avoid re-running the script so many times,
- # which can be expensive (up to around 5 seconds on ARM)
- # and make the admin appear to be slow (c.f. the calls to diagnosis
- # from the webadmin)
- #
- # The cache is in /tmp and shall disappear upon reboot
- # *or* we compare it to dpkg.log modification time
- # such that it's re-ran if there was package upgrades
- # (e.g. from yunohost)
- cache_file = "/tmp/yunohost-meltdown-diagnosis"
- dpkg_log = "/var/log/dpkg.log"
- if os.path.exists(cache_file):
- if not os.path.exists(dpkg_log) or os.path.getmtime(cache_file) > os.path.getmtime(dpkg_log):
- logger.debug("Using cached results for meltdown checker, from %s" % cache_file)
- return read_json(cache_file)[0]["VULNERABLE"]
-
- # script taken from https://github.com/speed47/spectre-meltdown-checker
- # script commit id is store directly in the script
- file_dir = os.path.split(__file__)[0]
- SCRIPT_PATH = os.path.join(file_dir, "./vendor/spectre-meltdown-checker/spectre-meltdown-checker.sh")
-
- # '--variant 3' corresponds to Meltdown
- # example output from the script:
- # [{"NAME":"MELTDOWN","CVE":"CVE-2017-5754","VULNERABLE":false,"INFOS":"PTI mitigates the vulnerability"}]
- try:
- logger.debug("Running meltdown vulnerability checker")
- call = subprocess.Popen("bash %s --batch json --variant 3" %
- SCRIPT_PATH, shell=True,
- stdout=subprocess.PIPE,
- stderr=subprocess.PIPE)
-
- # TODO / FIXME : here we are ignoring error messages ...
- # in particular on RPi2 and other hardware, the script complains about
- # "missing some kernel info (see -v), accuracy might be reduced"
- # Dunno what to do about that but we probably don't want to harass
- # users with this warning ...
- output, err = call.communicate()
- assert call.returncode in (0, 2, 3), "Return code: %s" % call.returncode
-
- # If there are multiple lines, sounds like there was some messages
- # in stdout that are not json >.> ... Try to get the actual json
- # stuff which should be the last line
- output = output.strip()
- if "\n" in output:
- logger.debug("Original meltdown checker output : %s" % output)
- output = output.split("\n")[-1]
-
- CVEs = json.loads(output)
- assert len(CVEs) == 1
- assert CVEs[0]["NAME"] == "MELTDOWN"
- except Exception as e:
- import traceback
- traceback.print_exc()
- logger.warning("Something wrong happened when trying to diagnose Meltdown vunerability, exception: %s" % e)
- raise Exception("Command output for failed meltdown check: '%s'" % output)
-
- logger.debug("Writing results from meltdown checker to cache file, %s" % cache_file)
- write_to_json(cache_file, CVEs)
- return CVEs[0]["VULNERABLE"]
-
-
-def tools_port_available(port):
- """
- Check availability of a local port
-
- Keyword argument:
- port -- Port to check
-
- """
- try:
- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- s.settimeout(1)
- s.connect(("localhost", int(port)))
- s.close()
- except socket.error:
- return True
- else:
- return False
-
-
@is_unit_operation()
def tools_shutdown(operation_logger, force=False):
shutdown = force
diff --git a/src/yunohost/user.py b/src/yunohost/user.py
index a6c262ed7..282ec8407 100644
--- a/src/yunohost/user.py
+++ b/src/yunohost/user.py
@@ -32,10 +32,13 @@ import crypt
import random
import string
import subprocess
+import copy
from moulinette import m18n
-from yunohost.utils.error import YunohostError
from moulinette.utils.log import getActionLogger
+from moulinette.utils.filesystem import read_json, write_to_json, read_yaml, write_to_yaml
+
+from yunohost.utils.error import YunohostError
from yunohost.service import service_status
from yunohost.log import is_unit_operation
@@ -126,12 +129,18 @@ def user_create(operation_logger, username, firstname, lastname, mail, password,
ldap = _get_ldap_interface()
+ if username in user_list()["users"]:
+ raise YunohostError("user_already_exists", user=username)
+
# Validate uniqueness of username and mail in LDAP
- ldap.validate_uniqueness({
- 'uid': username,
- 'mail': mail,
- 'cn': username
- })
+ try:
+ ldap.validate_uniqueness({
+ 'uid': username,
+ 'mail': mail,
+ 'cn': username
+ })
+ except Exception as e:
+ raise YunohostError('user_creation_failed', user=username, error=e)
# Validate uniqueness of username in system users
all_existing_usernames = {x.pw_name for x in pwd.getpwall()}
@@ -156,12 +165,13 @@ def user_create(operation_logger, username, firstname, lastname, mail, password,
operation_logger.start()
# Get random UID/GID
- all_uid = {x.pw_uid for x in pwd.getpwall()}
- all_gid = {x.gr_gid for x in grp.getgrall()}
+ all_uid = {str(x.pw_uid) for x in pwd.getpwall()}
+ all_gid = {str(x.gr_gid) for x in grp.getgrall()}
uid_guid_found = False
while not uid_guid_found:
- uid = str(random.randint(200, 99999))
+ # LXC uid number is limited to 65536 by default
+ uid = str(random.randint(200, 65000))
uid_guid_found = uid not in all_uid and uid not in all_gid
# Adapt values for LDAP
@@ -187,50 +197,35 @@ def user_create(operation_logger, username, firstname, lastname, mail, password,
if not ldap.search(base='ou=users,dc=yunohost,dc=org', filter='uid=*'):
attr_dict['mail'] = [attr_dict['mail']] + aliases
- # If exists, remove the redirection from the SSO
- try:
- with open('/etc/ssowat/conf.json.persistent') as json_conf:
- ssowat_conf = json.loads(str(json_conf.read()))
- except ValueError as e:
- raise YunohostError('ssowat_persistent_conf_read_error', error=e.strerror)
- except IOError:
- ssowat_conf = {}
+ try:
+ ldap.add('uid=%s,ou=users' % username, attr_dict)
+ except Exception as e:
+ raise YunohostError('user_creation_failed', user=username, error=e)
- if 'redirected_urls' in ssowat_conf and '/' in ssowat_conf['redirected_urls']:
- del ssowat_conf['redirected_urls']['/']
- try:
- with open('/etc/ssowat/conf.json.persistent', 'w+') as f:
- json.dump(ssowat_conf, f, sort_keys=True, indent=4)
- except IOError as e:
- raise YunohostError('ssowat_persistent_conf_write_error', error=e.strerror)
+ # Invalidate passwd and group to take user and group creation into account
+ subprocess.call(['nscd', '-i', 'passwd'])
+ subprocess.call(['nscd', '-i', 'group'])
- if ldap.add('uid=%s,ou=users' % username, attr_dict):
- # Invalidate passwd to take user creation into account
- subprocess.call(['nscd', '-i', 'passwd'])
+ try:
+ # Attempt to create user home folder
+ subprocess.check_call(
+ ['su', '-', username, '-c', "''"])
+ except subprocess.CalledProcessError:
+ if not os.path.isdir('/home/{0}'.format(username)):
+ logger.warning(m18n.n('user_home_creation_failed'),
+ exc_info=1)
- try:
- # Attempt to create user home folder
- subprocess.check_call(
- ['su', '-', username, '-c', "''"])
- except subprocess.CalledProcessError:
- if not os.path.isdir('/home/{0}'.format(username)):
- logger.warning(m18n.n('user_home_creation_failed'),
- exc_info=1)
+ # Create group for user and add to group 'all_users'
+ user_group_create(groupname=username, gid=uid, primary_group=True, sync_perm=False)
+ user_group_update(groupname='all_users', add=username, force=True, sync_perm=True)
- # Create group for user and add to group 'all_users'
- user_group_add(groupname=username, gid=uid, sync_perm=False)
- user_group_update(groupname=username, add_user=username, force=True, sync_perm=False)
- user_group_update(groupname='all_users', add_user=username, force=True, sync_perm=True)
+ # TODO: Send a welcome mail to user
+ logger.success(m18n.n('user_created'))
- # TODO: Send a welcome mail to user
- logger.success(m18n.n('user_created'))
+ hook_callback('post_user_create',
+ args=[username, mail, password, firstname, lastname])
- hook_callback('post_user_create',
- args=[username, mail, password, firstname, lastname])
-
- return {'fullname': fullname, 'username': username, 'mail': mail}
-
- raise YunohostError('user_creation_failed')
+ return {'fullname': fullname, 'username': username, 'mail': mail}
@is_unit_operation([('username', 'user')])
@@ -245,32 +240,40 @@ def user_delete(operation_logger, username, purge=False):
"""
from yunohost.hook import hook_callback
from yunohost.utils.ldap import _get_ldap_interface
+ from yunohost.permission import permission_sync_to_user
+
+ if username not in user_list()["users"]:
+ raise YunohostError('user_unknown', user=username)
operation_logger.start()
+ user_group_update("all_users", remove=username, force=True, sync_perm=False)
+ for group, infos in user_group_list()["groups"].items():
+ if group == "all_users":
+ continue
+ # If the user is in this group (and it's not the primary group),
+ # remove the member from the group
+ if username != group and username in infos["members"]:
+ user_group_update(group, remove=username, sync_perm=False)
+
+ # Delete primary group if it exists (why wouldnt it exists ? because some
+ # epic bug happened somewhere else and only a partial removal was
+ # performed...)
+ if username in user_group_list()['groups'].keys():
+ user_group_delete(username, force=True, sync_perm=True)
+
ldap = _get_ldap_interface()
- if ldap.remove('uid=%s,ou=users' % username):
- # Invalidate passwd to take user deletion into account
- subprocess.call(['nscd', '-i', 'passwd'])
+ try:
+ ldap.remove('uid=%s,ou=users' % username)
+ except Exception as e:
+ raise YunohostError('user_deletion_failed', user=username, error=e)
- if purge:
- subprocess.call(['rm', '-rf', '/home/{0}'.format(username)])
- subprocess.call(['rm', '-rf', '/var/mail/{0}'.format(username)])
- else:
- raise YunohostError('user_deletion_failed')
+ # Invalidate passwd to take user deletion into account
+ subprocess.call(['nscd', '-i', 'passwd'])
- user_group_delete(username, force=True, sync_perm=True)
-
- group_list = ldap.search('ou=groups,dc=yunohost,dc=org',
- '(&(objectclass=groupOfNamesYnh)(memberUid=%s))'
- % username, ['cn'])
- for group in group_list:
- user_list = ldap.search('ou=groups,dc=yunohost,dc=org',
- 'cn=' + group['cn'][0],
- ['memberUid'])[0]
- user_list['memberUid'].remove(username)
- if not ldap.update('cn=%s,ou=groups' % group['cn'][0], user_list):
- raise YunohostError('group_update_failed')
+ if purge:
+ subprocess.call(['rm', '-rf', '/home/{0}'.format(username)])
+ subprocess.call(['rm', '-rf', '/var/mail/{0}'.format(username)])
hook_callback('post_user_delete', args=[username, purge])
@@ -338,7 +341,10 @@ def user_update(operation_logger, username, firstname=None, lastname=None, mail=
'webmaster@' + main_domain,
'postmaster@' + main_domain,
]
- ldap.validate_uniqueness({'mail': mail})
+ try:
+ ldap.validate_uniqueness({'mail': mail})
+ except Exception as e:
+ raise YunohostError('user_update_failed', user=username, error=e)
if mail[mail.find('@') + 1:] not in domains:
raise YunohostError('mail_domain_unknown', domain=mail[mail.find('@') + 1:])
if mail in aliases:
@@ -351,7 +357,10 @@ def user_update(operation_logger, username, firstname=None, lastname=None, mail=
if not isinstance(add_mailalias, list):
add_mailalias = [add_mailalias]
for mail in add_mailalias:
- ldap.validate_uniqueness({'mail': mail})
+ try:
+ ldap.validate_uniqueness({'mail': mail})
+ except Exception as e:
+ raise YunohostError('user_update_failed', user=username, error=e)
if mail[mail.find('@') + 1:] not in domains:
raise YunohostError('mail_domain_unknown', domain=mail[mail.find('@') + 1:])
user['mail'].append(mail)
@@ -391,12 +400,14 @@ def user_update(operation_logger, username, firstname=None, lastname=None, mail=
operation_logger.start()
- if ldap.update('uid=%s,ou=users' % username, new_attr_dict):
- logger.success(m18n.n('user_updated'))
- app_ssowatconf()
- return user_info(username)
- else:
- raise YunohostError('user_update_failed')
+ try:
+ ldap.update('uid=%s,ou=users' % username, new_attr_dict)
+ except Exception as e:
+ raise YunohostError('user_update_failed', user=username, error=e)
+
+ logger.success(m18n.n('user_updated'))
+ app_ssowatconf()
+ return user_info(username)
def user_info(username):
@@ -453,7 +464,7 @@ def user_info(username):
if service_status("dovecot")["status"] != "running":
logger.warning(m18n.n('mailbox_used_space_dovecot_down'))
- elif not user_permission_list(app="mail", permission="main", username=username)['permissions']:
+ elif username not in user_permission_list(full=True)["permissions"]["mail.main"]["corresponding_users"]:
logger.warning(m18n.n('mailbox_disabled', user=username))
else:
cmd = 'doveadm -f flow quota get -u %s' % user['uid'][0]
@@ -480,81 +491,59 @@ def user_info(username):
'use': storage_use
}
- if result:
- return result_dict
- else:
- raise YunohostError('user_info_failed')
+ return result_dict
#
# Group subcategory
#
-def user_group_list(fields=None):
+def user_group_list(short=False, full=False, include_primary_groups=True):
"""
List users
Keyword argument:
- filter -- LDAP filter used to search
- offset -- Starting number for user fetching
- limit -- Maximum number of user fetched
- fields -- fields to fetch
-
+ short -- Only list the name of the groups without any additional info
+ full -- List all the info available for each groups
+ include_primary_groups -- Include groups corresponding to users (which should always only contains this user)
+ This option is set to false by default in the action map because we don't want to have
+ these displayed when the user runs `yunohost user group list`, but internally we do want
+ to list them when called from other functions
"""
- from yunohost.utils.ldap import _get_ldap_interface
+
+ # Fetch relevant informations
+
+ from yunohost.utils.ldap import _get_ldap_interface, _ldap_path_extract
ldap = _get_ldap_interface()
- group_attr = {
- 'cn': 'groupname',
- 'member': 'members',
- 'permission': 'permission'
- }
- attrs = ['cn']
+ groups_infos = ldap.search('ou=groups,dc=yunohost,dc=org',
+ '(objectclass=groupOfNamesYnh)',
+ ["cn", "member", "permission"])
+
+ # Parse / organize information to be outputed
+
+ users = user_list()["users"]
groups = {}
+ for infos in groups_infos:
- if fields:
- keys = group_attr.keys()
- for attr in fields:
- if attr in keys:
- attrs.append(attr)
- else:
- raise YunohostError('field_invalid', attr)
- else:
- attrs = ['cn', 'member']
+ name = infos["cn"][0]
- result = ldap.search('ou=groups,dc=yunohost,dc=org',
- '(objectclass=groupOfNamesYnh)',
- attrs)
-
- for group in result:
- # The group "admins" should be hidden for the user
- if group_attr['cn'] == "admins":
+ if not include_primary_groups and name in users:
continue
- entry = {}
- for attr, values in group.items():
- if values:
- if attr == "member":
- entry[group_attr[attr]] = []
- for v in values:
- entry[group_attr[attr]].append(v.split("=")[1].split(",")[0])
- elif attr == "permission":
- entry[group_attr[attr]] = {}
- for v in values:
- permission = v.split("=")[1].split(",")[0].split(".")[1]
- pType = v.split("=")[1].split(",")[0].split(".")[0]
- if permission in entry[group_attr[attr]]:
- entry[group_attr[attr]][permission].append(pType)
- else:
- entry[group_attr[attr]][permission] = [pType]
- else:
- entry[group_attr[attr]] = values[0]
- groupname = entry[group_attr['cn']]
- groups[groupname] = entry
+ groups[name] = {}
+
+ groups[name]["members"] = [_ldap_path_extract(p, "uid") for p in infos.get("member", [])]
+
+ if full:
+ groups[name]["permissions"] = [_ldap_path_extract(p, "cn") for p in infos.get("permission", [])]
+
+ if short:
+ groups = groups.keys()
return {'groups': groups}
-@is_unit_operation([('groupname', 'user')])
-def user_group_add(operation_logger, groupname, gid=None, sync_perm=True):
+@is_unit_operation([('groupname', 'group')])
+def user_group_create(operation_logger, groupname, gid=None, primary_group=False, sync_perm=True):
"""
Create group
@@ -565,8 +554,6 @@ def user_group_add(operation_logger, groupname, gid=None, sync_perm=True):
from yunohost.permission import permission_sync_to_user
from yunohost.utils.ldap import _get_ldap_interface
- operation_logger.start()
-
ldap = _get_ldap_interface()
# Validate uniqueness of groupname in LDAP
@@ -574,12 +561,16 @@ def user_group_add(operation_logger, groupname, gid=None, sync_perm=True):
'cn': groupname
}, base_dn='ou=groups,dc=yunohost,dc=org')
if conflict:
- raise YunohostError('group_name_already_exist', name=groupname)
+ raise YunohostError('group_already_exist', group=groupname)
# Validate uniqueness of groupname in system group
all_existing_groupnames = {x.gr_name for x in grp.getgrall()}
if groupname in all_existing_groupnames:
- raise YunohostError('system_groupname_exists')
+ if primary_group:
+ logger.warning(m18n.n('group_already_exist_on_system_but_removing_it', group=groupname))
+ subprocess.check_call("sed --in-place '/^%s:/d' /etc/group" % groupname, shell=True)
+ else:
+ raise YunohostError('group_already_exist_on_system', group=groupname)
if not gid:
# Get random GID
@@ -596,16 +587,30 @@ def user_group_add(operation_logger, groupname, gid=None, sync_perm=True):
'gidNumber': gid,
}
- if ldap.add('cn=%s,ou=groups' % groupname, attr_dict):
+ # Here we handle the creation of a primary group
+ # We want to initialize this group to contain the corresponding user
+ # (then we won't be able to add/remove any user in this group)
+ if primary_group:
+ attr_dict["member"] = ["uid=" + groupname + ",ou=users,dc=yunohost,dc=org"]
+
+ operation_logger.start()
+ try:
+ ldap.add('cn=%s,ou=groups' % groupname, attr_dict)
+ except Exception as e:
+ raise YunohostError('group_creation_failed', group=groupname, error=e)
+
+ if sync_perm:
+ permission_sync_to_user()
+
+ if not primary_group:
logger.success(m18n.n('group_created', group=groupname))
- if sync_perm:
- permission_sync_to_user()
- return {'name': groupname}
+ else:
+ logger.debug(m18n.n('group_created', group=groupname))
- raise YunohostError('group_creation_failed', group=groupname)
+ return {'name': groupname}
-@is_unit_operation([('groupname', 'user')])
+@is_unit_operation([('groupname', 'group')])
def user_group_delete(operation_logger, groupname, force=False, sync_perm=True):
"""
Delete user
@@ -617,102 +622,109 @@ def user_group_delete(operation_logger, groupname, force=False, sync_perm=True):
from yunohost.permission import permission_sync_to_user
from yunohost.utils.ldap import _get_ldap_interface
- forbidden_groups = ["all_users", "admins"] + user_list(fields=['uid'])['users'].keys()
- if not force and groupname in forbidden_groups:
- raise YunohostError('group_deletion_not_allowed', group=groupname)
+ existing_groups = user_group_list()['groups'].keys()
+ if groupname not in existing_groups:
+ raise YunohostError('group_unknown', group=groupname)
+
+ # Refuse to delete primary groups of a user (e.g. group 'sam' related to user 'sam')
+ # without the force option...
+ #
+ # We also can't delete "all_users" because that's a special group...
+ existing_users = user_list()['users'].keys()
+ undeletable_groups = existing_users + ["all_users", "visitors"]
+ if groupname in undeletable_groups and not force:
+ raise YunohostError('group_cannot_be_deleted', group=groupname)
operation_logger.start()
ldap = _get_ldap_interface()
- if not ldap.remove('cn=%s,ou=groups' % groupname):
- raise YunohostError('group_deletion_failed', group=groupname)
+ try:
+ ldap.remove('cn=%s,ou=groups' % groupname)
+ except Exception as e:
+ raise YunohostError('group_deletion_failed', group=groupname, error=e)
- logger.success(m18n.n('group_deleted', group=groupname))
if sync_perm:
permission_sync_to_user()
+ if groupname not in existing_users:
+ logger.success(m18n.n('group_deleted', group=groupname))
+ else:
+ logger.debug(m18n.n('group_deleted', group=groupname))
-@is_unit_operation([('groupname', 'user')])
-def user_group_update(operation_logger, groupname, add_user=None, remove_user=None, force=False, sync_perm=True):
+
+@is_unit_operation([('groupname', 'group')])
+def user_group_update(operation_logger, groupname, add=None, remove=None, force=False, sync_perm=True):
"""
Update user informations
Keyword argument:
groupname -- Groupname to update
- add_user -- User to add in group
- remove_user -- User to remove in group
+ add -- User(s) to add in group
+ remove -- User(s) to remove in group
"""
from yunohost.permission import permission_sync_to_user
from yunohost.utils.ldap import _get_ldap_interface
- if (groupname == 'all_users' or groupname == 'admins') and not force:
- raise YunohostError('edit_group_not_allowed', group=groupname)
+ existing_users = user_list()['users'].keys()
- ldap = _get_ldap_interface()
+ # Refuse to edit a primary group of a user (e.g. group 'sam' related to user 'sam')
+ # Those kind of group should only ever contain the user (e.g. sam) and only this one.
+ # We also can't edit "all_users" without the force option because that's a special group...
+ if not force:
+ if groupname == "all_users":
+ raise YunohostError('group_cannot_edit_all_users')
+ elif groupname == "visitors":
+ raise YunohostError('group_cannot_edit_visitors')
+ elif groupname in existing_users:
+ raise YunohostError('group_cannot_edit_primary_group', group=groupname)
- # Populate group informations
- attrs_to_fetch = ['member']
- result = ldap.search(base='ou=groups,dc=yunohost,dc=org',
- filter='cn=' + groupname, attrs=attrs_to_fetch)
- if not result:
- raise YunohostError('group_unknown', group=groupname)
- group = result[0]
+ # We extract the uid for each member of the group to keep a simple flat list of members
+ current_group = user_group_info(groupname)["members"]
+ new_group = copy.copy(current_group)
- new_group_list = {'member': set(), 'memberUid': set()}
- if 'member' in group:
- new_group_list['member'] = set(group['member'])
- else:
- group['member'] = []
+ if add:
+ users_to_add = [add] if not isinstance(add, list) else add
- existing_users = user_list(fields=['uid'])['users'].keys()
-
- if add_user:
- if not isinstance(add_user, list):
- add_user = [add_user]
-
- for user in add_user:
+ for user in users_to_add:
if user not in existing_users:
raise YunohostError('user_unknown', user=user)
- for user in add_user:
- userDN = "uid=" + user + ",ou=users,dc=yunohost,dc=org"
- if userDN in group['member']:
- logger.warning(m18n.n('user_already_in_group', user=user, group=groupname))
- new_group_list['member'].add(userDN)
-
- if remove_user:
- if not isinstance(remove_user, list):
- remove_user = [remove_user]
-
- for user in remove_user:
- if user == groupname:
- raise YunohostError('remove_user_of_group_not_allowed', user=user, group=groupname)
-
- for user in remove_user:
- userDN = "uid=" + user + ",ou=users,dc=yunohost,dc=org"
- if 'member' in group and userDN in group['member']:
- new_group_list['member'].remove(userDN)
+ if user in current_group:
+ logger.warning(m18n.n('group_user_already_in_group', user=user, group=groupname))
else:
- logger.warning(m18n.n('user_not_in_group', user=user, group=groupname))
+ operation_logger.related_to.append(('user', user))
- # Sychronise memberUid with member (to keep the posix group structure)
- # In posixgroup the main group of each user is only written in the gid number of the user
- for member in new_group_list['member']:
- member_Uid = member.split("=")[1].split(",")[0]
- # Don't add main user in the group.
- # Note that in the Unix system the main user of the group is linked by the gid in the user attribute.
- # So the main user need to be not in the memberUid list of his own group.
- if member_Uid != groupname:
- new_group_list['memberUid'].add(member_Uid)
+ new_group += users_to_add
- operation_logger.start()
+ if remove:
+ users_to_remove = [remove] if not isinstance(remove, list) else remove
- if new_group_list['member'] != set(group['member']):
- if not ldap.update('cn=%s,ou=groups' % groupname, new_group_list):
- raise YunohostError('group_update_failed', group=groupname)
+ for user in users_to_remove:
+ if user not in current_group:
+ logger.warning(m18n.n('group_user_not_in_group', user=user, group=groupname))
+ else:
+ operation_logger.related_to.append(('user', user))
+
+ # Remove users_to_remove from new_group
+ # Kinda like a new_group -= users_to_remove
+ new_group = [u for u in new_group if u not in users_to_remove]
+
+ new_group_dns = ["uid=" + user + ",ou=users,dc=yunohost,dc=org" for user in new_group]
+
+ if set(new_group) != set(current_group):
+ operation_logger.start()
+ ldap = _get_ldap_interface()
+ try:
+ ldap.update('cn=%s,ou=groups' % groupname, {"member": set(new_group_dns), "memberUid": set(new_group)})
+ except Exception as e:
+ raise YunohostError('group_update_failed', group=groupname, error=e)
+
+ if groupname != "all_users":
+ logger.success(m18n.n('group_updated', group=groupname))
+ else:
+ logger.debug(m18n.n('group_updated', group=groupname))
- logger.success(m18n.n('group_updated', group=groupname))
if sync_perm:
permission_sync_to_user()
return user_group_info(groupname)
@@ -727,62 +739,54 @@ def user_group_info(groupname):
"""
- from yunohost.utils.ldap import _get_ldap_interface
+ from yunohost.utils.ldap import _get_ldap_interface, _ldap_path_extract
ldap = _get_ldap_interface()
- group_attrs = [
- 'cn', 'member', 'permission'
- ]
- result = ldap.search('ou=groups,dc=yunohost,dc=org', "cn=" + groupname, group_attrs)
+ # Fetch info for this group
+ result = ldap.search('ou=groups,dc=yunohost,dc=org',
+ "cn=" + groupname,
+ ["cn", "member", "permission"])
if not result:
raise YunohostError('group_unknown', group=groupname)
- group = result[0]
+ infos = result[0]
- result_dict = {
- 'groupname': group['cn'][0],
- 'member': None
+ # Format data
+
+ return {
+ 'members': [_ldap_path_extract(p, "uid") for p in infos.get("member", [])],
+ 'permissions': [_ldap_path_extract(p, "cn") for p in infos.get("permission", [])]
}
- if 'member' in group:
- result_dict['member'] = {m.split("=")[1].split(",")[0] for m in group['member']}
- return result_dict
#
# Permission subcategory
#
-def user_permission_list(app=None, permission=None, username=None, group=None, sync_perm=True):
+def user_permission_list(short=False, full=False):
import yunohost.permission
- return yunohost.permission.user_permission_list(app, permission, username, group)
+ return yunohost.permission.user_permission_list(short, full)
-@is_unit_operation([('app', 'user')])
-def user_permission_add(operation_logger, app, permission="main", username=None, group=None, sync_perm=True):
+def user_permission_update(permission, add=None, remove=None, sync_perm=True):
import yunohost.permission
- return yunohost.permission.user_permission_update(operation_logger, app, permission=permission,
- add_username=username, add_group=group,
- del_username=None, del_group=None,
+ return yunohost.permission.user_permission_update(permission,
+ add=add, remove=remove,
sync_perm=sync_perm)
-@is_unit_operation([('app', 'user')])
-def user_permission_remove(operation_logger, app, permission="main", username=None, group=None, sync_perm=True):
+def user_permission_reset(permission, sync_perm=True):
import yunohost.permission
- return yunohost.permission.user_permission_update(operation_logger, app, permission=permission,
- add_username=None, add_group=None,
- del_username=username, del_group=group,
- sync_perm=sync_perm)
-
-
-@is_unit_operation([('app', 'user')])
-def user_permission_clear(operation_logger, app, permission=None, sync_perm=True):
- import yunohost.permission
- return yunohost.permission.user_permission_clear(operation_logger, app, permission,
+ return yunohost.permission.user_permission_reset(permission,
sync_perm=sync_perm)
+def user_permission_info(permission):
+ import yunohost.permission
+ return yunohost.permission.user_permission_info(permission)
+
+
#
# SSH subcategory
#
diff --git a/src/yunohost/utils/error.py b/src/yunohost/utils/error.py
index aeffabcf0..75cf35093 100644
--- a/src/yunohost/utils/error.py
+++ b/src/yunohost/utils/error.py
@@ -27,12 +27,14 @@ class YunohostError(MoulinetteError):
"""
Yunohost base exception
-
+
The (only?) main difference with MoulinetteError being that keys
are translated via m18n.n (namespace) instead of m18n.g (global?)
"""
def __init__(self, key, raw_msg=False, *args, **kwargs):
+ self.key = key # Saving the key is useful for unit testing
+ self.kwargs = kwargs # Saving the key is useful for unit testing
if raw_msg:
msg = key
else:
diff --git a/src/yunohost/utils/ldap.py b/src/yunohost/utils/ldap.py
index 186cdbdec..22e95ad07 100644
--- a/src/yunohost/utils/ldap.py
+++ b/src/yunohost/utils/ldap.py
@@ -20,7 +20,7 @@
"""
import atexit
-from moulinette.core import init_authenticator
+from moulinette.authenticators import ldap
# We use a global variable to do some caching
# to avoid re-authenticating in case we call _get_ldap_authenticator multiple times
@@ -31,15 +31,33 @@ def _get_ldap_interface():
global _ldap_interface
if _ldap_interface is None:
- # Instantiate LDAP Authenticator
- AUTH_IDENTIFIER = ('ldap', 'as-root')
- AUTH_PARAMETERS = {'uri': 'ldapi://%2Fvar%2Frun%2Fslapd%2Fldapi',
- 'base_dn': 'dc=yunohost,dc=org',
- 'user_rdn': 'gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth'}
- _ldap_interface = init_authenticator(AUTH_IDENTIFIER, AUTH_PARAMETERS)
+
+ conf = { "vendor": "ldap",
+ "name": "as-root",
+ "parameters": { 'uri': 'ldapi://%2Fvar%2Frun%2Fslapd%2Fldapi',
+ 'base_dn': 'dc=yunohost,dc=org',
+ 'user_rdn': 'gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth' },
+ "extra": {}
+ }
+
+ _ldap_interface = ldap.Authenticator(**conf)
return _ldap_interface
+
+# We regularly want to extract stuff like 'bar' in ldap path like
+# foo=bar,dn=users.example.org,ou=example.org,dc=org so this small helper allow
+# to do this without relying of dozens of mysterious string.split()[0]
+#
+# e.g. using _ldap_path_extract(path, "foo") on the previous example will
+# return bar
+
+def _ldap_path_extract(path, info):
+ for element in path.split(","):
+ if element.startswith(info + "="):
+ return element[len(info + "="):]
+
+
# Add this to properly close / delete the ldap interface / authenticator
# when Python exits ...
# Otherwise there's a risk that some funky error appears at the very end
diff --git a/src/yunohost/utils/network.py b/src/yunohost/utils/network.py
index 4e23516c3..23b2310f8 100644
--- a/src/yunohost/utils/network.py
+++ b/src/yunohost/utils/network.py
@@ -18,10 +18,14 @@
along with this program; if not, see http://www.gnu.org/licenses
"""
-import logging
+import os
import re
-import subprocess
+import logging
+import dns.resolver
+
from moulinette.utils.network import download_text
+from moulinette.utils.process import check_output
+from moulinette.utils.filesystem import read_file
logger = logging.getLogger('yunohost.utils.network')
@@ -36,6 +40,17 @@ def get_public_ip(protocol=4):
else:
raise ValueError("invalid protocol version")
+ # We can know that ipv6 is not available directly if this file does not exists
+ if protocol == 6 and not os.path.exists("/proc/net/if_inet6"):
+ logger.debug("IPv6 appears not at all available on the system, so assuming there's no IP address for that version")
+ return None
+
+ # If we are indeed connected in ipv4 or ipv6, we should find a default route
+ routes = check_output("ip -%s route" % protocol).split("\n")
+ if not any(r.startswith("default") for r in routes):
+ logger.debug("No default route for IPv%s, so assuming there's no IP address for that version" % protocol)
+ return None
+
try:
return download_text(url, timeout=30).strip()
except Exception as e:
@@ -47,7 +62,7 @@ def get_network_interfaces():
# Get network devices and their addresses (raw infos from 'ip addr')
devices_raw = {}
- output = subprocess.check_output('ip addr show'.split())
+ output = check_output('ip addr show')
for d in re.split(r'^(?:[0-9]+: )', output, flags=re.MULTILINE):
# Extract device name (1) and its addresses (2)
m = re.match(r'([^\s@]+)(?:@[\S]+)?: (.*)', d, flags=re.DOTALL)
@@ -62,7 +77,7 @@ def get_network_interfaces():
def get_gateway():
- output = subprocess.check_output('ip route show'.split())
+ output = check_output('ip route show')
m = re.search(r'default via (.*) dev ([a-z]+[0-9]?)', output)
if not m:
return None
@@ -71,6 +86,52 @@ def get_gateway():
return addr.popitem()[1] if len(addr) == 1 else None
+# Lazy dev caching to avoid re-reading the file multiple time when calling
+# dig() often during same yunohost operation
+external_resolvers_ = []
+
+
+def external_resolvers():
+
+ global external_resolvers_
+
+ if not external_resolvers_:
+ resolv_dnsmasq_conf = read_file("/etc/resolv.dnsmasq.conf").split("\n")
+ external_resolvers_ = [r.split(" ")[1] for r in resolv_dnsmasq_conf if r.startswith("nameserver")]
+
+ return external_resolvers_
+
+
+def dig(qname, rdtype="A", timeout=5, resolvers="local", edns_size=1500, full_answers=False):
+ """
+ Do a quick DNS request and avoid the "search" trap inside /etc/resolv.conf
+ """
+
+ if resolvers == "local":
+ resolvers = ["127.0.0.1"]
+ elif resolvers == "force_external":
+ resolvers = external_resolvers()
+ else:
+ assert isinstance(resolvers, list)
+
+ resolver = dns.resolver.Resolver(configure=False)
+ resolver.use_edns(0, 0, edns_size)
+ resolver.nameservers = resolvers
+ resolver.timeout = timeout
+ try:
+ answers = resolver.query(qname, rdtype)
+ except (dns.resolver.NXDOMAIN,
+ dns.resolver.NoNameservers,
+ dns.resolver.NoAnswer,
+ dns.exception.Timeout) as e:
+ return ("nok", (e.__class__.__name__, e))
+
+ if not full_answers:
+ answers = [answer.to_text() for answer in answers]
+
+ return ("ok", answers)
+
+
def _extract_inet(string, skip_netmask=False, skip_loopback=True):
"""
Extract IP addresses (v4 and/or v6) from a string limited to one
diff --git a/src/yunohost/utils/packages.py b/src/yunohost/utils/packages.py
index 84901bbff..debba70f4 100644
--- a/src/yunohost/utils/packages.py
+++ b/src/yunohost/utils/packages.py
@@ -33,36 +33,6 @@ logger = logging.getLogger('yunohost.utils.packages')
# Exceptions -----------------------------------------------------------------
-class PackageException(Exception):
-
- """Base exception related to a package
-
- Represent an exception related to the package named `pkgname`. If no
- `message` is provided, it will first try to use the translation key
- `message_key` if defined by the derived class. Otherwise, a standard
- message will be used.
-
- """
- message_key = 'package_unexpected_error'
-
- def __init__(self, pkgname, message=None):
- super(PackageException, self).__init__(
- message or m18n.n(self.message_key, pkgname=pkgname))
- self.pkgname = pkgname
-
-
-class UnknownPackage(PackageException):
-
- """The package is not found in the cache."""
- message_key = 'package_unknown'
-
-
-class UninstalledPackage(PackageException):
-
- """The package is not installed."""
- message_key = 'package_not_installed'
-
-
class InvalidSpecifier(ValueError):
"""An invalid specifier was found."""
@@ -402,43 +372,43 @@ def get_installed_version(*pkgnames, **kwargs):
"""Get the installed version of package(s)
Retrieve one or more packages named `pkgnames` and return their installed
- version as a dict or as a string if only one is requested and `as_dict` is
- `False`. If `strict` is `True`, an exception will be raised if a package
- is unknown or not installed.
+ version as a dict or as a string if only one is requested.
"""
versions = OrderedDict()
cache = apt.Cache()
# Retrieve options
- as_dict = kwargs.get('as_dict', False)
- strict = kwargs.get('strict', False)
with_repo = kwargs.get('with_repo', False)
for pkgname in pkgnames:
try:
pkg = cache[pkgname]
except KeyError:
- if strict:
- raise UnknownPackage(pkgname)
logger.warning(m18n.n('package_unknown', pkgname=pkgname))
+ if with_repo:
+ versions[pkgname] = {
+ "version": None,
+ "repo": None,
+ }
+ else:
+ versions[pkgname] = None
continue
try:
version = pkg.installed.version
except AttributeError:
- if strict:
- raise UninstalledPackage(pkgname)
version = None
try:
# stable, testing, unstable
repo = pkg.installed.origins[0].component
except AttributeError:
- if strict:
- raise UninstalledPackage(pkgname)
repo = ""
+ if repo == "now":
+ repo = "local"
+
if with_repo:
versions[pkgname] = {
"version": version,
@@ -449,7 +419,7 @@ def get_installed_version(*pkgnames, **kwargs):
else:
versions[pkgname] = version
- if len(pkgnames) == 1 and not as_dict:
+ if len(pkgnames) == 1:
return versions[pkgnames[0]]
return versions
diff --git a/src/yunohost/utils/yunopaste.py b/src/yunohost/utils/yunopaste.py
index 89c62d761..dc8b6fb8d 100644
--- a/src/yunohost/utils/yunopaste.py
+++ b/src/yunohost/utils/yunopaste.py
@@ -2,14 +2,23 @@
import requests
import json
+import logging
+from yunohost.domain import _get_maindomain, domain_list
+from yunohost.utils.network import get_public_ip
from yunohost.utils.error import YunohostError
+logger = logging.getLogger('yunohost.utils.yunopaste')
def yunopaste(data):
paste_server = "https://paste.yunohost.org"
+ try:
+ data = anonymize(data)
+ except Exception as e:
+ logger.warning("For some reason, YunoHost was not able to anonymize the pasted data. Sorry about that. Be careful about sharing the link, as it may contain somewhat private infos like domain names or IP addresses. Error: %s" % e)
+
try:
r = requests.post("%s/documents" % paste_server, data=data, timeout=30)
except Exception as e:
@@ -24,3 +33,47 @@ def yunopaste(data):
raise YunohostError("Uhoh, couldn't parse the answer from paste.yunohost.org : %s" % r.text, raw_msg=True)
return "%s/raw/%s" % (paste_server, url)
+
+
+def anonymize(data):
+
+ def anonymize_domain(data, domain, redact):
+ data = data.replace(domain, redact)
+ # This stuff appears sometimes because some folder in
+ # /var/lib/metronome/ have some folders named this way
+ data = data.replace(domain.replace(".", "%2e"), redact.replace(".", "%2e"))
+ return data
+
+
+ # First, let's replace every occurence of the main domain by "domain.tld"
+ # This should cover a good fraction of the info leaked
+ main_domain = _get_maindomain()
+ data = anonymize_domain(data, main_domain, "maindomain.tld")
+
+ # Next, let's replace other domains. We do this in increasing lengths,
+ # because e.g. knowing that the domain is a sub-domain of another domain may
+ # still be informative.
+ # So e.g. if there's jitsi.foobar.com as a subdomain of foobar.com, it may
+ # be interesting to know that the log is about a supposedly dedicated domain
+ # for jisti (hopefully this explanation make sense).
+ domains = domain_list()["domains"]
+ domains = sorted(domains, key=lambda d: len(d))
+
+ count = 2
+ for domain in domains:
+ if domain not in data:
+ continue
+ data = anonymize_domain(data, domain, "domain%s.tld" % count)
+ count += 1
+
+ # We also want to anonymize the ips
+ ipv4 = get_public_ip()
+ ipv6 = get_public_ip(6)
+
+ if ipv4:
+ data = data.replace(str(ipv4), "xx.xx.xx.xx")
+
+ if ipv6:
+ data = data.replace(str(ipv6), "xx:xx:xx:xx:xx:xx")
+
+ return data
diff --git a/tests/_test_m18nkeys.py b/tests/_test_m18nkeys.py
deleted file mode 100644
index cc6202517..000000000
--- a/tests/_test_m18nkeys.py
+++ /dev/null
@@ -1,101 +0,0 @@
-# -*- coding: utf-8 -*-
-
-import re
-import glob
-import json
-import yaml
-
-ignore = [ "service_description_",
- "migration_description_",
- "global_settings_setting_",
- "password_too_simple_",
- "password_listed",
- "backup_method_",
- "backup_applying_method_",
- "confirm_app_install_",
- "log_",
- ]
-
-###############################################################################
-# Find used keys in python code #
-###############################################################################
-
-# This regex matches « foo » in patterns like « m18n.n( "foo" »
-p1 = re.compile(r'm18n\.n\(\s*[\"\']([a-zA-Z0-9_]+)[\"\']')
-p2 = re.compile(r'YunohostError\([\'\"]([a-zA-Z0-9_]+)[\'\"]')
-
-python_files = glob.glob("../src/yunohost/*.py")
-python_files.extend(glob.glob("../src/yunohost/utils/*.py"))
-python_files.extend(glob.glob("../src/yunohost/data_migrations/*.py"))
-python_files.append("../bin/yunohost")
-
-python_keys = set()
-for python_file in python_files:
- content = open(python_file).read()
- for match in p1.findall(content):
- python_keys.add(match)
- for match in p2.findall(content):
- python_keys.add(match)
-
-###############################################################################
-# Find keys used in actionmap #
-###############################################################################
-
-actionmap_keys = set()
-actionmap = yaml.load(open("../data/actionsmap/yunohost.yml"))
-for _, category in actionmap.items():
- if "actions" not in category.keys():
- continue
- for _, action in category["actions"].items():
- if "arguments" not in action.keys():
- continue
- for _, argument in action["arguments"].items():
- if "extra" not in argument.keys():
- continue
- if "password" in argument["extra"]:
- actionmap_keys.add(argument["extra"]["password"])
- if "ask" in argument["extra"]:
- actionmap_keys.add(argument["extra"]["ask"])
- if "comment" in argument["extra"]:
- actionmap_keys.add(argument["extra"]["comment"])
- if "pattern" in argument["extra"]:
- actionmap_keys.add(argument["extra"]["pattern"][1])
- if "help" in argument["extra"]:
- print argument["extra"]["help"]
-
-actionmap_keys.add("admin_password")
-
-###############################################################################
-# Load en locale json keys #
-###############################################################################
-
-en_locale_file = "../locales/en.json"
-with open(en_locale_file) as f:
- en_locale_json = json.loads(f.read())
-
-en_locale_keys = set(en_locale_json.keys())
-
-###############################################################################
-# Compare keys used and keys defined #
-###############################################################################
-
-used_keys = python_keys.union(actionmap_keys)
-
-keys_used_but_not_defined = used_keys.difference(en_locale_keys)
-keys_defined_but_not_used = en_locale_keys.difference(used_keys)
-
-if len(keys_used_but_not_defined) != 0:
- print "> Error ! Those keys are used in some files but not defined :"
- for key in sorted(keys_used_but_not_defined):
- if any(key.startswith(i) for i in ignore):
- continue
- print " - %s" % key
-
-if len(keys_defined_but_not_used) != 0:
- print "> Warning ! Those keys are defined but seems unused :"
- for key in sorted(keys_defined_but_not_used):
- if any(key.startswith(i) for i in ignore):
- continue
- print " - %s" % key
-
-
diff --git a/tests/remove_stale_translated_strings.py b/tests/remove_stale_translated_strings.py
new file mode 100644
index 000000000..0213bc2be
--- /dev/null
+++ b/tests/remove_stale_translated_strings.py
@@ -0,0 +1,19 @@
+import re
+import json
+import glob
+from collections import OrderedDict
+
+locale_folder = "../locales/"
+locale_files = glob.glob(locale_folder + "*.json")
+locale_files = [filename.split("/")[-1] for filename in locale_files]
+locale_files.remove("en.json")
+
+reference = json.loads(open(locale_folder + "en.json").read())
+
+for locale_file in locale_files:
+
+ print(locale_file)
+ this_locale = json.loads(open(locale_folder + locale_file).read(), object_pairs_hook=OrderedDict)
+ this_locale_fixed = {k:v for k, v in this_locale.items() if k in reference}
+
+ json.dump(this_locale_fixed, open(locale_folder + locale_file, "w"), indent=4, ensure_ascii=False)
diff --git a/tests/test_i18n_keys.py b/tests/test_i18n_keys.py
new file mode 100644
index 000000000..9125c5d52
--- /dev/null
+++ b/tests/test_i18n_keys.py
@@ -0,0 +1,169 @@
+# -*- coding: utf-8 -*-
+
+import os
+import re
+import glob
+import json
+import yaml
+import subprocess
+
+ignore = ["password_too_simple_",
+ "password_listed",
+ "backup_method_",
+ "backup_applying_method_",
+ "confirm_app_install_"]
+
+###############################################################################
+# Find used keys in python code #
+###############################################################################
+
+
+def find_expected_string_keys():
+
+ # Try to find :
+ # m18n.n( "foo"
+ # YunohostError("foo"
+ p1 = re.compile(r'm18n\.n\(\s*[\"\'](\w+)[\"\']')
+ p2 = re.compile(r'YunohostError\([\'\"](\w+)[\'\"]')
+
+ python_files = glob.glob("src/yunohost/*.py")
+ python_files.extend(glob.glob("src/yunohost/utils/*.py"))
+ python_files.extend(glob.glob("src/yunohost/data_migrations/*.py"))
+ python_files.extend(glob.glob("data/hooks/diagnosis/*.py"))
+ python_files.append("bin/yunohost")
+
+ for python_file in python_files:
+ content = open(python_file).read()
+ for m in p1.findall(content):
+ if m.endswith("_"):
+ continue
+ yield m
+ for m in p2.findall(content):
+ if m.endswith("_"):
+ continue
+ yield m
+
+ # For each diagnosis, try to find strings like "diagnosis_stuff_foo" (c.f. diagnosis summaries)
+ # Also we expect to have "diagnosis_description_" for each diagnosis
+ p3 = re.compile(r'[\"\'](diagnosis_[a-z]+_\w+)[\"\']')
+ for python_file in glob.glob("data/hooks/diagnosis/*.py"):
+ content = open(python_file).read()
+ for m in p3.findall(content):
+ if m.endswith("_"):
+ # Ignore some name fragments which are actually concatenated with other stuff..
+ continue
+ yield m
+ yield "diagnosis_description_" + os.path.basename(python_file)[:-3].split("-")[-1]
+
+ # For each migration, expect to find "migration_description_"
+ for path in glob.glob("src/yunohost/data_migrations/*.py"):
+ if "__init__" in path:
+ continue
+ yield "migration_description_" + os.path.basename(path)[:-3]
+
+ # For each default service, expect to find "service_description_"
+ for service, info in yaml.safe_load(open("data/templates/yunohost/services.yml")).items():
+ if info is None:
+ continue
+ yield "service_description_" + service
+
+ # For all unit operations, expect to find "log_"
+ # A unit operation is created either using the @is_unit_operation decorator
+ # or using OperationLogger(
+ cmd = "grep -hr '@is_unit_operation' src/yunohost/ -A3 2>/dev/null | grep '^def' | sed -E 's@^def (\\w+)\\(.*@\\1@g'"
+ for funcname in subprocess.check_output(cmd, shell=True).decode("utf-8").strip().split("\n"):
+ yield "log_" + funcname
+
+ p4 = re.compile(r"OperationLogger\([\"\'](\w+)[\"\']")
+ for python_file in python_files:
+ content = open(python_file).read()
+ for m in ("log_" + match for match in p4.findall(content)):
+ yield m
+
+ # Global settings descriptions
+ # Will be on a line like : ("service.ssh.allow_deprecated_dsa_hostkey", {"type": "bool", ...
+ p5 = re.compile(r" \([\"\'](\w[\w\.]+)[\"\'],")
+ content = open("src/yunohost/settings.py").read()
+ for m in ("global_settings_setting_" + s.replace(".", "_") for s in p5.findall(content)):
+ yield m
+
+ # Keys for the actionmap ...
+ for category in yaml.load(open("data/actionsmap/yunohost.yml")).values():
+ if "actions" not in category.keys():
+ continue
+ for action in category["actions"].values():
+ if "arguments" not in action.keys():
+ continue
+ for argument in action["arguments"].values():
+ extra = argument.get("extra")
+ if not extra:
+ continue
+ if "password" in extra:
+ yield extra["password"]
+ if "ask" in extra:
+ yield extra["ask"]
+ if "comment" in extra:
+ yield extra["comment"]
+ if "pattern" in extra:
+ yield extra["pattern"][1]
+ if "help" in extra:
+ yield extra["help"]
+
+ # Hardcoded expected keys ...
+ yield "admin_password" # Not sure that's actually used nowadays...
+
+ for method in ["tar", "copy", "borg", "custom"]:
+ yield "backup_applying_method_%s" % method
+ yield "backup_method_%s_finished" % method
+
+ for level in ["danger", "thirdparty", "warning"]:
+ yield "confirm_app_install_%s" % level
+
+ for errortype in ["bad_status_code", "connection_error", "timeout"]:
+ yield "diagnosis_http_%s" % errortype
+
+ yield "password_listed"
+ for i in [1, 2, 3, 4]:
+ yield "password_too_simple_%s" % i
+
+ checks = ["outgoing_port_25_ok", "ehlo_ok", "fcrdns_ok",
+ "blacklist_ok", "queue_ok", "ehlo_bad_answer",
+ "ehlo_unreachable", "ehlo_bad_answer_details",
+ "ehlo_unreachable_details", ]
+ for check in checks:
+ yield "diagnosis_mail_%s" % check
+
+###############################################################################
+# Load en locale json keys #
+###############################################################################
+
+
+def keys_defined_for_en():
+ return json.loads(open("locales/en.json").read()).keys()
+
+###############################################################################
+# Compare keys used and keys defined #
+###############################################################################
+
+
+expected_string_keys = set(find_expected_string_keys())
+keys_defined = set(keys_defined_for_en())
+
+
+def test_undefined_i18n_keys():
+ undefined_keys = expected_string_keys.difference(keys_defined)
+ undefined_keys = sorted(undefined_keys)
+
+ if undefined_keys:
+ raise Exception("Those i18n keys should be defined in en.json:\n"
+ " - " + "\n - ".join(undefined_keys))
+
+
+def test_unused_i18n_keys():
+
+ unused_keys = keys_defined.difference(expected_string_keys)
+ unused_keys = sorted(unused_keys)
+
+ if unused_keys:
+ raise Exception("Those i18n keys appears unused:\n"
+ " - " + "\n - ".join(unused_keys))
diff --git a/tests/test_translation_format_consistency.py b/tests/test_translation_format_consistency.py
new file mode 100644
index 000000000..81e98f3d5
--- /dev/null
+++ b/tests/test_translation_format_consistency.py
@@ -0,0 +1,45 @@
+import re
+import json
+import glob
+import pytest
+
+# List all locale files (except en.json being the ref)
+locale_folder = "locales/"
+locale_files = glob.glob(locale_folder + "*.json")
+locale_files = [filename.split("/")[-1] for filename in locale_files]
+locale_files.remove("en.json")
+
+reference = json.loads(open(locale_folder + "en.json").read())
+
+
+def find_inconsistencies(locale_file):
+
+ this_locale = json.loads(open(locale_folder + locale_file).read())
+
+ # We iterate over all keys/string in en.json
+ for key, string in reference.items():
+
+ # Ignore check if there's no translation yet for this key
+ if key not in this_locale:
+ continue
+
+ # Then we check that every "{stuff}" (for python's .format())
+ # should also be in the translated string, otherwise the .format
+ # will trigger an exception!
+ subkeys_in_ref = set(k[0] for k in re.findall(r"{(\w+)(:\w)?}", string))
+ subkeys_in_this_locale = set(k[0] for k in re.findall(r"{(\w+)(:\w)?}", this_locale[key]))
+
+ if any(k not in subkeys_in_ref for k in subkeys_in_this_locale):
+ yield """\n
+==========================
+Format inconsistency for string {key} in {locale_file}:"
+en.json -> {string}
+{locale_file} -> {translated_string}
+""".format(key=key, string=string.encode("utf-8"), locale_file=locale_file, translated_string=this_locale[key].encode("utf-8"))
+
+
+@pytest.mark.parametrize('locale_file', locale_files)
+def test_translation_format_consistency(locale_file):
+ inconsistencies = list(find_inconsistencies(locale_file))
+ if inconsistencies:
+ raise Exception(''.join(inconsistencies))