Compare commits

...

No commits in common. "debian/2.1.7" and "dev" have entirely different histories.

163 changed files with 12392 additions and 8446 deletions

35
.github/workflows/autoblack.yml vendored Normal file
View file

@ -0,0 +1,35 @@
name: Check / auto apply Black
on:
push:
branches:
- dev
jobs:
black:
name: Check / auto apply black
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check files using the black formatter
uses: psf/black@stable
id: black
with:
options: "."
continue-on-error: true
- shell: pwsh
id: check_files_changed
run: |
# Diff HEAD with the previous commit
$diff = git diff
$HasDiff = $diff.Length -gt 0
Write-Host "::set-output name=files_changed::$HasDiff"
- name: Create Pull Request
if: steps.check_files_changed.outputs.files_changed == 'true'
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
title: "Format Python code with Black"
commit-message: ":art: Format Python code with Black"
body: |
This pull request uses the [psf/black](https://github.com/psf/black) formatter.
base: ${{ github.head_ref }} # Creates pull request onto pull request or commit branch
branch: actions/black

29
.github/workflows/i18n.yml vendored Normal file
View file

@ -0,0 +1,29 @@
name: Autoreformat locale files
on:
push:
branches:
- dev
jobs:
i18n:
name: Autoreformat locale files
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Apply reformating scripts
id: action_reformat
run: |
python3 test/remove_stale_i18n_strings.py
python3 test/autofix_locale_format.py
python3 test/reformat_locales.py
git diff -w --exit-code
- name: Create Pull Request
if: ${{ failure() }}
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
title: "Reformat locale files"
commit-message: ":robot: Reformat locale files"
body: |
Automatic pull request using the scripts in `test/`
base: ${{ github.head_ref }}
branch: actions/i18nreformat

49
.github/workflows/tox.yml vendored Normal file
View file

@ -0,0 +1,49 @@
name: Tests
on:
push:
branches:
- dev
- bullseye
pull_request:
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.9]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install apt dependencies
run: sudo apt install ldap-utils slapd libsasl2-dev libldap2-dev libssl-dev
- name: Install tox
run: |
python -m pip install --upgrade pip
pip install tox tox-gh-actions
- name: Test with tox
run: tox -e py39-pytest
invalidcode:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.9]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install tox
run: |
python -m pip install --upgrade pip
pip install tox tox-gh-actions
- name: Linter
run: tox -e py39-invalidcode
- name: Mypy
run: tox -e py39-mypy

13
.gitignore vendored
View file

@ -1,10 +1,14 @@
*.py[co]
# Documentation
doc/_build/
# Packages
*.egg
*.egg-info
*.swp
*.swo
*~
dist
build
eggs
@ -29,3 +33,12 @@ pip-log.txt
# Mr Developer
.mr.developer.cfg
# Moulinette
doc/*.json
moulinette/package.py
# track only test namespace
lib/**
!lib/test
data/actionsmap/**
!data/actionsmap/test.yml

70
CONTRIBUTORS.md Normal file
View file

@ -0,0 +1,70 @@
Moulinette contributors
=======================
YunoHost is built and maintained by the YunoHost project community.
Everyone is encouraged to submit issues and changes, and to contribute in other ways -- see https://yunohost.org/contribute to find out how.
--
Initial Moulinette was built by Kload & jerome, for YunoHost v2.
Most of actual Moulinette code was written by jerome, with help of numerous contributors.
Translation is made by a bunch of lovely people all over the world.
We would like to thank anyone who ever helped the YunoHost project <3
Moulinette Contributors
-----------------------
- Kload
- Jérôme Lebleu
- Adrien 'beudbeud' Beudin
- titoko
- Laurent 'Bram' Peuch
- Julien 'ju' Malik
- npze
- lmangani
- Valentin 'zamentur' / 'ljf' Grimaud
- dblugeon
Moulinette Translators
----------------------
If you want to help translation, please visit https://translate.yunohost.org/projects/yunohost/yunohost/
### Dutch
- marut
### English
- Anmol
### French
- Bobo
- Laurent Peuch
- Jean-Baptiste Holcroft
- Jérôme Lebleu
### German
- David Bartke
- Felix Bartels
- Marvin Gärtner
### Hindi
- Anmol
### Portuguese
- frju
### Spanish
- Juanu

662
LICENSE Normal file
View file

@ -0,0 +1,662 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license
for software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are
designed to take away your freedom to share and change the works. By
contrast, our General Public Licenses are intended to guarantee your
freedom to share and change all versions of a program--to make sure it
remains free software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public
License.
"Copyright" also means copyright-like laws that apply to other kinds
of works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further restriction,
you may remove that term. If a license document contains a further
restriction but permits relicensing or conveying under this License, you
may add to a covered work material governed by the terms of that license
document, provided that the further restriction does not survive such
relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have permission
to link or combine any covered work with a work licensed under version 3
of the GNU General Public License into a single combined work, and to
convey the resulting work. The terms of this License will continue to
apply to the part which is the covered work, but the work with which it is
combined will remain governed by version 3 of the GNU General Public
License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may differ
in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero
General Public License "or any later version" applies to it, you have
the option of following the terms and conditions either of that
numbered version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number
of the GNU Affero General Public License, you may choose any version
ever published by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that
proxy's public statement of acceptance of a version permanently
authorizes you to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
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/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<http://www.gnu.org/licenses/>.

46
README.md Normal file
View file

@ -0,0 +1,46 @@
<h1 align="center">Moulinette</h1>
<div align="center">
![Version](https://img.shields.io/github/v/tag/yunohost/moulinette?label=version&sort=semver)
[![Tests status](https://github.com/YunoHost/moulinette/actions/workflows/tox.yml/badge.svg)](https://github.com/YunoHost/moulinette/actions/workflows/tox.yml)
[![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/YunoHost/moulinette.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/YunoHost/moulinette/context:python)
[![GitHub license](https://img.shields.io/github/license/YunoHost/moulinette)](https://github.com/YunoHost/moulinette/blob/dev/LICENSE)
Moulinette is a small Python framework meant to easily create programs with unified CLI and API.
In particular, it is used as a base framework for the YunoHost project.
</div>
Issues
------
- [Please report issues on YunoHost bugtracker](https://github.com/YunoHost/issues).
Overview
--------
Moulinette allows to create a YAML "actionmaps" that describes what commands are available. Moulinette will automatically make these commands available through the CLI and Web API, and will be mapped to a python function. Moulinette also provide some general helpers, for example for logging, i18n, authentication, or common file system operations.
<div align="center"><img src="doc/actionsmap.png" width="700" /></div>
Translation
-----------
You can help translate Moulinette on our [translation platform](https://translate.yunohost.org/engage/yunohost/?utm_source=widget)
<div align="center"><img src="https://translate.yunohost.org/widgets/yunohost/-/moulinette/horizontal-auto.svg" alt="Translation status" /></div>
Developpers
-----------
- You can learn how to get started with developing on YunoHost by reading [this piece of documentation](https://yunohost.org/dev).
- Specific doc for moulinette: https://moulinette.readthedocs.org
- Run tests with:
```
$ pip install tox
$ tox
```

View file

@ -1,161 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import sys
import os
# Either we are in a development environment or not
IN_DEVEL = False
# Either cache has to be used inside the moulinette or not
USE_CACHE = True
# Either the output has to be encoded as a JSON encoded string or not
PRINT_JSON = False
# Level for which loggers will log
LOGGERS_LEVEL = 'INFO'
# Handlers that will be used by loggers
# - file: log to the file LOG_DIR/LOG_FILE
# - console: log to stderr
LOGGERS_HANDLERS = ['file']
# Directory and file to be used by logging
LOG_DIR = '/var/log/yunohost'
LOG_FILE = 'yunohost-cli.log'
# Initialization & helpers functions -----------------------------------
def _die(message, title='Error:'):
"""Print error message and exit"""
try:
from moulinette.interfaces.cli import colorize
except ImportError:
colorize = lambda msg, c: msg
print('%s %s' % (colorize(title, 'red'), message))
sys.exit(1)
def _check_in_devel():
"""Check and load if needed development environment"""
global IN_DEVEL, LOG_DIR
basedir = os.path.abspath('%s/../' % os.path.dirname(__file__))
if os.path.isdir('%s/moulinette' % basedir) and not IN_DEVEL:
# Add base directory to python path
sys.path.insert(0, basedir)
# Update global variables
IN_DEVEL = True
LOG_DIR = '%s/log' % basedir
def _parse_argv():
"""Parse additional arguments and return remaining ones"""
argv = list(sys.argv)
argv.pop(0)
if '--no-cache' in argv:
global USE_CACHE
USE_CACHE = False
argv.remove('--no-cache')
if '--json' in argv:
global PRINT_JSON
PRINT_JSON = True
argv.remove('--json')
if '--debug' in argv:
global LOGGERS_LEVEL
LOGGERS_LEVEL = 'DEBUG'
argv.remove('--debug')
if '--verbose' in argv:
global LOGGERS_HANDLERS
if 'console' not in LOGGERS_HANDLERS:
LOGGERS_HANDLERS.append('console')
argv.remove('--verbose')
return argv
def _init_moulinette():
"""Configure logging and initialize the moulinette"""
from moulinette import init
# Custom logging configuration
logging = {
'version': 1,
'disable_existing_loggers': True,
'formatters': {
'simple': {
'format': '%(relativeCreated)-5d %(levelname)-8s %(name)s - %(message)s'
},
'precise': {
'format': '%(asctime)-15s %(levelname)-8s %(name)s %(funcName)s - %(message)s'
},
},
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'formatter': 'simple',
'stream': 'ext://sys.stderr',
},
'file': {
'class': 'logging.FileHandler',
'formatter': 'precise',
'filename': '%s/%s' % (LOG_DIR, LOG_FILE),
},
},
'loggers': {
'moulinette': {
'level': LOGGERS_LEVEL,
'handlers': LOGGERS_HANDLERS,
},
'yunohost': {
'level': LOGGERS_LEVEL,
'handlers': LOGGERS_HANDLERS,
},
},
}
# Create log directory
if not os.path.isdir(LOG_DIR):
try:
os.makedirs(LOG_DIR, 0750)
except os.error as e:
_die(str(e))
# Initialize moulinette
init(logging_config=logging, _from_source=IN_DEVEL)
def _retrieve_namespaces():
"""Return the list of namespaces to load"""
from moulinette.actionsmap import ActionsMap
ret = ['yunohost']
for n in ActionsMap.get_namespaces():
# Append YunoHost modules
if n.startswith('ynh_'):
ret.append(n)
return ret
# Main action ----------------------------------------------------------
if __name__ == '__main__':
_check_in_devel()
args = _parse_argv()
_init_moulinette()
# Check that YunoHost is installed
if not os.path.isfile('/etc/yunohost/installed') and \
(len(args) < 2 or (args[0] +' '+ args[1] != 'tools postinstall' and \
args[0] +' '+ args[1] != 'backup restore')):
from moulinette.interfaces.cli import get_locale
# Init i18n
m18n.load_namespace('yunohost')
m18n.set_locale(get_locale())
# Print error and exit
_die(m18n.n('yunohost_not_installed'), m18n.g('error'))
# Execute the action
from moulinette import cli
ret = cli(_retrieve_namespaces(), args,
print_json=PRINT_JSON, use_cache=USE_CACHE)
sys.exit(ret)

View file

@ -1,165 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import sys
import os.path
# Either we are in a development environment or not
IN_DEVEL = False
# Either cache has to be used inside the moulinette or not
USE_CACHE = True
# Either WebSocket has to be installed by the moulinette or not
USE_WEBSOCKET = True
# Level for which loggers will log
LOGGERS_LEVEL = 'INFO'
# Handlers that will be used by loggers
# - file: log to the file LOG_DIR/LOG_FILE
# - console: log to stderr
LOGGERS_HANDLERS = ['file']
# Directory and file to be used by logging
LOG_DIR = '/var/log/yunohost'
LOG_FILE = 'yunohost-api.log'
# Initialization & helpers functions -----------------------------------
def _die(message, title='Error:'):
"""Print error message and exit"""
try:
from moulinette.interfaces.cli import colorize
except ImportError:
colorize = lambda msg, c: msg
print('%s %s' % (colorize(title, 'red'), message))
sys.exit(1)
def _check_in_devel():
"""Check and load if needed development environment"""
global IN_DEVEL, LOG_DIR
basedir = os.path.abspath('%s/../' % os.path.dirname(__file__))
if os.path.isdir('%s/moulinette' % basedir) and not IN_DEVEL:
# Add base directory to python path
sys.path.insert(0, basedir)
# Update global variables
IN_DEVEL = True
LOG_DIR = '%s/log' % basedir
def _parse_argv():
"""Parse additional arguments and return remaining ones"""
argv = list(sys.argv)
argv.pop(0)
if '--no-cache' in argv:
global USE_CACHE
USE_CACHE = False
argv.remove('--no-cache')
if '--no-websocket' in argv:
global USE_WEBSOCKET
USE_WEBSOCKET = False
argv.remove('--no-websocket')
if '--debug' in argv:
global LOGGERS_LEVEL
LOGGERS_LEVEL = 'DEBUG'
argv.remove('--debug')
if '--verbose' in argv:
global LOGGERS_HANDLERS
if 'console' not in LOGGERS_HANDLERS:
LOGGERS_HANDLERS.append('console')
argv.remove('--verbose')
return argv
def _init_moulinette():
"""Configure logging and initialize the moulinette"""
from moulinette import init
# Custom logging configuration
logging = {
'version': 1,
'disable_existing_loggers': True,
'formatters': {
'simple': {
'format': '%(relativeCreated)-5d %(levelname)-8s %(name)s - %(message)s'
},
'precise': {
'format': '%(asctime)-15s %(levelname)-8s %(name)s %(funcName)s - %(message)s'
},
},
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'formatter': 'simple',
'stream': 'ext://sys.stderr',
},
'file': {
'class': 'logging.handlers.WatchedFileHandler',
'formatter': 'precise',
'filename': '%s/%s' % (LOG_DIR, LOG_FILE),
},
},
'loggers': {
'moulinette': {
'level': LOGGERS_LEVEL,
'handlers': LOGGERS_HANDLERS,
},
'yunohost': {
'level': LOGGERS_LEVEL,
'handlers': LOGGERS_HANDLERS,
},
},
}
# Create log directory
if not os.path.isdir(LOG_DIR):
try:
os.makedirs(LOG_DIR, 0750)
except os.error as e:
_die(str(e))
# Initialize moulinette
init(logging_config=logging, _from_source=IN_DEVEL)
def _retrieve_namespaces():
"""Return the list of namespaces to load"""
from moulinette.actionsmap import ActionsMap
ret = ['yunohost']
for n in ActionsMap.get_namespaces():
# Append YunoHost modules
if n.startswith('ynh_'):
ret.append(n)
return ret
# Callbacks for additional routes --------------------------------------
def is_installed():
"""
Check whether YunoHost is installed or not
"""
installed = False
if os.path.isfile('/etc/yunohost/installed'):
installed = True
return { 'installed': installed }
# Main action ----------------------------------------------------------
if __name__ == '__main__':
_check_in_devel()
_parse_argv()
_init_moulinette()
from moulinette import (api, MoulinetteError)
try:
# Run the server
api(_retrieve_namespaces(), port=6787,
routes={('GET', '/installed'): is_installed},
use_cache=USE_CACHE, use_websocket=USE_WEBSOCKET)
except MoulinetteError as e:
_die(e.strerror, m18n.g('error'))
sys.exit(0)

File diff suppressed because it is too large Load diff

View file

@ -1,9 +0,0 @@
#!/bin/bash
backup_dir="$1/ldap"
mkdir -p $backup_dir
sudo cp -a /etc/ldap/slapd.conf $backup_dir/
sudo slapcat -l $backup_dir/slapcat.ldif.raw
egrep -v "^entryCSN:" < $backup_dir/slapcat.ldif.raw > $backup_dir/slapcat.ldif
rm -f $backup_dir/slapcat.ldif.raw

View file

@ -1,6 +0,0 @@
#!/bin/bash
backup_dir="$1/ssh"
mkdir -p $backup_dir
sudo cp -a /etc/ssh/. $backup_dir

View file

@ -1,7 +0,0 @@
#!/bin/bash
backup_dir="$1/mysql"
mkdir -p $backup_dir
mysqlpwd=$(sudo cat /etc/yunohost/mysql)
sudo mysqldump -uroot -p"$mysqlpwd" mysql > $backup_dir/mysql.sql

View file

@ -1,6 +0,0 @@
#!/bin/bash
backup_dir="$1/ssowat"
mkdir -p $backup_dir
sudo cp -a /etc/ssowat/. $backup_dir

View file

@ -1,6 +0,0 @@
#!/bin/bash
backup_dir="$1/home"
mkdir -p $backup_dir
sudo rsync -a --exclude='/yunohost*' /home/ $backup_dir/

View file

@ -1,6 +0,0 @@
#!/bin/bash
backup_dir="$1/yunohost"
mkdir -p $backup_dir
sudo cp -a /etc/yunohost/. $backup_dir

View file

@ -1,5 +0,0 @@
#!/bin/bash
backup_dir="$1/mail"
sudo cp -a /var/mail/. $backup_dir

View file

@ -1,7 +0,0 @@
#!/bin/bash
backup_dir="$1/xmpp"
mkdir -p $backup_dir/{etc,var}
sudo cp -a /etc/metronome/. $backup_dir/etc
sudo cp -a /var/lib/metronome/. $backup_dir/var

View file

@ -1,6 +0,0 @@
#!/bin/bash
backup_dir="$1/nginx"
mkdir -p $backup_dir
sudo cp -a /etc/nginx/conf.d/. $backup_dir

View file

@ -1,6 +0,0 @@
#!/bin/bash
backup_dir="$1/cron"
mkdir -p $backup_dir
sudo cp -a /etc/cron.d/yunohost* $backup_dir/

View file

@ -1,38 +0,0 @@
#!/bin/bash
backup_dir="$1/ldap"
if [ -z "$2" ]; 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 bash $(pwd)/$0 $1 sudoed
else
service slapd stop
# Backup old configuration
mv /var/lib/ldap /var/lib/ldap.old
# Recreate new DB folder
mkdir /var/lib/ldap
chown openldap: /var/lib/ldap
chmod go-rwx /var/lib/ldap
# Restore LDAP configuration (just to be sure)
cp -a $backup_dir/slapd.conf /etc/ldap/slapd.conf
# Regenerate the configuration
rm -rf /etc/ldap/slapd.d/*
slaptest -u -f /etc/ldap/slapd.conf -F /etc/ldap/slapd.d
cp -rfp /var/lib/ldap.old/DB_CONFIG /var/lib/ldap
# Import the database
slapadd -l $backup_dir/slapcat.ldif
# Change permissions and restart slapd
chown openldap: /var/lib/ldap/*
service slapd start
rm -rf /var/lib/ldap.old
fi

View file

@ -1,6 +0,0 @@
#!/bin/bash
backup_dir="$1/ssh"
sudo cp -a $backup_dir/. /etc/ssh
sudo service ssh restart

View file

@ -1,7 +0,0 @@
#!/bin/bash
backup_dir="$1/mysql"
mysqlpwd=$(sudo cat /etc/yunohost/mysql)
sudo mysql -uroot -p"$mysqlpwd" mysql < $backup_dir/mysql.sql
sudo mysqladmin flush-privileges -p"$mysqlpwd"

View file

@ -1,5 +0,0 @@
#!/bin/bash
backup_dir="$1/ssowat"
sudo cp -a $backup_dir/. /etc/ssowat

View file

@ -1,5 +0,0 @@
#!/bin/bash
backup_dir="$1/home"
sudo cp -a $backup_dir/. /home

View file

@ -1,11 +0,0 @@
#!/bin/bash
backup_dir="$1/yunohost"
sudo cp -a $backup_dir/. /etc/yunohost
sudo yunohost app ssowatconf
sudo yunohost firewall reload
# Reload interface name
sudo rm /etc/yunohost/interface
sudo apt-get install --reinstall -y yunohost-config-others

View file

@ -1,9 +0,0 @@
#!/bin/bash
backup_dir="$1/mail"
sudo cp -a $backup_dir/. /var/mail
# Restart services to use migrated certs
sudo service postfix restart
sudo service dovecot restart

View file

@ -1,9 +0,0 @@
#!/bin/bash
backup_dir="$1/xmpp"
sudo cp -a $backup_dir/etc/. /etc/metronome
sudo cp -a $backup_dir/var/. /var/lib/metronome
# Restart to apply new conf and certs
sudo service metronome restart

View file

@ -1,8 +0,0 @@
#!/bin/bash
backup_dir="$1/nginx"
sudo cp -a $backup_dir/. /etc/nginx/conf.d
# Restart to use new conf and certs
sudo service nginx restart

View file

@ -1,8 +0,0 @@
#!/bin/bash
backup_dir="$1/cron"
sudo cp -a $backup_dir/. /etc/cron.d
# Restart just in case
sudo service cron restart

View file

@ -1,67 +0,0 @@
#!/bin/bash
if [ ! -d /tmp/yunohost ];
then
mkdir /tmp/yunohost
fi
if [ -f /tmp/yunohost/changelog ];
then
rm /tmp/yunohost/changelog
fi
apt-get update -y > /dev/null 2>&1
if [[ $? != 0 ]];
then
exit 2
else
echo OK > /tmp/yunohost/update_status
fi
# Set $DIRCACHE
eval `/usr/bin/apt-config shell DIRCACHE Dir::Cache`
# get the list of packages which are pending an upgrade
PKGNAMES=`/usr/bin/apt-get -q -y --ignore-hold --allow-unauthenticated -s dist-upgrade | \
/bin/grep ^Inst | /usr/bin/cut -d\ -f2 | /usr/bin/sort`
if [[ $PKGNAMES = "" ]];
then
exit 1
fi
if [ -n "$PKGNAMES" ] ; then
# do the upgrade downloads
/usr/bin/apt-get --ignore-hold -qq -d --allow-unauthenticated --force-yes dist-upgrade > /dev/null
fi
PKGPATH="/${DIRCACHE}archives/"
for PKG in $PKGNAMES ; do
VER=`LC_ALL=C /usr/bin/apt-cache policy $PKG |\
/bin/grep Candidate: | /usr/bin/cut -f 4 -d \ `
OLDVER=`LC_ALL=C /usr/bin/apt-cache policy $PKG |\
/bin/grep Installed: | /usr/bin/cut -f 4 -d \ `
VERFILE=`echo "$VER" | /bin/sed -e "s/:/%3a/g"`
if ls ${PKGPATH}${PKG}_${VERFILE}_*.deb >& /dev/null ; then
DEBS="$DEBS ${PKGPATH}${PKG}_${VERFILE}_*.deb"
fi
echo -e "$PKG $OLDVER -> $VER"
done
MISSING_DEBS=`apt-get -y --ignore-hold --allow-unauthenticated --print-uris dist-upgrade \
| grep "file:" \
| sed "s/'file:\(.*\)' .*/\1/g"`
DEBS=`echo $MISSING_DEBS $DEBS | /usr/bin/sort`
if [[ $DEBS = "" ]];
then
exit 3
else
if [ -x /usr/bin/apt-listchanges ] ; then
/usr/bin/apt-listchanges --which=both -f text $DEBS > /tmp/yunohost/changelog 2>/dev/null
fi
fi

View file

@ -1,10 +0,0 @@
uPnP:
enabled: false
TCP: [22, 25, 53, 80, 443, 465, 587, 993, 5222, 5269]
UDP: [53]
ipv4:
TCP: [22, 25, 53, 80, 443, 465, 587, 993, 5222, 5269]
UDP: [53, 5353]
ipv6:
TCP: [22, 25, 53, 80, 443, 465, 587, 993, 5222, 5269]
UDP: [53, 5353]

View file

@ -1,56 +0,0 @@
parents:
ou=users:
ou: users
objectClass:
- organizationalUnit
- top
ou=domains:
ou: domains
objectClass:
- organizationalUnit
- top
ou=apps:
ou: apps
objectClass:
- organizationalUnit
- top
ou=groups:
ou: groups
objectClass:
- organizationalUnit
- top
ou=sudo:
ou: sudo
objectClass:
- organizationalUnit
- top
children:
cn=admins,ou=groups:
cn: admins
gidNumber: "4001"
memberUid: admin
objectClass:
- posixGroup
- top
cn=sftpusers,ou=groups:
cn: sftpusers
gidNumber: "4002"
memberUid: admin
objectClass:
- posixGroup
- top
cn=admin,ou=sudo:
cn: admin
sudoUser: admin
sudoHost: ALL
sudoCommand: ALL
sudoOption: "!authenticate"
objectClass:
- sudoRole
- top

View file

@ -1,41 +0,0 @@
nginx:
status: service
log: /var/log/nginx
avahi-daemon:
status: service
log: /var/log/daemon.log
bind9:
status: service
log: /var/log/daemon.log
dovecot:
status: service
log: [/var/log/mail.log,/var/log/mail.err]
postfix:
status: service
log: [/var/log/mail.log,/var/log/mail.err]
mysql:
status: service
log: [/var/log/mysql.log,/var/log/mysql.err]
glances:
status: service
ssh:
status: service
log: /var/log/auth.log
metronome:
status: metronomectl status
log: [/var/log/metronome/metronome.log,/var/log/metronome/metronome.err]
slapd:
status: service
log: /var/log/syslog
php5-fpm:
status: service
log: /var/log/php5-fpm.log
yunohost-api:
status: service
log: /var/log/yunohost.log
postgrey:
status: service
log: /var/log/mail.log
amavis:
status: service
log: /var/log/mail.log

View file

@ -1,5 +0,0 @@
/bin/bash
rm /tmp/yunohost/update_status
sudo apt-get upgrade -y > /tmp/yunohost/update_log 2>&1
if [ $(echo $?) = 0 ]; then echo "OK" > /tmp/yunohost/upgrade_status; else echo "NOK" > /tmp/yunohost/upgrade_status; fi
rm /tmp/yunohost/upgrade.run

1579
debian/changelog vendored

File diff suppressed because it is too large Load diff

1
debian/compat vendored
View file

@ -1 +0,0 @@
9

44
debian/control vendored
View file

@ -1,22 +1,26 @@
Source: moulinette-yunohost
Section: net
Priority: extra
Maintainer: Jérôme Lebleu <jerome.lebleu@mailoo.org>
Build-Depends: debhelper (>=8.0.0)
Standards-Version: 3.9.4
Homepage: https://yunohost.org/
Source: moulinette
Section: python
Priority: optional
Maintainer: YunoHost Contributors <contrib@yunohost.org>
Build-Depends: debhelper (>= 9), debhelper-compat (= 13), python3 (>= 3.7), dh-python, python3-setuptools, python3-psutil, python3-all (>= 3.7)
Standards-Version: 3.9.6
Homepage: https://github.com/YunoHost/moulinette
Package: moulinette-yunohost
Package: moulinette
Architecture: all
Depends: moulinette,
python-psutil,
python-requests,
glances,
python-pip,
rubygems,
pyminiupnpc,
dnsutils,
bind9utils,
python-dnspython
Description: YunoHost Python scripts
Python functions to manage a YunoHost instance
Depends: ${misc:Depends}, ${python3:Depends},
python3-yaml,
python3-bottle (>= 0.12),
python3-gevent-websocket,
python3-toml,
python3-psutil,
python3-tz,
python3-prompt-toolkit,
python3-pygments
Breaks: yunohost (<< 4.1)
Description: prototype interfaces with ease in Python
Quickly and easily prototype interfaces for your application.
Each action can be served through an HTTP API and from the
command-line with a single method.
.
Originally designed and written for the YunoHost project.

1011
debian/copyright vendored

File diff suppressed because it is too large Load diff

2
debian/dirs vendored Normal file
View file

@ -0,0 +1,2 @@
usr/lib/moulinette
usr/share/moulinette/actionsmap

6
debian/install vendored
View file

@ -1,6 +0,0 @@
bin/* /usr/bin/
data/actionsmap/* /usr/share/moulinette/actionsmap/
data/hooks/* /usr/share/yunohost/hooks/
data/other/* /usr/share/yunohost/yunohost-config/moulinette/
lib/yunohost/*.py /usr/lib/moulinette/yunohost/
locales/* /usr/lib/moulinette/yunohost/locales/

56
debian/postinst vendored
View file

@ -1,56 +0,0 @@
#!/bin/bash
TMP=/usr/share/yunohost/yunohost-config/moulinette
if [ ! -d /etc/yunohost ];
then
mkdir -p /etc/yunohost
fi
# Allow users to access /media directory
if [ ! -d /etc/skel/media ];
then
mkdir -p /media
ln -s /media /etc/skel/
fi
#Firewall
grep -q "UPNP:" /etc/yunohost/firewall.yml > /dev/null 2>&1
if [[ $? -eq 0 ]] || [ ! -f /etc/yunohost/firewall.yml ];
then
cp $TMP/firewall.yml /etc/yunohost/
fi
# App fetchlist
if [ -f /etc/cron.d/yunohost-applist-yunohost ];
then
sed -i "s/--no-ldap //g" /etc/cron.d/yunohost-applist-yunohost
fi
# Service list
if [ ! -f /etc/yunohost/services.yml ];
then
cp $TMP/services.yml /etc/yunohost/
fi
# Stop old API
ps aux | grep "yunohost.tac" | grep -qv grep
if [[ $? -eq 0 ]];
then
killall twistd
fi
rm -rf /var/cache/moulinette/*
update-rc.d yunohost-api defaults
service yunohost-api restart
# Reload SSOwat conf if obsolete
if [ -f /etc/yunohost/installed ];
then
yunohost firewall upnp | grep -qi "true"
if [[ $? -eq 0 ]];
then
yunohost firewall upnp enable
fi
yunohost app ssowatconf
fi

11
debian/preinst vendored
View file

@ -1,11 +0,0 @@
#!/bin/sh
set -e
if [ -f /etc/init.d/yunohost-api ]; then
service yunohost-api stop
# nc -zv 127.0.0.1 6787 < /dev/null 2> /dev/null
# if [[ ! $? -eq 0 ]];
# then
# exit 1
# fi
fi

10
debian/rules vendored
View file

@ -1,12 +1,6 @@
#!/usr/bin/make -f
# -*- makefile -*-
# Uncomment this to turn on verbose mode.
#export DH_VERBOSE=1
export PYBUILD_NAME=moulinette
%:
dh $@
override_dh_installinit:
dh_installinit --name=yunohost-api
dh_installinit --name=yunohost-firewall
dh $@ --with python3 --buildsystem=pybuild

View file

@ -1,74 +0,0 @@
#! /bin/bash
### BEGIN INIT INFO
# Provides: yunohost-api
# Required-Start: $local_fs $remote_fs $network $syslog
# Required-Stop: $local_fs $remote_fs $network $syslog
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: Start/stop YunoHost API
### END INIT INFO
DAEMON=/usr/bin/yunohost-api
DAEMON_OPTS=""
test -x $DAEMON || exit 0
. /lib/lsb/init-functions
logger "YunoHost API: Start script executed"
case "$1" in
start)
logger "YunoHost API: Starting"
log_daemon_msg "Starting API: YunoHost"
if [[ -f /etc/nginx/conf.d/openresty.conf ]];
then
DAEMON_OPTS="--no-websocket"
fi
start-stop-daemon --start --background --pidfile /var/run/yunohost-api.pid --make-pidfile \
--exec /bin/bash -- -c "$DAEMON $DAEMON_OPTS >> /var/log/yunohost.log 2>&1"
log_end_msg $?
;;
stop)
logger "YunoHost API: Stopping"
log_daemon_msg "Stopping API: YunoHost"
if [ -f /var/run/yunohost-api.pid ]; then
kill `cat /var/run/yunohost-api.pid` > /dev/null 2>&1
rm -f /var/run/yunohost-api.pid
fi
kill `ps aux | grep 'python /usr/bin/yunohost-api' | grep -v grep | awk '{print $2}'` > /dev/null 2>&1
kill `ps aux | grep 'yunohost-api' | grep -v grep | grep -v stop | awk '{print $2}'` > /dev/null 2>&1
log_end_msg 0
;;
restart)
logger "YunoHost API: Restarting"
log_daemon_msg "Restarting API: YunoHost"
if [ -f /var/run/yunohost-api.pid ]; then
kill `cat /var/run/yunohost-api.pid` > /dev/null 2>&1
rm -f /var/run/yunohost-api.pid
fi
kill `ps aux | grep 'python /usr/bin/yunohost-api' | grep -v grep | awk '{print $2}'` > /dev/null 2>&1
kill `ps aux | grep 'yunohost-api' | grep -v grep | grep -v restart | awk '{print $2}'` > /dev/null 2>&1
kill `ps aux | grep 'yunohost.tac' | grep -v grep | awk '{print $2}'` > /dev/null 2>&1
if [[ -f /etc/nginx/conf.d/openresty.conf ]];
then
DAEMON_OPTS="--no-websocket"
fi
start-stop-daemon --start --background --pidfile /var/run/yunohost-api.pid --make-pidfile \
--exec /bin/bash -- -c "$DAEMON $DAEMON_OPTS >> /var/log/yunohost.log 2>&1"
log_end_msg $?
;;
status)
logger "YunoHost API: Running"
log_daemon_msg "YunoHost API: Running"
cat /var/run/yunohost-api.pid > /dev/null 2>&1
log_end_msg $?
;;
*)
logger "YunoHost API: Invalid usage"
echo "Usage: /etc/init.d/yunohost-api {start|stop|restart|status}" >&2
exit 1
;;
esac
exit 0

View file

@ -1,52 +0,0 @@
#! /bin/bash
### BEGIN INIT INFO
# Provides: yunohost-firewall
# Required-Start: $local_fs $remote_fs $network $syslog
# Required-Stop: $local_fs $remote_fs $network $syslog
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: Start/stop YunoHost firewall
### END INIT INFO
DAEMON=/usr/bin/yunohost
DAEMON_OPTS=""
test -x $DAEMON || exit 0
. /lib/lsb/init-functions
logger "YunoHost firewall: Start script executed"
case "$1" in
start)
logger "YunoHost firewall: Starting"
log_daemon_msg "Starting firewall: YunoHost"
/usr/bin/yunohost firewall reload
log_end_msg $?
;;
stop)
logger "YunoHost firewall: Stopping"
log_daemon_msg "Stopping firewall: YunoHost"
/usr/bin/yunohost firewall stop
log_end_msg $?
;;
restart)
logger "YunoHost firewall: Restarting"
log_daemon_msg "Restarting firewall: YunoHost"
/usr/bin/yunohost firewall reload
log_end_msg $?
;;
status)
logger "YunoHost API: Running"
log_daemon_msg "YunoHost API: Running"
iptables -L | grep "Chain INPUT (policy DROP)" > /dev/null 2>&1
log_end_msg $?
;;
*)
logger "YunoHost API: Invalid usage"
echo "Usage: /etc/init.d/yunohost-api {start|stop|restart|status}" >&2
exit 1
;;
esac
exit 0

24
doc/Makefile Normal file
View file

@ -0,0 +1,24 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = python -msphinx
SPHINXPROJ = Moulinette
SOURCEDIR = .
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
ldap_graph:
cat ldapsearch.result | python ldif2dot-0.1.py > ldap_graph.dot
dot -Tpng ldap_graph.dot > ldap_graph.png
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

0
lib/yunohost/__init__.py → doc/_static/.git-dont-delete-me vendored Executable file → Normal file
View file

BIN
doc/actionsmap.odg Normal file

Binary file not shown.

BIN
doc/actionsmap.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

33
doc/actionsmap.rst Normal file
View file

@ -0,0 +1,33 @@
=================================
Role and syntax of the actionsmap
=================================
.. _actionsmap:
Principle
=========
The actionsmap allows to easily define commands and their arguments through
a YAML file. Moulinette will automatically make the command available through
the CLI and Rest API, and will be mapped to a python function.
The illustration below summarizes how it works :
.. image:: actionsmap.png
Format of the actionmap
=======================
General description of categories/subcategories, actions, arguments.
Authentication configuration
----------------------------
Document the `configuration: authenticate: all` LDAP stuff ...
Special options for arguments
-----------------------------
Document `nargs`, `metavar`, `extra: pattern`

197
doc/conf.py Normal file
View file

@ -0,0 +1,197 @@
# -*- coding: utf-8 -*-
#
# Moulinette documentation build configuration file, created by
# sphinx-quickstart on Mon Jul 17 03:57:28 2017.
#
# This file is execfile()d with the current directory set to its
# containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
import os
import sys
sys.path.insert(0, os.path.abspath(".."))
import sys
from mock import Mock as MagicMock
class Mock(MagicMock):
@classmethod
def __getattr__(cls, name):
return MagicMock()
MOCK_MODULES = ["ldap", "ldap.modlist", "ldap.sasl"]
sys.modules.update((mod_name, Mock()) for mod_name in MOCK_MODULES)
# -- General configuration ------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#
# needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
"sphinx.ext.autodoc",
"sphinx.ext.intersphinx",
"sphinx.ext.todo",
"sphinx.ext.viewcode",
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"]
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
# source_suffix = ['.rst', '.md']
source_suffix = ".rst"
# The master toctree document.
master_doc = "index"
# General information about the project.
project = "Moulinette"
copyright = "2017, YunoHost Collective"
author = "YunoHost Collective"
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = "2.6.1"
# The full version, including alpha/beta/rc tags.
release = "2.6.1"
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = None
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This patterns also effect to html_static_path and html_extra_path
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = "sphinx"
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = True
# -- Options for HTML output ----------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = "classic"
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#
# html_theme_options = {}
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ["_static"]
# Custom sidebar templates, must be a dictionary that maps document names
# to template names.
#
# This is required for the alabaster theme
# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars
html_sidebars = {
"**": [
# 'about.html',
# 'navigation.html',
# 'relations.html', # needs 'show_related': True theme option to display
"searchbox.html",
# 'donate.html',
]
}
# -- Options for HTMLHelp output ------------------------------------------
# Output file base name for HTML help builder.
htmlhelp_basename = "Moulinettedoc"
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(
master_doc,
"Moulinette.tex",
"Moulinette Documentation",
"YunoHost Collective",
"manual",
),
]
# -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [(master_doc, "moulinette", "Moulinette Documentation", [author], 1)]
# -- Options for Texinfo output -------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(
master_doc,
"Moulinette",
"Moulinette Documentation",
author,
"Moulinette",
"One line description of project.",
"Miscellaneous",
),
]
# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {"https://docs.python.org/": None}

32
doc/index.rst Normal file
View file

@ -0,0 +1,32 @@
Welcome to Moulinette's documentation!
======================================
`https://github.com/yunohost/Moulinette <Moulinette>`_ is the internal
framework used by `YunoHost <https://yunohost.org>`_ to generate both its web
Rest API and CLI interface.
This framework is aimed at: define once, get both CLI and web Rest API at the
same time, offering easy and fast development and a ubiquitous experience from
different interfaces.
This documentation is mainly aimed for the YunoHost's collective and serves as
a reference.
.. toctree::
:maxdepth: 2
:caption: Contents:
actionsmap
ldap
m18n
utils/filesystem
utils/network
utils/process
utils/text
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

572
doc/ldap.rst Normal file
View file

@ -0,0 +1,572 @@
=================================================
Common LDAP operation (for YunoHost but not only)
=================================================
Moulinette is deeply integrated with LDAP which is used for a series of things
like:
* storing users
* storing domains (for users emails)
* SSO
This page document how to uses it on a programming side in YunoHost.
Getting access to LDAP in a command
===================================
To get access to LDAP you need to authenticate against it, for that you need to
declare your command with requiring authentication in the :ref:`actionsmap` this way:
::
configuration:
authenticate: all
Here is a complete example:
::
somecommand:
category_help: ..
actions:
### somecommand_stuff()
stuff:
action_help: ...
api: GET /...
configuration:
authenticate: all
This will prompt the user for a password in CLI.
If you only need to **read** LDAP (and not modify it, for example by listing
domains), then you prevent the need for a password by using the
:file:`ldap-anonymous` authenticator this way:
::
configuration:
authenticate: all
authenticator: ldap-anonymous
Once you have declared your command like that, your python function will
received the :file:`auth` object as first argument, it will be used to talk to
LDAP, so you need to declare your function this way:
::
def somecommand_stuff(auth, ...):
...
auth in the moulinette code
---------------------------
The :file:`auth` object is an instance of :file:`moulinette.authenticators.ldap.Authenticator` class.
Here its docstring:
.. autoclass:: moulinette.authenticators.ldap.Authenticator
LDAP Schema
===========
This is a generated example of the ldap schema provided by YunoHost
(to generate this graph uses :file:`make ldap_graph`, you'll need graphviz):
.. image:: ldap_graph.png
Reading from LDAP
=================
Reading data from LDAP is done using the :file:`auth` object received as first
argument of the python function. To see how to get this object read the
previous section.
The API looks like this:
::
auth.search(ldap_path, ldap_query)
This will return a list of dictionary with strings as keys and list as values.
You can also specify a list of attributes you want to access from LDAP using a list of string (on only one string apparently):
::
auth.search(ldap_path, ldap_query, ['first_attribute', 'another_attribute'])
For example, if we request the user :file:`alice` with its :file:`homeDirectory`, this would look like this:
::
auth.search('ou=users,dc=yunohost,dc=org', '(&(objectclass=person)(uid=alice))', ['homeDirectory', 'another_attribute'])
And as a result we will get:
::
[{'homeDirectory': ['/home/alice']}]
Notice that even for a single result we get a **list** of result and that every
value in the dictionary is also a **list** of values. This is not really convenient and it would be better to have a real ORM, but for now we are stuck with that.
Apparently if we don't specify the list of attributes it seems that we get all attributes (need to be confirmed).
Here is the method docstring:
.. automethod:: moulinette.authenticators.ldap.Authenticator.search
Users LDAP schema
-----------------
According to :file:`ldapvi` this is the user schema (on YunoHost >3.7):
::
# path: uid=the_unix_username,ou=users,dc=yunohost,dc=org
uid: the_unix_username
objectClass: mailAccount
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: userPermissionYnh
loginShell: /bin/false
uidNumber: 80833
maildrop: the_unix_username # why?
cn: first_name last_name
displayName: first_name last_name
mailuserquota: some_value
gidNumber: 80833
sn: last_name
homeDirectory: /home/the_unix_username
mail: the_unix_username@domain.com
# if the user is the admin he will also have the following mails
mail: root@domain.com
mail: admin@domain.com
mail: webmaster@domain.com
mail: postmaster@domain.com
givenName: first_name
memberOf: cn=the_unix_username,ou=groups,dc=yunohost,dc=org
memberOf: cn=all_users,ou=groups,dc=yunohost,dc=org
permission: cn=main.mail,ou=permission,dc=yunohost,dc=org
permission: cn=main.metronome,ou=permission,dc=yunohost,dc=org
The admin user is a special case that looks like this:
::
# path: cn=admin,dc=yunohost,dc=org
gidNumber: 1007
cn: admin
homeDirectory: /home/admin
objectClass: organizationalRole
objectClass: posixAccount
objectClass: simpleSecurityObject
loginShell: /bin/bash
description: LDAP Administrator
uidNumber: 1007
uid: admin
Other user related schemas:
::
# path: cn=admins,ou=groups,dc=yunohost,dc=org
objectClass: posixGroup
objectClass: top
memberUid: admin
gidNumber: 4001
cn: admins
# path: cn=admin,ou=sudo,dc=yunohost,dc=org
# this entry seems to specify which unix user is a sudoer
cn: admin
sudoCommand: ALL
sudoUser: admin
objectClass: sudoRole
objectClass: top
sudoOption: !authenticate
sudoHost: ALL
Reading users from LDAP
-----------------------
The user schema is located at this path: :file:`ou=users,dc=yunohost,dc=org`
According to already existing code, the queries we uses are:
* :file:`'(&(objectclass=person)(!(uid=root))(!(uid=nobody)))'` to get all users (not that I've never encountered users with :file:`root` or :file:`nobody` uid in the ldap database, those might be there for historical reason)
* :file:`'(&(objectclass=person)(uid=%s))' % username` to access one user data
This give us the 2 following python calls:
::
# all users
auth.search('ou=users,dc=yunohost,dc=org', '(&(objectclass=person)(!(uid=root))(!(uid=nobody)))')
# one user
auth.search('ou=users,dc=yunohost,dc=org', '(&(objectclass=person)(uid=some_username))')
Apparently we could also access one user using the following path (and not query): :file:`uid=user_username,ou=users,dc=yunohost,dc=org` but I haven't test it.
If you want specific attributes look at the general documentation on how to read from LDAP a bit above of this section.
Group LDAP schema
-----------------
According to :file:`ldapvi` this is the user schema (on YunoHost >3.4):
The groups will look like this:
::
dn: cn=the_unix_username,ou=groups,dc=yunohost,dc=org
objectClass: top
objectClass: groupOfNamesYnh
objectClass: posixGroup
gidNumber: 48335
cn: the_unix_username
structuralObjectClass: posixGroup
member: uid=the_unix_username,ou=users,dc=yunohost,dc=org
By default you will find in all case a group named `all_users` which will contains all Yunohost users.
::
# path dn: cn=all_users,ou=groups,dc=yunohost,dc=org
objectClass: posixGroup
objectClass: groupOfNamesYnh
gidNumber: 4002
cn: all_users
structuralObjectClass: posixGroup
permission: cn=main.mail,ou=permission,dc=yunohost,dc=org
permission: cn=main.metronome,ou=permission,dc=yunohost,dc=org
member: uid=the_unix_username,ou=users,dc=yunohost,dc=org
memberUid: the_unix_username
Reading group from LDAP
-----------------------
The group schema is located at this path: :file:`ou=groups,dc=yunohost,dc=org`
The queries we uses are the 2 following python calls:
::
# all groups
auth.search('ou=groups,dc=yunohost,dc=org', '(objectclass=groupOfNamesYnh)')
# one groups
auth.search(base='ou=groups,dc=yunohost,dc=org', filter='cn=' + groupname)
Permission LDAP schema
----------------------
According to :file:`ldapvi` this is the user schema (on YunoHost >3.4):
The permission will look like this:
::
dn: cn=main.mail,ou=permission,dc=yunohost,dc=org
objectClass: posixGroup
objectClass: permissionYnh
gidNumber: 5001
groupPermission: cn=all_users,ou=groups,dc=yunohost,dc=org
cn: main.mail
structuralObjectClass: posixGroup
memberUid: the_unix_username
inheritPermission: uid=the_unix_username,ou=users,dc=yunohost,dc=org
By default you will have a permission for the mail and for metronome. When you install an application a permission also created.
Reading permissions from LDAP
-----------------------------
The permission schema is located at this path: :file:`ou=permission,dc=yunohost,dc=org`
The queries we uses are the 2 following python calls:
::
# For all permission
auth.search('ou=permission,dc=yunohost,dc=org', '(objectclass=permissionYnh)')
# For one permission
auth.search(base='ou=permission,dc=yunohost,dc=org', filter='cn=' + permission_name)
Domain LDAP schema
------------------
According to :file:`ldapvi` this is the domain schema (on YunoHost 2.7):
::
10 virtualdomain=domain.com,ou=domains,dc=yunohost,dc=org
objectClass: mailDomain
objectClass: top
virtualdomain: domain.com
Adding data in LDAP
===================
If you add an object linked to user, group or permission you need run the function `permission_sync_to_user` to keep integrity of permission in LDAP.
Adding stuff in LDAP seems pretty simple, according to existing code it looks like this:
::
auth.add('key=%s,ou=some_location', {'attribute1': 'value', ...})
They weird stuff is the path you need to create. This looks like that for domain and users:
::
# domain
auth.add('virtualdomain=%s,ou=domains' % domain, attr_dict)
# user
auth.add('uid=%s,ou=users' % username, attr_dict)
You need to respect the expected attributes. Refer to the schema for that.
:file:`auth.add` seems to return something false when it failed (None probably)
so you need to check it's return code.
Here is the docstring:
.. automethod:: moulinette.authenticators.ldap.Authenticator.add
Adding user in LDAP
-------------------
Here is how it's done for a new user:
::
auth.add('uid=%s,ou=users' % username, {
'objectClass': ['mailAccount', 'inetOrgPerson', 'posixAccount'],
'givenName': firstname,
'sn': lastname,
'displayName': '%s %s' % (firstname, lastname),
'cn': fullname,
'uid': username,
'mail': mail,
'maildrop': username,
'mailuserquota': mailbox_quota,
'userPassword': user_pwd,
'gidNumber': uid,
'uidNumber': uid,
'homeDirectory': '/home/' + username,
'loginShell': '/bin/false'
})
Adding a domain in LDAP
-----------------------
Here is how it's done for a new domain:
::
auth.add('virtualdomain=%s,ou=domains' % domain, {
'objectClass': ['mailDomain', 'top']
'virtualdomain': domain,
})
Updating LDAP data
==================
If you add an object linked to user, group or permission you need run the function `permission_sync_to_user` to keep integrity of permission in LDAP.
Update a user from LDAP looks like a simplified version of searching. The syntax is the following one:
::
auth.update(exact_path_to_object, {'attribute_to_modify': 'new_value', 'another_attribute_to_modify': 'another_value', ...})
For example this will update a user :file:`loginShell`:
::
auth.update('uid=some_username,ou=users', {'loginShell': '/bin/bash'})
I don't know how this call behave if it fails and what it returns.
Here is the method docstring:
.. automethod:: moulinette.authenticators.ldap.Authenticator.update
Updating a user in LDAP
-------------------------
This is done this way:
::
auth.update('uid=some_username,ou=users', {'attribute': 'new_value', ...})
Refer to the user schema to know which attributes you can modify.
Validate uniqueness
===================
There is a method to validate the uniqueness of some entry that is used during
user creation. It's useful by example to be sure that we have no conflict about email between each user.
Here is how it's used (I don't understand why a path is not provided):
::
# Validate uniqueness of username and mail in LDAP
auth.validate_uniqueness({
'uid': username,
'mail': mail
})
And here is its docstring:
.. automethod:: moulinette.authenticators.ldap.Authenticator.validate_uniqueness
Get conflict
============
Like the last function `validate_uniqueness` but give instead of rising an error this function return which attribute with witch value generate a conflict.
::
# Validate uniqueness of groupname in LDAP
conflict = auth.get_conflict({
'cn': groupname
}, base_dn='ou=groups,dc=yunohost,dc=org')
if conflict:
raise YunohostError('group_name_already_exist', name=groupname)
Remove entries from LDAP
========================
If you add an object linked to user, group or permission you need run the function `permission_sync_to_user` to keep integrity of permission in LDAP.
Remove entries from LDAP is very simple, quite close to adding stuff except you don't need to specify the attributes dict, you just need to entrie path:
::
auth.remove(path)
Here how it looks like for domain and user:
::
# domain
auth.remove('virtualdomain=%s,ou=domains' % domain)
# user
auth.remove('uid=%s,ou=users' % username)
:file:`auth.remove` returns something that evaluate to False when it fails
(:file:`None` ?) so you need to check it returns code.
.. automethod:: moulinette.authenticators.ldap.Authenticator.remove
Reading LDIF file
=================
Reading parsing a ldif to be able to insert in the LDAP database is really easy. Here is how to get the content of a LDIF file
::
from moulinette.utils.filesystem import read_ldif
my_reslut = read_ldif("your_file.ldif")
Note that the main difference of what the auth object return with the search method is that this function return a 2-tuples with the "dn" and the LDAP entry.
=============================
LDAP architecture in Yunohost
=============================
In Yunohost to be able to manage the user and the permission we use 3 parts:
* User object
* Permission object
* Group object
We can see the interaction between these object as this following:
.. image:: Yunohost_LDAP_documentation/LDAP_Liaison_logique_entre_objets.png
As you can see there are link between these 3 objets:
* The first link is between the user and the group. It define which user is in which group. Note that all user has a group with his name. Note that in all Yunohost instance you have a group named `all_users`. In this group you will find all Yunohost users.
* The second link is between the permission and the groups. This link is defined by the administrator. By default all permission are linked to the group `all_users`, so all user will be allowed to access to this permission.
* The third link between the User and the Permission is more technical. It give the possibility to the application to get a list of all user allowed to access to. This link is dynamically generated by core. The function `permission_sync_to_user` in the module `permission` do this work.
The option `force` of the function `permission_sync_to_user` is used when you add the data to LDAP with `slapadd`. `slapadd` update the LDAP database without the LDAP demon process. The advantage of this is that you can bypass the integrity check (like the link between the object by the memberOf overlay). The disadvantage is that the the memberOf overlay wont update anything so if you don't fix the integrity after after to run `slapadd`, the permission in LDAP might be corrupted. Running the function permission_sync_to_user` with the option `force` will do this work to fix all integrity error.
To be able to have an attribute in both is of theses 3 link we use the `memberOf` overlay in LDAP. This following line define the configuration to have these 3 link dynamically updated :
::
# Link user <-> group
#dn: olcOverlay={0}memberof,olcDatabase={1}mdb,cn=config
overlay memberof
memberof-group-oc groupOfNamesYnh
memberof-member-ad member
memberof-memberof-ad memberOf
memberof-dangling error
memberof-refint TRUE
# Link permission <-> groupes
#dn: olcOverlay={1}memberof,olcDatabase={1}mdb,cn=config
overlay memberof
memberof-group-oc permissionYnh
memberof-member-ad groupPermission
memberof-memberof-ad permission
memberof-dangling error
memberof-refint TRUE
# Link permission <-> user
#dn: olcOverlay={2}memberof,olcDatabase={1}mdb,cn=config
overlay memberof
memberof-group-oc permissionYnh
memberof-member-ad inheritPermission
memberof-memberof-ad permission
memberof-dangling error
memberof-refint TRUE
This foolwing example show how will be represented in LDAP as simple concept of permission.
.. image:: Yunohost_LDAP_documentation/LDAP_Representation_logique.png
This schema show what will be in LDAP in these following schema:
.. image:: Yunohost_LDAP_documentation/Schema_LDAP_1.png
.. image:: Yunohost_LDAP_documentation/Schema_LDAP_2.png
=========================================
LDAP integration in Yunohost applications
=========================================
To have a complete integration of LDAP in your application you need to configure LDAP as follow :
::
Host: ldap://localhost
Port: 389
Base DN: dc=yunohost,dc=org
User DN: ou=users,dc=yunohost,dc=org
Group DN: ou=groups,dc=yunohost,dc=org
fiter : (&(objectClass=posixAccount)(permission=cn=YOUR_APP.main,ou=permission,dc=yunohost,dc=org))
LDAP Username: uid
LDAP Email Address: mail
By this your application will get the list of all user allowed to access to your application.

583
doc/ldap_graph.dot Normal file
View file

@ -0,0 +1,583 @@
strict digraph "<stdin>" {
rankdir=LR
fontname = "Helvetica"
fontsize = 10
splines = true
node [
fontname = "Helvetica"
fontsize = 10
shape = "plaintext"
]
edge [
fontname = "Helvetica"
fontsize = 10
]
n0 [label=<
<TABLE BGCOLOR="palegoldenrod" BORDER="0" CELLBORDER="0" CELLSPACING="0">
<TR><TD CELLPADDING="4" ALIGN="CENTER" BGCOLOR="olivedrab4">
<FONT FACE="Helvetica Bold" COLOR="white">
dn: dc=yunohost,dc=org
</FONT></TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">objectClass: dcObject</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">objectClass: organization</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">o: yunohost.org</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">dc: yunohost</FONT>
</TD></TR>
</TABLE>
>]
n1 [label=<
<TABLE BGCOLOR="palegoldenrod" BORDER="0" CELLBORDER="0" CELLSPACING="0">
<TR><TD CELLPADDING="4" ALIGN="CENTER" BGCOLOR="olivedrab4">
<FONT FACE="Helvetica Bold" COLOR="white">
dn: cn=admin,dc=yunohost,dc=org
</FONT></TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">gidNumber: 1007</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">cn: admin</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">homeDirectory: /home/admin</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">objectClass: organizationalRole</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">objectClass: posixAccount</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">objectClass: simpleSecurityObject</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">loginShell: /bin/bash</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">description: LDAP Administrator</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">uidNumber: 1007</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">uid: admin</FONT>
</TD></TR>
</TABLE>
>]
n0->n1
n2 [label=<
<TABLE BGCOLOR="palegoldenrod" BORDER="0" CELLBORDER="0" CELLSPACING="0">
<TR><TD CELLPADDING="4" ALIGN="CENTER" BGCOLOR="olivedrab4">
<FONT FACE="Helvetica Bold" COLOR="white">
dn: ou=users,dc=yunohost,dc=org
</FONT></TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">objectClass: organizationalUnit</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">ou: users</FONT>
</TD></TR>
</TABLE>
>]
n0->n2
n3 [label=<
<TABLE BGCOLOR="palegoldenrod" BORDER="0" CELLBORDER="0" CELLSPACING="0">
<TR><TD CELLPADDING="4" ALIGN="CENTER" BGCOLOR="olivedrab4">
<FONT FACE="Helvetica Bold" COLOR="white">
dn: ou=domains,dc=yunohost,dc=org
</FONT></TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">objectClass: organizationalUnit</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">ou: domains</FONT>
</TD></TR>
</TABLE>
>]
n0->n3
n4 [label=<
<TABLE BGCOLOR="palegoldenrod" BORDER="0" CELLBORDER="0" CELLSPACING="0">
<TR><TD CELLPADDING="4" ALIGN="CENTER" BGCOLOR="olivedrab4">
<FONT FACE="Helvetica Bold" COLOR="white">
dn: ou=groups,dc=yunohost,dc=org
</FONT></TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">objectClass: organizationalUnit</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">ou: groups</FONT>
</TD></TR>
</TABLE>
>]
n0->n4
n5 [label=<
<TABLE BGCOLOR="palegoldenrod" BORDER="0" CELLBORDER="0" CELLSPACING="0">
<TR><TD CELLPADDING="4" ALIGN="CENTER" BGCOLOR="olivedrab4">
<FONT FACE="Helvetica Bold" COLOR="white">
dn: ou=sudo,dc=yunohost,dc=org
</FONT></TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">objectClass: organizationalUnit</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">ou: sudo</FONT>
</TD></TR>
</TABLE>
>]
n0->n5
n6 [label=<
<TABLE BGCOLOR="palegoldenrod" BORDER="0" CELLBORDER="0" CELLSPACING="0">
<TR><TD CELLPADDING="4" ALIGN="CENTER" BGCOLOR="olivedrab4">
<FONT FACE="Helvetica Bold" COLOR="white">
dn: ou=apps,dc=yunohost,dc=org
</FONT></TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">objectClass: organizationalUnit</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">ou: apps</FONT>
</TD></TR>
</TABLE>
>]
n0->n6
n7 [label=<
<TABLE BGCOLOR="palegoldenrod" BORDER="0" CELLBORDER="0" CELLSPACING="0">
<TR><TD CELLPADDING="4" ALIGN="CENTER" BGCOLOR="olivedrab4">
<FONT FACE="Helvetica Bold" COLOR="white">
dn: ou=permission,dc=yunohost,dc=org
</FONT></TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">objectClass: organizationalUnit</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">ou: permission</FONT>
</TD></TR>
</TABLE>
>]
n0->n7
n8 [label=<
<TABLE BGCOLOR="palegoldenrod" BORDER="0" CELLBORDER="0" CELLSPACING="0">
<TR><TD CELLPADDING="4" ALIGN="CENTER" BGCOLOR="olivedrab4">
<FONT FACE="Helvetica Bold" COLOR="white">
dn: cn=all_users,ou=groups,dc=yunohost,dc=org
</FONT></TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">objectClass: posixGroup</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">objectClass: groupOfNamesYnh</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">gidNumber: 4002</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">cn: all_users</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">permission: cn=main.mail,ou=permission,dc=yunohost,dc=org</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">permission: cn=main.metronome,ou=permission,dc=yunohost,dc=org</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">member: uid=alice,ou=users,dc=yunohost,dc=org</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">member: uid=example_admin_user,ou=users,dc=yunohost,dc=org</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">memberUid: alice</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">memberUid: example_admin_user</FONT>
</TD></TR>
</TABLE>
>]
n4->n8
n9 [label=<
<TABLE BGCOLOR="palegoldenrod" BORDER="0" CELLBORDER="0" CELLSPACING="0">
<TR><TD CELLPADDING="4" ALIGN="CENTER" BGCOLOR="olivedrab4">
<FONT FACE="Helvetica Bold" COLOR="white">
dn: cn=admins,ou=groups,dc=yunohost,dc=org
</FONT></TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">objectClass: posixGroup</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">memberUid: admin</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">gidNumber: 4001</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">cn: admins</FONT>
</TD></TR>
</TABLE>
>]
n4->n9
n10 [label=<
<TABLE BGCOLOR="palegoldenrod" BORDER="0" CELLBORDER="0" CELLSPACING="0">
<TR><TD CELLPADDING="4" ALIGN="CENTER" BGCOLOR="olivedrab4">
<FONT FACE="Helvetica Bold" COLOR="white">
dn: cn=admin,ou=sudo,dc=yunohost,dc=org
</FONT></TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">cn: admin</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">sudoCommand: ALL</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">sudoUser: admin</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">objectClass: sudoRole</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">sudoOption: !authenticate</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">sudoHost: ALL</FONT>
</TD></TR>
</TABLE>
>]
n5->n10
n11 [label=<
<TABLE BGCOLOR="palegoldenrod" BORDER="0" CELLBORDER="0" CELLSPACING="0">
<TR><TD CELLPADDING="4" ALIGN="CENTER" BGCOLOR="olivedrab4">
<FONT FACE="Helvetica Bold" COLOR="white">
dn: cn=main.mail,ou=permission,dc=yunohost,dc=org
</FONT></TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">objectClass: posixGroup</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">objectClass: permissionYnh</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">gidNumber: 5001</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">groupPermission: cn=all_users,ou=groups,dc=yunohost,dc=org</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">cn: main.mail</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">memberUid: alice</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">memberUid: example_admin_user</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">inheritPermission: uid=alice,ou=users,dc=yunohost,dc=org</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">inheritPermission: uid=example_admin_user,ou=users,dc=yunohost,dc=org</FONT>
</TD></TR>
</TABLE>
>]
n7->n11
n12 [label=<
<TABLE BGCOLOR="palegoldenrod" BORDER="0" CELLBORDER="0" CELLSPACING="0">
<TR><TD CELLPADDING="4" ALIGN="CENTER" BGCOLOR="olivedrab4">
<FONT FACE="Helvetica Bold" COLOR="white">
dn: cn=main.metronome,ou=permission,dc=yunohost,dc=org
</FONT></TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">objectClass: posixGroup</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">objectClass: permissionYnh</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">gidNumber: 5002</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">groupPermission: cn=all_users,ou=groups,dc=yunohost,dc=org</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">cn: main.metronome</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">memberUid: alice</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">memberUid: example_admin_user</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">inheritPermission: uid=alice,ou=users,dc=yunohost,dc=org</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">inheritPermission: uid=example_admin_user,ou=users,dc=yunohost,dc=org</FONT>
</TD></TR>
</TABLE>
>]
n7->n12
n13 [label=<
<TABLE BGCOLOR="palegoldenrod" BORDER="0" CELLBORDER="0" CELLSPACING="0">
<TR><TD CELLPADDING="4" ALIGN="CENTER" BGCOLOR="olivedrab4">
<FONT FACE="Helvetica Bold" COLOR="white">
dn: virtualdomain=domain.com,ou=domains,dc=yunohost,dc=org
</FONT></TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">objectClass: mailDomain</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">virtualdomain: domain.com</FONT>
</TD></TR>
</TABLE>
>]
n3->n13
n14 [label=<
<TABLE BGCOLOR="palegoldenrod" BORDER="0" CELLBORDER="0" CELLSPACING="0">
<TR><TD CELLPADDING="4" ALIGN="CENTER" BGCOLOR="olivedrab4">
<FONT FACE="Helvetica Bold" COLOR="white">
dn: uid=example_admin_user,ou=users,dc=yunohost,dc=org
</FONT></TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">uid: example_admin_user</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">objectClass: mailAccount</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">objectClass: inetOrgPerson</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">objectClass: posixAccount</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">objectClass: userPermissionYnh</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">loginShell: /bin/false</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">uidNumber: 23431</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">maildrop: example_admin_user</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">cn: firstname lastname</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">displayName: firstname lastname</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">mailuserquota: 0</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">gidNumber: 23431</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">sn: lastname</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">homeDirectory: /home/example_admin_user</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">mail: example_admin_user@domain.com</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">mail: root@domain.com</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">mail: admin@domain.com</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">mail: webmaster@domain.com</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">mail: postmaster@domain.com</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">givenName: firstname</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">permission: cn=main.mail,ou=permission,dc=yunohost,dc=org</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">permission: cn=main.metronome,ou=permission,dc=yunohost,dc=org</FONT>
</TD></TR>
</TABLE>
>]
n2->n14
n15 [label=<
<TABLE BGCOLOR="palegoldenrod" BORDER="0" CELLBORDER="0" CELLSPACING="0">
<TR><TD CELLPADDING="4" ALIGN="CENTER" BGCOLOR="olivedrab4">
<FONT FACE="Helvetica Bold" COLOR="white">
dn: cn=example_admin_user,ou=groups,dc=yunohost,dc=org
</FONT></TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">objectClass: groupOfNamesYnh</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">objectClass: posixGroup</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">gidNumber: 23431</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">cn: example_admin_user</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">member: uid=example_admin_user,ou=users,dc=yunohost,dc=org</FONT>
</TD></TR>
</TABLE>
>]
n4->n15
n16 [label=<
<TABLE BGCOLOR="palegoldenrod" BORDER="0" CELLBORDER="0" CELLSPACING="0">
<TR><TD CELLPADDING="4" ALIGN="CENTER" BGCOLOR="olivedrab4">
<FONT FACE="Helvetica Bold" COLOR="white">
dn: uid=alice,ou=users,dc=yunohost,dc=org
</FONT></TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">uid: alice</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">objectClass: mailAccount</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">objectClass: inetOrgPerson</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">objectClass: posixAccount</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">objectClass: userPermissionYnh</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">loginShell: /bin/false</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">uidNumber: 98803</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">maildrop: alice</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">cn: alice pouet</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">displayName: alice pouet</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">mailuserquota: 0</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">gidNumber: 98803</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">sn: pouet</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">homeDirectory: /home/alice</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">mail: alice@domain.com</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">givenName: alice</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">permission: cn=main.mail,ou=permission,dc=yunohost,dc=org</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">permission: cn=main.metronome,ou=permission,dc=yunohost,dc=org</FONT>
</TD></TR>
</TABLE>
>]
n2->n16
n17 [label=<
<TABLE BGCOLOR="palegoldenrod" BORDER="0" CELLBORDER="0" CELLSPACING="0">
<TR><TD CELLPADDING="4" ALIGN="CENTER" BGCOLOR="olivedrab4">
<FONT FACE="Helvetica Bold" COLOR="white">
dn: cn=alice,ou=groups,dc=yunohost,dc=org
</FONT></TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">objectClass: groupOfNamesYnh</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">objectClass: posixGroup</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">gidNumber: 98803</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">cn: alice</FONT>
</TD></TR>
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">member: uid=alice,ou=users,dc=yunohost,dc=org</FONT>
</TD></TR>
</TABLE>
>]
n4->n17
}

BIN
doc/ldap_graph.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 KiB

196
doc/ldapsearch.result Normal file
View file

@ -0,0 +1,196 @@
# extended LDIF
#
# LDAPv3
# base <dc=yunohost,dc=org> with scope subtree
# filter: (objectclass=*)
# requesting: ALL
#
# yunohost.org
dn: dc=yunohost,dc=org
objectClass: top
objectClass: dcObject
objectClass: organization
o: yunohost.org
dc: yunohost
# admin, yunohost.org
dn: cn=admin,dc=yunohost,dc=org
gidNumber: 1007
cn: admin
homeDirectory: /home/admin
objectClass: organizationalRole
objectClass: posixAccount
objectClass: simpleSecurityObject
loginShell: /bin/bash
description: LDAP Administrator
uidNumber: 1007
uid: admin
# users, yunohost.org
dn: ou=users,dc=yunohost,dc=org
objectClass: organizationalUnit
objectClass: top
ou: users
# domains, yunohost.org
dn: ou=domains,dc=yunohost,dc=org
objectClass: organizationalUnit
objectClass: top
ou: domains
# groups, yunohost.org
dn: ou=groups,dc=yunohost,dc=org
objectClass: organizationalUnit
objectClass: top
ou: groups
# sudo, yunohost.org
dn: ou=sudo,dc=yunohost,dc=org
objectClass: organizationalUnit
objectClass: top
ou: sudo
# apps, yunohost.org
dn: ou=apps,dc=yunohost,dc=org
objectClass: organizationalUnit
objectClass: top
ou: apps
# permission, yunohost.org
dn: ou=permission,dc=yunohost,dc=org
objectClass: organizationalUnit
objectClass: top
ou: permission
# all_users, groups, yunohost.org
dn: cn=all_users,ou=groups,dc=yunohost,dc=org
objectClass: posixGroup
objectClass: groupOfNamesYnh
gidNumber: 4002
cn: all_users
permission: cn=main.mail,ou=permission,dc=yunohost,dc=org
permission: cn=main.metronome,ou=permission,dc=yunohost,dc=org
member: uid=alice,ou=users,dc=yunohost,dc=org
member: uid=example_admin_user,ou=users,dc=yunohost,dc=org
memberUid: alice
memberUid: example_admin_user
# admins, groups, yunohost.org
dn: cn=admins,ou=groups,dc=yunohost,dc=org
objectClass: posixGroup
objectClass: top
memberUid: admin
gidNumber: 4001
cn: admins
# admin, sudo, yunohost.org
dn: cn=admin,ou=sudo,dc=yunohost,dc=org
cn: admin
sudoCommand: ALL
sudoUser: admin
objectClass: sudoRole
objectClass: top
sudoOption: !authenticate
sudoHost: ALL
# main.mail, permission, yunohost.org
dn: cn=main.mail,ou=permission,dc=yunohost,dc=org
objectClass: posixGroup
objectClass: permissionYnh
gidNumber: 5001
groupPermission: cn=all_users,ou=groups,dc=yunohost,dc=org
cn: main.mail
memberUid: alice
memberUid: example_admin_user
inheritPermission: uid=alice,ou=users,dc=yunohost,dc=org
inheritPermission: uid=example_admin_user,ou=users,dc=yunohost,dc=org
# main.metronome, permission, yunohost.org
dn: cn=main.metronome,ou=permission,dc=yunohost,dc=org
objectClass: posixGroup
objectClass: permissionYnh
gidNumber: 5002
groupPermission: cn=all_users,ou=groups,dc=yunohost,dc=org
cn: main.metronome
memberUid: alice
memberUid: example_admin_user
inheritPermission: uid=alice,ou=users,dc=yunohost,dc=org
inheritPermission: uid=example_admin_user,ou=users,dc=yunohost,dc=org
# domain.com, domains, yunohost.org
dn: virtualdomain=domain.com,ou=domains,dc=yunohost,dc=org
objectClass: mailDomain
objectClass: top
virtualdomain: domain.com
# example_admin_user, users, yunohost.org
dn: uid=example_admin_user,ou=users,dc=yunohost,dc=org
uid: example_admin_user
objectClass: mailAccount
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: userPermissionYnh
loginShell: /bin/false
uidNumber: 23431
maildrop: example_admin_user
cn: firstname lastname
displayName: firstname lastname
mailuserquota: 0
gidNumber: 23431
sn: lastname
homeDirectory: /home/example_admin_user
mail: example_admin_user@domain.com
mail: root@domain.com
mail: admin@domain.com
mail: webmaster@domain.com
mail: postmaster@domain.com
givenName: firstname
permission: cn=main.mail,ou=permission,dc=yunohost,dc=org
permission: cn=main.metronome,ou=permission,dc=yunohost,dc=org
# example_admin_user, groups, yunohost.org
dn: cn=example_admin_user,ou=groups,dc=yunohost,dc=org
objectClass: top
objectClass: groupOfNamesYnh
objectClass: posixGroup
gidNumber: 23431
cn: example_admin_user
member: uid=example_admin_user,ou=users,dc=yunohost,dc=org
# alice, users, yunohost.org
dn: uid=alice,ou=users,dc=yunohost,dc=org
uid: alice
objectClass: mailAccount
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: userPermissionYnh
loginShell: /bin/false
uidNumber: 98803
maildrop: alice
cn: alice pouet
displayName: alice pouet
mailuserquota: 0
gidNumber: 98803
sn: pouet
homeDirectory: /home/alice
mail: alice@domain.com
givenName: alice
permission: cn=main.mail,ou=permission,dc=yunohost,dc=org
permission: cn=main.metronome,ou=permission,dc=yunohost,dc=org
# alice, groups, yunohost.org
dn: cn=alice,ou=groups,dc=yunohost,dc=org
objectClass: top
objectClass: groupOfNamesYnh
objectClass: posixGroup
gidNumber: 98803
cn: alice
member: uid=alice,ou=users,dc=yunohost,dc=org
# search result
search: 2
result: 0 Success
# numResponses: 19
# numEntries: 18

213
doc/ldif2dot-0.1.py Normal file
View file

@ -0,0 +1,213 @@
#!/usr/bin/python
# A simple script to convert an LDIF file to DOT format for drawing graphs.
# Copyright 2009 Marcin Owsiany <marcin@owsiany.pl>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
"""A simple script to convert an LDIF file to DOT format for drawing graphs.
So far it only supports the most basic form of entry records: "attrdesc: value".
In particular line continuations, BASE64 or other encodings, change records,
include statements, etc... are not supported.
Example usage, assuming your DIT's base is dc=nodomain:
ldapsearch -x -b 'dc=nodomain' | \\
ldif2dot | \\
dot -o nodomain.png -Nshape=box -Tpng /dev/stdin
"""
import sys
class Element:
"""Represents an LDIF entry."""
def __init__(self):
"""Initializes an object."""
self.attributes = []
def __repr__(self):
"""Returns a basic state dump."""
return "Element" + str(self.index) + str(self.attributes)
def add(self, line):
"""Adds a line of input to the object.
Args:
- line: a string with trailing newline stripped
Returns: True if this object is ready for processing (i.e. a separator
line was passed). Otherwise returns False. Behaviour is undefined if
this method is called after a previous invocation has returned True.
"""
def _valid(line):
return line and not line.startswith("#")
def _interesting(line):
return line != "objectClass: top"
if self.is_valid() and not _valid(line):
return True
if _valid(line) and _interesting(line):
self.attributes.append(line)
return False
def is_valid(self):
"""Indicates whether a valid entry has been read."""
return len(self.attributes) != 0 and self.attributes[0].startswith("dn: ")
def dn(self):
"""Returns the DN for this entry."""
if self.attributes[0].startswith("dn: "):
return self.attributes[0][4:]
else:
return None
def edge(self, dnmap):
"""Returns a text represenation of a grapsh edge.
Finds its parent in provided dnmap (dictionary mapping dn names to
Element objects) and returns a string which declares a DOT edge, or an
empty string, if no parent was found.
"""
dn_components = self.dn().split(",")
for i in range(1, len(dn_components) + 1):
parent = ",".join(dn_components[i:])
if parent in dnmap:
return " n%d->n%d\n" % (dnmap[parent].index, self.index)
return ""
def dot(self, dnmap):
"""Returns a text representation of the node and perhaps its parent edge.
Args:
- dnmap: dictionary mapping dn names to Element objects
"""
def _format(attributes):
result = [TITLE_ENTRY_TEMPALTE % attributes[0]]
for attribute in attributes[1:]:
result.append(ENTRY_TEMPALTE % attribute)
return result
return TABLE_TEMPLATE % (
self.index,
"\n ".join(_format(self.attributes)),
self.edge(dnmap),
)
class Converter:
"""An LDIF to DOT converter."""
def __init__(self):
"""Initializes the object."""
self.elements = []
self.dnmap = {}
def _append(self, e):
"""Adds an element to internal list and map.
First sets it up with an index in the list, for node naming.
"""
index = len(self.elements)
e.index = index
self.elements.append(e)
self.dnmap[e.dn()] = e
def parse(self, file, name):
"""Reads the given file into memory.
Args:
- file: an object which yields text lines on iteration.
- name: a name for the graph
Returns a string containing the graph in DOT format.
"""
e = Element()
for line in file:
line = line.rstrip()
if e.add(line):
self._append(e)
e = Element()
if e.is_valid():
self._append(e)
return BASE_TEMPLATE % (
name,
"".join([e.dot(self.dnmap) for e in self.elements]),
)
BASE_TEMPLATE = """\
strict digraph "%s" {
rankdir=LR
fontname = "Helvetica"
fontsize = 10
splines = true
node [
fontname = "Helvetica"
fontsize = 10
shape = "plaintext"
]
edge [
fontname = "Helvetica"
fontsize = 10
]
%s}
"""
TABLE_TEMPLATE = """\n
n%d [label=<
<TABLE BGCOLOR="palegoldenrod" BORDER="0" CELLBORDER="0" CELLSPACING="0">
%s
</TABLE>
>]
%s
"""
TITLE_ENTRY_TEMPALTE = """\
<TR><TD CELLPADDING="4" ALIGN="CENTER" BGCOLOR="olivedrab4">
<FONT FACE="Helvetica Bold" COLOR="white">
%s
</FONT></TD></TR>\
"""
ENTRY_TEMPALTE = """\
<TR><TD BORDER="0" ALIGN="LEFT">
<FONT FACE="Helvetica Bold">%s</FONT>
</TD></TR>\
"""
if __name__ == "__main__":
if len(sys.argv) > 2:
raise "Expected at most one argument."
elif len(sys.argv) == 2:
name = sys.argv[1]
file = open(sys.argv[1], "r")
else:
name = "<stdin>"
file = sys.stdin
print(Converter().parse(file, name))

38
doc/m18n.rst Normal file
View file

@ -0,0 +1,38 @@
Translations using the m18n object
==================================
The moulinette provides a way to do translations and YunoHost uses it. This is
done via the `m18n` object that you can import this way:
::
from moulinette import m18n
The `m18n` object comes with 2 method:
* `m18n.n` to uses for translations within YunoHost
* `m18n.g` to uses for translations within Moulinette itself
Their API is identical.
Here are example of uses:
::
m18n.n('some_translation_key')
m18n.g('some_translation_key')
m18n.n('some_translation_key', string_formating_argument_1=some_variable)
m18n.g('some_translation_key', string_formating_argument_1=some_variable)
The translation key must be present in `locales/en.json` of either YunoHost
(for `.n`) or moulinette (for `.g`).
Docstring
---------
As a reference, here are the docstrings of the m18n class:
.. autoclass:: moulinette.core.Moulinette18n
.. automethod:: moulinette.core.Moulinette18n.n
.. automethod:: moulinette.core.Moulinette18n.g

36
doc/make.bat Normal file
View file

@ -0,0 +1,36 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=python -msphinx
)
set SOURCEDIR=.
set BUILDDIR=_build
set SPHINXPROJ=Moulinette
if "%1" == "" goto help
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The Sphinx module was not found. Make sure you have Sphinx installed,
echo.then set the SPHINXBUILD environment variable to point to the full
echo.path of the 'sphinx-build' executable. Alternatively you may add the
echo.Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.http://sphinx-doc.org/
exit /b 1
)
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
:end
popd

4
doc/requirements.txt Normal file
View file

@ -0,0 +1,4 @@
sphinx
mock
pyyaml
toml

14
doc/utils/filesystem.rst Normal file
View file

@ -0,0 +1,14 @@
File system operation utils
===========================
.. autofunction:: moulinette.utils.filesystem.read_file
.. autofunction:: moulinette.utils.filesystem.read_json
.. autofunction:: moulinette.utils.filesystem.read_yaml
.. autofunction:: moulinette.utils.filesystem.read_toml
.. autofunction:: moulinette.utils.filesystem.write_to_file
.. autofunction:: moulinette.utils.filesystem.append_to_file
.. autofunction:: moulinette.utils.filesystem.write_to_json
.. autofunction:: moulinette.utils.filesystem.mkdir
.. autofunction:: moulinette.utils.filesystem.chown
.. autofunction:: moulinette.utils.filesystem.chmod
.. autofunction:: moulinette.utils.filesystem.rm

5
doc/utils/network.rst Normal file
View file

@ -0,0 +1,5 @@
Network operation utils
=======================
.. autofunction:: moulinette.utils.network.download_text
.. autofunction:: moulinette.utils.network.download_json

7
doc/utils/process.rst Normal file
View file

@ -0,0 +1,7 @@
Process operation utils
=======================
.. autofunction:: moulinette.utils.process.check_output
.. autofunction:: moulinette.utils.process.call_async_output
.. autofunction:: moulinette.utils.process.run_commands
.. autofunction:: moulinette.utils.process.LogPipe

7
doc/utils/text.rst Normal file
View file

@ -0,0 +1,7 @@
Text operation utils
====================
.. autofunction:: moulinette.utils.text.search
.. autofunction:: moulinette.utils.text.searchf
.. autofunction:: moulinette.utils.text.prependlines
.. autofunction:: moulinette.utils.text.random_ascii

File diff suppressed because it is too large Load diff

View file

@ -1,345 +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_backup.py
Manage backups
"""
import os
import re
import sys
import json
import errno
import time
import shutil
import tarfile
from moulinette.core import MoulinetteError
from moulinette.utils.log import getActionLogger
backup_path = '/home/yunohost.backup'
archives_path = '%s/archives' % backup_path
logger = getActionLogger('yunohost.backup')
def backup_create(name=None, description=None, output_directory=None,
no_compress=False, hooks=[], ignore_apps=False):
"""
Create a backup local archive
Keyword arguments:
name -- Name of the backup archive
description -- Short description of the backup
output_directory -- Output directory for the backup
no_compress -- Do not create an archive file
hooks -- List of backup hooks names to execute
ignore_apps -- Do not backup apps
"""
# TODO: Add a 'clean' argument to clean output directory
from yunohost.hook import hook_add
from yunohost.hook import hook_callback
tmp_dir = None
# Validate and define backup name
timestamp = int(time.time())
if not name:
name = str(timestamp)
if name in backup_list()['archives']:
raise MoulinetteError(errno.EINVAL,
m18n.n('backup_archive_name_exists'))
# Validate additional arguments
if no_compress and not output_directory:
raise MoulinetteError(errno.EINVAL,
m18n.n('backup_output_directory_required'))
if output_directory:
output_directory = os.path.abspath(output_directory)
# Check for forbidden folders
if output_directory.startswith(archives_path) or \
re.match(r'^/(|(bin|boot|dev|etc|lib|root|run|sbin|sys|usr|var)(|/.*))$',
output_directory):
logger.error("forbidden output directory '%'", output_directory)
raise MoulinetteError(errno.EINVAL,
m18n.n('backup_output_directory_forbidden'))
# Create the output directory
if not os.path.isdir(output_directory):
logger.info("creating output directory '%s'", output_directory)
os.makedirs(output_directory, 0750)
# Check that output directory is empty
elif no_compress and os.listdir(output_directory):
logger.error("not empty output directory '%'", output_directory)
raise MoulinetteError(errno.EIO,
m18n.n('backup_output_directory_not_empty'))
# Define temporary directory
if no_compress:
tmp_dir = output_directory
else:
output_directory = archives_path
# Create temporary directory
if not tmp_dir:
tmp_dir = "%s/tmp/%s" % (backup_path, name)
if os.path.isdir(tmp_dir):
logger.warning("temporary directory for backup '%s' already exists",
tmp_dir)
os.system('rm -rf %s' % tmp_dir)
try:
os.mkdir(tmp_dir, 0750)
except OSError:
# Create temporary directory recursively
os.makedirs(tmp_dir, 0750)
os.system('chown -hR admin: %s' % backup_path)
else:
os.system('chown -hR admin: %s' % tmp_dir)
# Initialize backup info
info = {
'description': description or '',
'created_at': timestamp,
'apps': {},
}
# Add apps backup hook
if not ignore_apps:
from yunohost.app import app_info
try:
for app_id in os.listdir('/etc/yunohost/apps'):
hook = '/etc/yunohost/apps/%s/scripts/backup' % app_id
if os.path.isfile(hook):
hook_add(app_id, hook)
# Add app info
i = app_info(app_id)
info['apps'][app_id] = {
'version': i['version'],
}
else:
logger.warning("unable to find app's backup hook '%s'",
hook)
msignals.display(m18n.n('unbackup_app', app_id),
'warning')
except IOError as e:
logger.info("unable to add apps backup hook: %s", str(e))
# Run hooks
msignals.display(m18n.n('backup_running_hooks'))
hook_callback('backup', hooks, args=[tmp_dir])
# Create backup info file
with open("%s/info.json" % tmp_dir, 'w') as f:
f.write(json.dumps(info))
# Create the archive
if not no_compress:
msignals.display(m18n.n('backup_creating_archive'))
archive_file = "%s/%s.tar.gz" % (output_directory, name)
try:
tar = tarfile.open(archive_file, "w:gz")
except:
tar = None
# Create the archives directory and retry
if not os.path.isdir(archives_path):
os.mkdir(archives_path, 0750)
try:
tar = tarfile.open(archive_file, "w:gz")
except:
logger.exception("unable to open the archive '%s' for writing "
"after creating directory '%s'",
archive_file, archives_path)
tar = None
else:
logger.exception("unable to open the archive '%s' for writing",
archive_file)
if tar is None:
raise MoulinetteError(errno.EIO,
m18n.n('backup_archive_open_failed'))
tar.add(tmp_dir, arcname='')
tar.close()
# Copy info file
os.system('mv %s/info.json %s/%s.info.json' %
(tmp_dir, archives_path, name))
# Clean temporary directory
if tmp_dir != output_directory:
os.system('rm -rf %s' % tmp_dir)
msignals.display(m18n.n('backup_complete'), 'success')
def backup_restore(name, hooks=[], ignore_apps=False, force=False):
"""
Restore from a local backup archive
Keyword argument:
name -- Name of the local backup archive
hooks -- List of restoration hooks names to execute
ignore_apps -- Do not restore apps
force -- Force restauration on an already installed system
"""
from yunohost.hook import hook_add
from yunohost.hook import hook_callback
# Retrieve and open the archive
archive_file = backup_info(name)['path']
try:
tar = tarfile.open(archive_file, "r:gz")
except:
logger.exception("unable to open the archive '%s' for reading",
archive_file)
raise MoulinetteError(errno.EIO, m18n.n('backup_archive_open_failed'))
# Check temporary directory
tmp_dir = "%s/tmp/%s" % (backup_path, name)
if os.path.isdir(tmp_dir):
logger.warning("temporary directory for restoration '%s' already exists",
tmp_dir)
os.system('rm -rf %s' % tmp_dir)
# Extract the tarball
msignals.display(m18n.n('backup_extracting_archive'))
tar.extractall(tmp_dir)
tar.close()
# Retrieve backup info
try:
with open("%s/info.json" % tmp_dir, 'r') as f:
info = json.load(f)
except IOError:
logger.error("unable to retrieve backup info from '%s/info.json'",
tmp_dir)
raise MoulinetteError(errno.EIO, m18n.n('backup_invalid_archive'))
else:
logger.info("restoring from backup '%s' created on %s", name,
time.ctime(info['created_at']))
# Retrieve domain from the backup
try:
with open("%s/yunohost/current_host" % tmp_dir, 'r') as f:
domain = f.readline().rstrip()
except IOError:
logger.error("unable to retrieve domain from '%s/yunohost/current_host'",
tmp_dir)
raise MoulinetteError(errno.EIO, m18n.n('backup_invalid_archive'))
# Check if YunoHost is installed
if os.path.isfile('/etc/yunohost/installed'):
msignals.display(m18n.n('yunohost_already_installed'), 'warning')
if not force:
try:
# Ask confirmation for restoring
i = msignals.prompt(m18n.n('restore_confirm_yunohost_installed',
answers='y/N'))
except NotImplemented:
pass
else:
if i == 'y' or i == 'Y':
force = True
if not force:
raise MoulinetteError(errno.EEXIST, m18n.n('restore_failed'))
else:
from yunohost.tools import tools_postinstall
logger.info("executing the post-install...")
tools_postinstall(domain, 'yunohost', True)
# Add apps restore hook
if not ignore_apps:
for app_id in info['apps'].keys():
hook = "/etc/yunohost/apps/%s/scripts/restore" % app_id
if os.path.isfile(hook):
hook_add(app_id, hook)
logger.info("app '%s' will be restored", app_id)
else:
msignals.display(m18n.n('unrestore_app', app_id), 'warning')
# Run hooks
msignals.display(m18n.n('restore_running_hooks'))
hook_callback('restore', hooks, args=[tmp_dir])
# Remove temporary directory
os.system('rm -rf %s' % tmp_dir)
msignals.display(m18n.n('restore_complete'), 'success')
def backup_list():
"""
List available local backup archives
"""
result = []
try:
# Retrieve local archives
archives = os.listdir(archives_path)
except IOError as e:
logging.info("unable to iterate over local archives: %s", str(e))
else:
# Iterate over local archives
for f in archives:
try:
name = f[:f.rindex('.tar.gz')]
except ValueError:
continue
result.append(name)
return { 'archives': result }
def backup_info(name):
"""
Get info about a local backup archive
Keyword arguments:
name -- Name of the local backup archive
"""
archive_file = '%s/%s.tar.gz' % (archives_path, name)
if not os.path.isfile(archive_file):
logger.error("no local backup archive found at '%s'", archive_file)
raise MoulinetteError(errno.EIO, m18n.n('backup_archive_name_unknown'))
info_file = "%s/%s.info.json" % (archives_path, name)
try:
with open(info_file) as f:
# Retrieve backup info
info = json.load(f)
except:
# TODO: Attempt to extract backup info file from tarball
logger.exception("unable to retrive backup info file '%s'",
info_file)
raise MoulinetteError(errno.EIO, m18n.n('backup_invalid_archive'))
return {
'path': archive_file,
'created_at': time.strftime(m18n.n('format_datetime_short'),
time.gmtime(info['created_at'])),
'description': info['description'],
'apps': info['apps'],
}

View file

@ -1,275 +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_domain.py
Manage domains
"""
import os
import sys
import datetime
import re
import shutil
import json
import yaml
import errno
from urllib import urlopen
from moulinette.core import MoulinetteError
def domain_list(auth, filter=None, limit=None, offset=None):
"""
List domains
Keyword argument:
filter -- LDAP filter used to search
offset -- Starting number for domain fetching
limit -- Maximum number of domain fetched
"""
result_list = []
# Set default arguments values
if offset is None:
offset = 0
if limit is None:
limit = 1000
if filter is None:
filter = 'virtualdomain=*'
result = auth.search('ou=domains,dc=yunohost,dc=org', filter, ['virtualdomain'])
if len(result) > offset and limit > 0:
for domain in result[offset:offset+limit]:
result_list.append(domain['virtualdomain'][0])
return { 'domains': result_list }
def domain_add(auth, domain, dyndns=False):
"""
Create a custom domain
Keyword argument:
domain -- Domain name to add
dyndns -- Subscribe to DynDNS
"""
attr_dict = { 'objectClass' : ['mailDomain', 'top'] }
try:
ip = str(urlopen('http://ip.yunohost.org').read())
except IOError:
ip = "127.0.0.1"
now = datetime.datetime.now()
timestamp = str(now.year) + str(now.month) + str(now.day)
if domain in domain_list(auth)['domains']:
raise MoulinetteError(errno.EEXIST, m18n.n('domain_exists'))
# DynDNS domain
if dyndns:
if len(domain.split('.')) < 3:
raise MoulinetteError(errno.EINVAL, m18n.n('domain_dyndns_invalid'))
import requests
from yunohost.dyndns import dyndns_subscribe
try:
r = requests.get('https://dyndns.yunohost.org/domains')
except ConnectionError:
pass
else:
dyndomains = json.loads(r.text)
dyndomain = '.'.join(domain.split('.')[1:])
if dyndomain in dyndomains:
if os.path.exists('/etc/cron.d/yunohost-dyndns'):
raise MoulinetteError(errno.EPERM,
m18n.n('domain_dyndns_already_subscribed'))
dyndns_subscribe(domain=domain)
else:
raise MoulinetteError(errno.EINVAL,
m18n.n('domain_dyndns_root_unknown'))
try:
# Commands
ssl_dir = '/usr/share/yunohost/yunohost-config/ssl/yunoCA'
ssl_domain_path = '/etc/yunohost/certs/%s' % domain
with open('%s/serial' % ssl_dir, 'r') as f:
serial = f.readline().rstrip()
try: os.listdir(ssl_domain_path)
except OSError: os.makedirs(ssl_domain_path)
command_list = [
'cp %s/openssl.cnf %s' % (ssl_dir, ssl_domain_path),
'sed -i "s/yunohost.org/%s/g" %s/openssl.cnf' % (domain, ssl_domain_path),
'openssl req -new -config %s/openssl.cnf -days 3650 -out %s/certs/yunohost_csr.pem -keyout %s/certs/yunohost_key.pem -nodes -batch'
% (ssl_domain_path, ssl_dir, ssl_dir),
'openssl ca -config %s/openssl.cnf -days 3650 -in %s/certs/yunohost_csr.pem -out %s/certs/yunohost_crt.pem -batch'
% (ssl_domain_path, ssl_dir, ssl_dir),
'ln -s /etc/ssl/certs/ca-yunohost_crt.pem %s/ca.pem' % ssl_domain_path,
'cp %s/certs/yunohost_key.pem %s/key.pem' % (ssl_dir, ssl_domain_path),
'cp %s/newcerts/%s.pem %s/crt.pem' % (ssl_dir, serial, ssl_domain_path),
'chmod 755 %s' % ssl_domain_path,
'chmod 640 %s/key.pem' % ssl_domain_path,
'chmod 640 %s/crt.pem' % ssl_domain_path,
'chmod 600 %s/openssl.cnf' % ssl_domain_path,
'chown root:metronome %s/key.pem' % ssl_domain_path,
'chown root:metronome %s/crt.pem' % ssl_domain_path,
'cat %s/ca.pem >> %s/crt.pem' % (ssl_domain_path, ssl_domain_path)
]
for command in command_list:
if os.system(command) != 0:
raise MoulinetteError(errno.EIO,
m18n.n('domain_cert_gen_failed'))
try:
auth.validate_uniqueness({ 'virtualdomain': domain })
except MoulinetteError:
raise MoulinetteError(errno.EEXIST, m18n.n('domain_exists'))
attr_dict['virtualdomain'] = domain
dnsmasq_config_path='/etc/dnsmasq.d'
try:
os.listdir(dnsmasq_config_path)
except OSError:
msignals.display(m18n.n('dnsmasq_isnt_installed'),
'warning')
os.makedirs(dnsmasq_config_path)
try:
with open('%s/%s' % (dnsmasq_config_path, domain)) as f: pass
except IOError as e:
zone_lines = [
'resolv-file=',
'address=/%s/%s' % (domain, ip),
'txt-record=%s,"v=spf1 mx a -all"' % domain,
'mx-host=%s,%s,5' % (domain, domain),
'srv-host=_xmpp-client._tcp.%s,%s,5222,0,5' % (domain, domain),
'srv-host=_xmpp-server._tcp.%s,%s,5269,0,5' % (domain, domain),
'srv-host=_jabber._tcp.%s,%s,5269,0,5' % (domain, domain),
]
with open('%s/%s' % (dnsmasq_config_path, domain), 'w') as zone:
for line in zone_lines:
zone.write(line + '\n')
os.system('service dnsmasq restart')
else:
msignals.display(m18n.n('domain_zone_exists'),
'warning')
# XMPP
try:
with open('/etc/metronome/conf.d/%s.cfg.lua' % domain) as f: pass
except IOError as e:
conf_lines = [
'VirtualHost "%s"' % domain,
' ssl = {',
' key = "%s/key.pem";' % ssl_domain_path,
' certificate = "%s/crt.pem";' % ssl_domain_path,
' }',
' authentication = "ldap2"',
' ldap = {',
' hostname = "localhost",',
' user = {',
' basedn = "ou=users,dc=yunohost,dc=org",',
' filter = "(&(objectClass=posixAccount)(mail=*@%s))",' % domain,
' usernamefield = "mail",',
' namefield = "cn",',
' },',
' }',
]
with open('/etc/metronome/conf.d/%s.cfg.lua' % domain, 'w') as conf:
for line in conf_lines:
conf.write(line + '\n')
os.system('mkdir -p /var/lib/metronome/%s/pep' % domain.replace('.', '%2e'))
os.system('chown -R metronome: /var/lib/metronome/')
os.system('chown -R metronome: /etc/metronome/conf.d/')
os.system('service metronome restart')
# Nginx
os.system('cp /usr/share/yunohost/yunohost-config/nginx/template.conf /etc/nginx/conf.d/%s.conf' % domain)
os.system('mkdir /etc/nginx/conf.d/%s.d/' % domain)
os.system('sed -i s/yunohost.org/%s/g /etc/nginx/conf.d/%s.conf' % (domain, domain))
os.system('service nginx reload')
if not auth.add('virtualdomain=%s,ou=domains' % domain, attr_dict):
raise MoulinetteError(errno.EIO, m18n.n('domain_creation_failed'))
os.system('yunohost app ssowatconf > /dev/null 2>&1')
except:
# Force domain removal silently
try: domain_remove(auth, domain, True)
except: pass
raise
msignals.display(m18n.n('domain_created'), 'success')
def domain_remove(auth, domain, force=False):
"""
Delete domains
Keyword argument:
domain -- Domain to delete
force -- Force the domain removal
"""
if not force and domain not in domain_list(auth)['domains']:
raise MoulinetteError(errno.EINVAL, m18n.n('domain_unknown'))
# 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 MoulinetteError(errno.EPERM,
m18n.n('domain_uninstall_app_first'))
if auth.remove('virtualdomain=' + domain + ',ou=domains') or force:
command_list = [
'rm -rf /etc/yunohost/certs/%s' % domain,
'rm -f /etc/dnsmasq.d/%s' % domain,
'rm -rf /var/lib/metronome/%s' % domain.replace('.', '%2e'),
'rm -f /etc/metronome/conf.d/%s.cfg.lua' % domain,
'rm -rf /etc/nginx/conf.d/%s.d' % domain,
'rm -f /etc/nginx/conf.d/%s.conf' % domain,
]
for command in command_list:
if os.system(command) != 0:
msignals.display(m18n.n('path_removal_failed', command[7:]),
'warning')
else:
raise MoulinetteError(errno.EIO, m18n.n('domain_deletion_failed'))
os.system('yunohost app ssowatconf > /dev/null 2>&1')
os.system('service nginx reload')
os.system('service dnsmasq restart')
os.system('service metronome restart')
msignals.display(m18n.n('domain_deleted'), 'success')

View file

@ -1,224 +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_dyndns.py
Subscribe and Update DynDNS Hosts
"""
import os
import sys
import requests
import json
import glob
import base64
import errno
from moulinette.core import MoulinetteError
def dyndns_subscribe(subscribe_host="dyndns.yunohost.org", domain=None, key=None):
"""
Subscribe to a DynDNS service
Keyword argument:
domain -- Full domain to subscribe with
key -- Public DNS key
subscribe_host -- Dynette HTTP API to subscribe to
"""
if domain is None:
with open('/etc/yunohost/current_host', 'r') as f:
domain = f.readline().rstrip()
# Verify if domain is available
try:
if requests.get('https://%s/test/%s' % (subscribe_host, domain)).status_code != 200:
raise MoulinetteError(errno.EEXIST, m18n.n('dyndns_unavailable'))
except requests.ConnectionError:
raise MoulinetteError(errno.ENETUNREACH, m18n.n('no_internet_connection'))
if key is None:
if len(glob.glob('/etc/yunohost/dyndns/*.key')) == 0:
os.makedirs('/etc/yunohost/dyndns')
msignals.display(m18n.n('dyndns_key_generating'))
os.system('cd /etc/yunohost/dyndns && ' \
'dnssec-keygen -a hmac-md5 -b 128 -n USER %s' % domain)
os.system('chmod 600 /etc/yunohost/dyndns/*.key /etc/yunohost/dyndns/*.private')
key_file = glob.glob('/etc/yunohost/dyndns/*.key')[0]
with open(key_file) as f:
key = f.readline().strip().split(' ')[-1]
# Send subscription
try:
r = requests.post('https://%s/key/%s' % (subscribe_host, base64.b64encode(key)), data={ 'subdomain': domain })
except ConnectionError:
raise MoulinetteError(errno.ENETUNREACH, m18n.n('no_internet_connection'))
if r.status_code != 201:
try: error = json.loads(r.text)['error']
except: error = "Server error"
raise MoulinetteError(errno.EPERM,
m18n.n('dyndns_registration_failed', error))
msignals.display(m18n.n('dyndns_registered'), 'success')
dyndns_installcron()
def dyndns_update(dyn_host="dynhost.yunohost.org", domain=None, key=None, ip=None):
"""
Update IP on DynDNS platform
Keyword argument:
domain -- Full domain to subscribe with
dyn_host -- Dynette DNS server to inform
key -- Public DNS key
ip -- IP address to send
"""
if domain is None:
with open('/etc/yunohost/current_host', 'r') as f:
domain = f.readline().rstrip()
if ip is None:
try:
new_ip = requests.get('http://ip.yunohost.org').text
except ConnectionError:
raise MoulinetteError(errno.ENETUNREACH, m18n.n('no_internet_connection'))
else:
new_ip = ip
try:
with open('/etc/yunohost/dyndns/old_ip', 'r') as f:
old_ip = f.readline().rstrip()
except IOError:
old_ip = '0.0.0.0'
# IPv6
# TODO: Put global IPv6 in the DNS zone instead of ULA
new_ipv6 = None
try:
with open('/etc/yunohost/ipv6') as f:
old_ipv6 = f.readline().rstrip()
except IOError:
old_ipv6 = '0000:0000:0000:0000:0000:0000:0000:0000'
try:
# Get the interface
with open('/etc/yunohost/interface') as f:
interface = f.readline().rstrip()
# Get the ULA
with open('/etc/yunohost/ula') as f:
ula = f.readline().rstrip()
# Get the IPv6 address given by radvd and sanitize it
with open('/proc/net/if_inet6') as f:
plain_ula = ''
for hextet in ula.split(':')[0:3]:
if len(hextet) < 4:
hextet = '0000'+ hextet
hextet = hextet[-4:]
plain_ula = plain_ula + hextet
for line in f.readlines():
if interface in line and plain_ula == line[0:12]:
new_ipv6 = ':'.join([line[0:32][i:i+4] for i in range(0, 32, 4)])
with open('/etc/yunohost/ipv6', 'w+') as f:
f.write(new_ipv6)
break
except IOError:
pass
if new_ipv6 is None:
new_ipv6 = '0000:0000:0000:0000:0000:0000:0000:0000'
if old_ip != new_ip or old_ipv6 != new_ipv6 and new_ipv6 is not None:
host = domain.split('.')[1:]
host = '.'.join(host)
lines = [
'server %s' % dyn_host,
'zone %s' % host,
'update delete %s. A' % domain,
'update delete %s. AAAA' % domain,
'update delete %s. MX' % domain,
'update delete %s. TXT' % domain,
'update delete pubsub.%s. A' % domain,
'update delete muc.%s. A' % domain,
'update delete vjud.%s. A' % domain,
'update delete _xmpp-client._tcp.%s. SRV' % domain,
'update delete _xmpp-server._tcp.%s. SRV' % domain,
'update add %s. 1800 A %s' % (domain, new_ip),
'update add %s. 1800 AAAA %s' % (domain, new_ipv6),
'update add %s. 14400 MX 5 %s.' % (domain, domain),
'update add %s. 14400 TXT "v=spf1 a mx -all"' % domain,
'update add pubsub.%s. 1800 A %s' % (domain, new_ip),
'update add pubsub.%s. 1800 AAAA %s' % (domain, new_ipv6),
'update add muc.%s. 1800 A %s' % (domain, new_ip),
'update add muc.%s. 1800 AAAA %s' % (domain, new_ipv6),
'update add vjud.%s. 1800 A %s' % (domain, new_ip),
'update add vjud.%s. 1800 AAAA %s' % (domain, new_ipv6),
'update add _xmpp-client._tcp.%s. 14400 SRV 0 5 5222 %s.' % (domain, domain),
'update add _xmpp-server._tcp.%s. 14400 SRV 0 5 5269 %s.' % (domain, domain),
'show',
'send'
]
with open('/etc/yunohost/dyndns/zone', 'w') as zone:
for line in lines:
zone.write(line + '\n')
if key is None:
private_key_file = glob.glob('/etc/yunohost/dyndns/*.private')[0]
else:
private_key_file = key
if os.system('/usr/bin/nsupdate -k %s /etc/yunohost/dyndns/zone' % private_key_file) == 0:
msignals.display(m18n.n('dyndns_ip_updated'), 'success')
with open('/etc/yunohost/dyndns/old_ip', 'w') as f:
f.write(new_ip)
else:
os.system('rm /etc/yunohost/dyndns/old_ip > /dev/null 2>&1')
raise MoulinetteError(errno.EPERM,
m18n.n('dyndns_ip_update_failed'))
def dyndns_installcron():
"""
Install IP update cron
"""
with open('/etc/cron.d/yunohost-dyndns', 'w+') as f:
f.write('*/2 * * * * root yunohost dyndns update >> /dev/null\n')
msignals.display(m18n.n('dyndns_cron_installed'), 'success')
def dyndns_removecron():
"""
Remove IP update cron
"""
try:
os.remove("/etc/cron.d/yunohost-dyndns")
except:
raise MoulinetteError(errno.EIO, m18n.n('dyndns_cron_remove_failed'))
msignals.display(m18n.n('dyndns_cron_removed'), 'success')

View file

@ -1,446 +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_firewall.py
Manage firewall rules
"""
import os
import sys
import yaml
import errno
try:
import miniupnpc
except ImportError:
sys.stderr.write('Error: Yunohost CLI Require miniupnpc lib\n')
sys.exit(1)
from moulinette.core import MoulinetteError
from moulinette.utils import process
from moulinette.utils.log import getActionLogger
from moulinette.utils.text import prependlines
firewall_file = '/etc/yunohost/firewall.yml'
upnp_cron_job = '/etc/cron.d/yunohost-firewall-upnp'
logger = getActionLogger('yunohost.firewall')
def firewall_allow(port, protocol='TCP', ipv4_only=False, ipv6_only=False,
no_upnp=False, no_reload=False):
"""
Allow connections on a port
Keyword arguments:
port -- Port or range of ports to open
protocol -- Protocol type to allow (default: TCP)
ipv4_only -- Only add a rule for IPv4 connections
ipv6_only -- Only add a rule for IPv6 connections
no_upnp -- Do not add forwarding of this port with UPnP
no_reload -- Do not reload firewall rules
"""
firewall = firewall_list(raw=True)
# Validate port
if not isinstance(port, int) and ':' not in port:
port = int(port)
# Validate protocols
protocols = ['TCP', 'UDP']
if protocol != 'Both' and protocol in protocols:
protocols = [protocol,]
# Validate IP versions
ipvs = ['ipv4', 'ipv6']
if ipv4_only and not ipv6_only:
ipvs = ['ipv4',]
elif ipv6_only and not ipv4_only:
ipvs = ['ipv6',]
for p in protocols:
# Iterate over IP versions to add port
for i in ipvs:
if port not in firewall[i][p]:
firewall[i][p].append(port)
else:
ipv = "IPv%s" % i[3]
msignals.display(m18n.n('port_already_opened', port, ipv),
'warning')
# Add port forwarding with UPnP
if not no_upnp and port not in firewall['uPnP'][p]:
firewall['uPnP'][p].append(port)
# Update and reload firewall
_update_firewall_file(firewall)
if not no_reload:
return firewall_reload()
def firewall_disallow(port, protocol='TCP', ipv4_only=False, ipv6_only=False,
upnp_only=False, no_reload=False):
"""
Disallow connections on a port
Keyword arguments:
port -- Port or range of ports to close
protocol -- Protocol type to disallow (default: TCP)
ipv4_only -- Only remove the rule for IPv4 connections
ipv6_only -- Only remove the rule for IPv6 connections
upnp_only -- Only remove forwarding of this port with UPnP
no_reload -- Do not reload firewall rules
"""
firewall = firewall_list(raw=True)
# Validate port
if ':' not in port:
port = int(port)
# Validate protocols
protocols = ['TCP', 'UDP']
if protocol != 'Both' and protocol in protocols:
protocols = [protocol,]
# Validate IP versions and UPnP
ipvs = ['ipv4', 'ipv6']
upnp = True
if ipv4_only and ipv6_only:
upnp = True # automatically disallow UPnP
elif ipv4_only:
ipvs = ['ipv4',]
upnp = upnp_only
elif ipv6_only:
ipvs = ['ipv6',]
upnp = upnp_only
elif upnp_only:
ipvs = []
for p in protocols:
# Iterate over IP versions to remove port
for i in ipvs:
if port in firewall[i][p]:
firewall[i][p].remove(port)
else:
ipv = "IPv%s" % i[3]
msignals.display(m18n.n('port_already_closed', port, ipv),
'warning')
# Remove port forwarding with UPnP
if upnp and port in firewall['uPnP'][p]:
firewall['uPnP'][p].remove(port)
# Update and reload firewall
_update_firewall_file(firewall)
if not no_reload:
return firewall_reload()
def firewall_list(raw=False, by_ip_version=False, list_forwarded=False):
"""
List all firewall rules
Keyword arguments:
raw -- Return the complete YAML dict
by_ip_version -- List rules by IP version
list_forwarded -- List forwarded ports with UPnP
"""
with open(firewall_file) as f:
firewall = yaml.load(f)
if raw:
return firewall
# Retrieve all ports for IPv4 and IPv6
ports = {}
for i in ['ipv4', 'ipv6']:
f = firewall[i]
# Combine TCP and UDP ports
ports[i] = sorted(set(f['TCP']) | set(f['UDP']))
if not by_ip_version:
# Combine IPv4 and IPv6 ports
ports = sorted(set(ports['ipv4']) | set(ports['ipv6']))
# Format returned dict
ret = { "opened_ports": ports }
if list_forwarded:
# Combine TCP and UDP forwarded ports
ret['forwarded_ports'] = sorted(
set(firewall['uPnP']['TCP']) | set(firewall['uPnP']['UDP']))
return ret
def firewall_reload():
"""
Reload all firewall rules
"""
from yunohost.hook import hook_callback
reloaded = False
errors = False
# Check if SSH port is allowed
ssh_port = _get_ssh_port()
if ssh_port not in firewall_list()['opened_ports']:
firewall_allow(ssh_port, no_reload=True)
# Retrieve firewall rules and UPnP status
firewall = firewall_list(raw=True)
upnp = firewall_upnp()['enabled']
# IPv4
try:
process.check_output("iptables -L")
except process.CalledProcessError as e:
logger.info('iptables seems to be not available, it outputs:\n%s',
prependlines(e.output.rstrip(), '> '))
msignals.display(m18n.n('iptables_unavailable'), 'info')
else:
rules = [
"iptables -F",
"iptables -X",
"iptables -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT",
]
# Iterate over ports and add rule
for protocol in ['TCP', 'UDP']:
for port in firewall['ipv4'][protocol]:
rules.append("iptables -A INPUT -p %s --dport %s -j ACCEPT" \
% (protocol, process.quote(str(port))))
rules += [
"iptables -A INPUT -i lo -j ACCEPT",
"iptables -A INPUT -p icmp -j ACCEPT",
"iptables -P INPUT DROP",
]
# Execute each rule
if process.check_commands(rules, callback=_on_rule_command_error):
errors = True
reloaded = True
# IPv6
try:
process.check_output("ip6tables -L")
except process.CalledProcessError as e:
logger.info('ip6tables seems to be not available, it outputs:\n%s',
prependlines(e.output.rstrip(), '> '))
msignals.display(m18n.n('ip6tables_unavailable'), 'info')
else:
rules = [
"ip6tables -F",
"ip6tables -X",
"ip6tables -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT",
]
# Iterate over ports and add rule
for protocol in ['TCP', 'UDP']:
for port in firewall['ipv6'][protocol]:
rules.append("ip6tables -A INPUT -p %s --dport %s -j ACCEPT" \
% (protocol, process.quote(str(port))))
rules += [
"ip6tables -A INPUT -i lo -j ACCEPT",
"ip6tables -A INPUT -p icmpv6 -j ACCEPT",
"ip6tables -P INPUT DROP",
]
# Execute each rule
if process.check_commands(rules, callback=_on_rule_command_error):
errors = True
reloaded = True
if not reloaded:
raise MoulinetteError(errno.ESRCH, m18n.n('firewall_reload_failed'))
hook_callback('post_iptable_rules',
args=[upnp, os.path.exists("/proc/net/if_inet6")])
if upnp:
# Refresh port forwarding with UPnP
firewall_upnp(no_refresh=False)
# TODO: Use service_restart
os.system("service fail2ban restart")
if errors:
msignals.display(m18n.n('firewall_rules_cmd_failed'), 'warning')
else:
msignals.display(m18n.n('firewall_reloaded'), 'success')
return firewall_list()
def firewall_upnp(action='status', no_refresh=False):
"""
Manage port forwarding using UPnP
Note: 'reload' action is deprecated and will be removed in the near
future. You should use 'status' instead - which retrieve UPnP status
and automatically refresh port forwarding if 'no_refresh' is False.
Keyword argument:
action -- Action to perform
no_refresh -- Do not refresh port forwarding
"""
firewall = firewall_list(raw=True)
enabled = firewall['uPnP']['enabled']
# Compatibility with previous version
if action == 'reload':
logger.warning("'reload' action is deprecated and will be removed")
try:
# Remove old cron job
os.remove('/etc/cron.d/yunohost-firewall')
except: pass
action = 'status'
no_refresh = False
if action == 'status' and no_refresh:
# Only return current state
return { 'enabled': enabled }
elif action == 'enable' or (enabled and action == 'status'):
# Add cron job
with open(upnp_cron_job, 'w+') as f:
f.write('*/50 * * * * root '
'/usr/bin/yunohost firewall upnp status >>/dev/null\n')
enabled = True
elif action == 'disable' or (not enabled and action == 'status'):
try:
# Remove cron job
os.remove(upnp_cron_job)
except: pass
enabled = False
if action == 'status':
no_refresh = True
else:
raise MoulinetteError(errno.EINVAL, m18n.n('action_invalid', action))
# Refresh port mapping using UPnP
if not no_refresh:
upnpc = miniupnpc.UPnP()
upnpc.discoverdelay = 3000
# Discover UPnP device(s)
logger.debug('discovering UPnP devices...')
nb_dev = upnpc.discover()
logger.debug('found %d UPnP device(s)', int(nb_dev))
if nb_dev < 1:
msignals.display(m18n.n('upnp_dev_not_found'), 'error')
enabled = False
else:
try:
# Select UPnP device
upnpc.selectigd()
except:
logger.exception('unable to select UPnP device')
enabled = False
else:
# Iterate over ports
for protocol in ['TCP', 'UDP']:
for port in firewall['uPnP'][protocol]:
# Clean the mapping of this port
if upnpc.getspecificportmapping(port, protocol):
try:
upnpc.deleteportmapping(port, protocol)
except: pass
if not enabled:
continue
try:
# Add new port mapping
upnpc.addportmapping(port, protocol, upnpc.lanaddr,
port, 'yunohost firewall: port %d' % port, '')
except:
logger.exception('unable to add port %d using UPnP',
port)
enabled = False
if enabled != firewall['uPnP']['enabled']:
firewall['uPnP']['enabled'] = enabled
# Make a backup and update firewall file
os.system("cp {0} {0}.old".format(firewall_file))
with open(firewall_file, 'w') as f:
yaml.safe_dump(firewall, f, default_flow_style=False)
if not no_refresh:
# Display success message if needed
if action == 'enable' and enabled:
msignals.display(m18n.n('upnp_enabled'), 'success')
elif action == 'disable' and not enabled:
msignals.display(m18n.n('upnp_disabled'), 'success')
# Make sure to disable UPnP
elif action != 'disable' and not enabled:
firewall_upnp('disable', no_refresh=True)
if action == 'enable' and not enabled:
raise MoulinetteError(errno.ENXIO, m18n.n('upnp_port_open_failed'))
return { 'enabled': enabled }
def firewall_stop():
"""
Stop iptables and ip6tables
"""
if os.system("iptables -P INPUT ACCEPT") != 0:
raise MoulinetteError(errno.ESRCH, m18n.n('iptables_unavailable'))
os.system("iptables -F")
os.system("iptables -X")
if os.path.exists("/proc/net/if_inet6"):
os.system("ip6tables -P INPUT ACCEPT")
os.system("ip6tables -F")
os.system("ip6tables -X")
if os.path.exists(upnp_cron_job):
firewall_upnp('disable')
def _get_ssh_port(default=22):
"""Return the SSH port to use
Retrieve the SSH port from the sshd_config file or used the default
one if it's not defined.
"""
from moulinette.utils.text import searchf
try:
m = searchf(r'^Port[ \t]+([0-9]+)$',
'/etc/ssh/sshd_config', count=-1)
if m:
return int(m)
except:
pass
return default
def _update_firewall_file(rules):
"""Make a backup and write new rules to firewall file"""
os.system("cp {0} {0}.old".format(firewall_file))
with open(firewall_file, 'w') as f:
yaml.safe_dump(rules, f, default_flow_style=False)
def _on_rule_command_error(returncode, cmd, output):
"""Callback for rules commands error"""
# Log error and continue commands execution
logger.error('"%s" returned non-zero exit status %d:\n%s',
cmd, returncode, prependlines(output.rstrip(), '> '))
return True

View file

@ -1,340 +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_hook.py
Manage hooks
"""
import os
import sys
import re
import json
import errno
import subprocess
from shlex import split as arg_split
from moulinette.core import MoulinetteError
from moulinette.utils.log import getActionLogger
hook_folder = '/usr/share/yunohost/hooks/'
custom_hook_folder = '/etc/yunohost/hooks.d/'
logger = getActionLogger('yunohost.hook')
def hook_add(app, file):
"""
Store hook script to filsystem
Keyword argument:
app -- App to link with
file -- Script to add (/path/priority-file)
"""
path, filename = os.path.split(file)
priority, action = _extract_filename_parts(filename)
try: os.listdir(custom_hook_folder + action)
except OSError: os.makedirs(custom_hook_folder + action)
finalpath = custom_hook_folder + action +'/'+ priority +'-'+ app
os.system('cp %s %s' % (file, finalpath))
os.system('chown -hR admin: %s' % hook_folder)
return { 'hook': finalpath }
def hook_remove(app):
"""
Remove hooks linked to a specific app
Keyword argument:
app -- Scripts related to app will be removed
"""
try:
for action in os.listdir(custom_hook_folder):
for script in os.listdir(custom_hook_folder + action):
if script.endswith(app):
os.remove(custom_hook_folder + action +'/'+ script)
except OSError: pass
def hook_list(action, list_by='name', show_info=False):
"""
List available hooks for an action
Keyword argument:
action -- Action name
list_by -- Property to list hook by
show_info -- Show hook information
"""
result = {}
# Process the property to list hook by
if list_by == 'priority':
if show_info:
def _append_hook(d, priority, name, path):
# Use the priority as key and a dict of hooks names
# with their info as value
value = { 'path': path }
try:
d[priority][name] = value
except KeyError:
d[priority] = { name: value }
else:
def _append_hook(d, priority, name, path):
# Use the priority as key and the name as value
try:
d[priority].add(name)
except KeyError:
d[priority] = set([name])
elif list_by == 'name' or list_by == 'folder':
if show_info:
def _append_hook(d, priority, name, path):
# Use the name as key and a list of hooks info - the
# executed ones with this name - as value
l = d.get(name, list())
for h in l:
# Only one priority for the hook is accepted
if h['priority'] == priority:
# Custom hooks overwrite system ones and they
# are appended at the end - so overwite it
if h['path'] != path:
h['path'] = path
return
l.append({ 'priority': priority, 'path': path })
d[name] = l
else:
if list_by == 'name':
result = set()
def _append_hook(d, priority, name, path):
# Add only the name
d.add(name)
else:
raise MoulinetteError(errno.EINVAL, m18n.n('hook_list_by_invalid'))
def _append_folder(d, folder):
# Iterate over and add hook from a folder
for f in os.listdir(folder + action):
path = '%s%s/%s' % (folder, action, f)
priority, name = _extract_filename_parts(f)
_append_hook(d, priority, name, path)
try:
# Append system hooks first
if list_by == 'folder':
result['system'] = dict() if show_info else set()
_append_folder(result['system'], hook_folder)
else:
_append_folder(result, hook_folder)
except OSError:
logger.debug("system hook folder not found for action '%s' in %s",
action, hook_folder)
try:
# Append custom hooks
if list_by == 'folder':
result['custom'] = dict() if show_info else set()
_append_folder(result['custom'], custom_hook_folder)
else:
_append_folder(result, custom_hook_folder)
except OSError:
logger.debug("custom hook folder not found for action '%s' in %s",
action, custom_hook_folder)
return { 'hooks': result }
def hook_callback(action, hooks=[], args=None):
"""
Execute all scripts binded to an action
Keyword argument:
action -- Action name
hooks -- List of hooks names to execute
args -- Ordered list of arguments to pass to the script
"""
result = { 'succeed': list(), 'failed': list() }
hooks_dict = {}
# Retrieve hooks
if not hooks:
hooks_dict = hook_list(action, list_by='priority',
show_info=True)['hooks']
else:
hooks_names = hook_list(action, list_by='name',
show_info=True)['hooks']
# Iterate over given hooks names list
for n in hooks:
try:
hl = hooks_names[n]
except KeyError:
raise MoulinetteError(errno.EINVAL,
m18n.n('hook_name_unknown', n))
# Iterate over hooks with this name
for h in hl:
# Update hooks dict
d = hooks_dict.get(h['priority'], dict())
d.update({ n: { 'path': h['path'] }})
hooks_dict[h['priority']] = d
if not hooks_dict:
return result
# Format arguments
if args is None:
args = []
elif not isinstance(args, list):
args = [args]
# Iterate over hooks and execute them
for priority in sorted(hooks_dict):
for name, info in iter(hooks_dict[priority].items()):
filename = '%s-%s' % (priority, name)
try:
hook_exec(info['path'], args=args)
except:
logger.exception("error while executing hook '%s'",
info['path'])
result['failed'].append(filename)
else:
result['succeed'].append(filename)
return result
def hook_check(file):
"""
Parse the script file and get arguments
Keyword argument:
file -- File to check
"""
try:
with open(file[:file.index('scripts/')] + 'manifest.json') as f:
manifest = json.loads(str(f.read()))
except:
raise MoulinetteError(errno.EIO, m18n.n('app_manifest_invalid'))
action = file[file.index('scripts/') + 8:]
if 'arguments' in manifest and action in manifest['arguments']:
return manifest['arguments'][action]
else:
return {}
def hook_exec(file, args=None):
"""
Execute hook from a file with arguments
Keyword argument:
file -- Script to execute
args -- Arguments to pass to the script
"""
from moulinette.utils.stream import NonBlockingStreamReader
from yunohost.app import _value_for_locale
if isinstance(args, list):
arg_list = args
else:
required_args = hook_check(file)
if args is None:
args = {}
arg_list = []
for arg in required_args:
if arg['name'] in args:
if 'choices' in arg and args[arg['name']] not in arg['choices']:
raise MoulinetteError(errno.EINVAL,
m18n.n('hook_choice_invalid', args[arg['name']]))
arg_list.append(args[arg['name']])
else:
if os.isatty(1) and 'ask' in arg:
# Retrieve proper ask string
ask_string = _value_for_locale(arg['ask'])
# Append extra strings
if 'choices' in arg:
ask_string += ' ({:s})'.format('|'.join(arg['choices']))
if 'default' in arg:
ask_string += ' (default: {:s})'.format(arg['default'])
input_string = msignals.prompt(ask_string)
if input_string == '' and 'default' in arg:
input_string = arg['default']
arg_list.append(input_string)
elif 'default' in arg:
arg_list.append(arg['default'])
else:
raise MoulinetteError(errno.EINVAL,
m18n.n('hook_argument_missing', arg['name']))
file_path = "./"
if "/" in file and file[0:2] != file_path:
file_path = os.path.dirname(file)
file = file.replace(file_path +"/", "")
#TODO: Allow python script
arg_str = ''
if arg_list:
# Concatenate arguments and escape them with double quotes to prevent
# bash related issue if an argument is empty and is not the last
arg_str = '\\"{:s}\\"'.format('\\" \\"'.join(arg_list))
msignals.display(m18n.n('executing_script'))
p = subprocess.Popen(
arg_split('su - admin -c "cd \\"{:s}\\" && ' \
'/bin/bash -x \\"{:s}\\" {:s}"'.format(
file_path, file, arg_str)),
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
shell=False)
# Wrap and get process ouput
stream = NonBlockingStreamReader(p.stdout)
while True:
line = stream.readline(True, 0.1)
if not line:
# Check if process has terminated
returncode = p.poll()
if returncode is not None:
break
else:
msignals.display(line.rstrip(), 'log')
stream.close()
return returncode
def _extract_filename_parts(filename):
"""Extract hook parts from filename"""
if '-' in filename:
priority, action = filename.split('-', 1)
else:
priority = '50'
action = filename
return priority, action

View file

@ -1,737 +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 errno
import os
import dns.resolver
import cPickle as pickle
from urllib import urlopen
from datetime import datetime, timedelta
from moulinette.core import MoulinetteError
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 MoulinetteError(errno.ENODEV, m18n.n('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:
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 MoulinetteError(errno.EINVAL, m18n.n('unit_unknown', 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
devices = {}
output = subprocess.check_output('ip addr show'.split())
for d in re.split('^(?:[0-9]+: )', output, flags=re.MULTILINE):
d = re.sub('\n[ ]+', ' % ', d) # Replace new lines by %
m = re.match('([a-z]+[0-9]?): (.*)', d) # Extract device name (1) and its addresses (2)
if m:
devices[m.group(1)] = m.group(2)
# Retrieve monitoring for unit(s)
for u in units:
if u == 'check':
result[u] = {}
with open('/etc/yunohost/current_host', 'r') as f:
domain = f.readline().rstrip()
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
elif u == 'infos':
try:
p_ip = str(urlopen('http://ip.yunohost.org').read())
except:
p_ip = 'unknown'
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_ip,
'local_ip': l_ip,
'gateway': gateway,
}
else:
raise MoulinetteError(errno.EINVAL, m18n.n('unit_unknown', 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 MoulinetteError(errno.EINVAL, m18n.n('unit_unknown', u))
if len(units) == 1 and type(result[units[0]]) is not 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 MoulinetteError(errno.EINVAL, m18n.n('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 MoulinetteError(errno.ENODATA, m18n.n('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 MoulinetteError(errno.EINVAL, m18n.n('monitor_period_invalid'))
result = _retrieve_stats(period, date)
if result is False:
raise MoulinetteError(errno.ENOENT,
m18n.n('monitor_stats_file_not_found'))
elif result is None:
raise MoulinetteError(errno.EINVAL,
m18n.n('monitor_stats_period_unavailable'))
return result
def monitor_enable(no_stats=False):
"""
Enable server monitoring
Keyword argument:
no_stats -- Disable 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 not no_stats:
cmd = 'yunohost monitor update-stats'
# day: every 5 min # week: every 1 h # month: every 4 h #
rules = ('*/5 * * * * root %(cmd)s day >> /dev/null\n' + \
'3 * * * * root %(cmd)s week >> /dev/null\n' + \
'6 */4 * * * root %(cmd)s month >> /dev/null') % {'cmd': cmd}
os.system("touch %s" % crontab_path)
os.system("echo '%s' >%s" % (rules, crontab_path))
msignals.display(m18n.n('monitor_enabled'), 'success')
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 MoulinetteError as e:
msignals.display(e.strerror, 'warning')
# Remove crontab
try:
os.remove(crontab_path)
except:
pass
msignals.display(m18n.n('monitor_disabled'), 'success')
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 MoulinetteError(errno.EPERM, m18n.n('monitor_not_enabled'))
raise MoulinetteError(errno.EIO, m18n.n('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

View file

@ -1,342 +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_service.py
Manage services
"""
import yaml
import glob
import subprocess
import errno
import os.path
from moulinette.core import MoulinetteError
def service_add(name, status=None, log=None, runlevel=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
"""
services = _get_services()
if not status:
services[name] = { 'status': 'service' }
else:
services[name] = { 'status': status }
if log is not None:
services[name]['log'] = log
if runlevel is not None:
services[name]['runlevel'] = runlevel
try:
_save_services(services)
except:
raise MoulinetteError(errno.EIO, m18n.n('service_add_failed', name))
msignals.display(m18n.n('service_added'), 'success')
def service_remove(name):
"""
Remove a custom service
Keyword argument:
name -- Service name to remove
"""
services = _get_services()
try:
del services[name]
except KeyError:
raise MoulinetteError(errno.EINVAL, m18n.n('service_unknown', name))
try:
_save_services(services)
except:
raise MoulinetteError(errno.EIO, m18n.n('service_remove_failed', name))
msignals.display(m18n.n('service_removed'), 'success')
def service_start(names):
"""
Start one or more services
Keyword argument:
names -- Services name to start
"""
if isinstance(names, str):
names = [names]
for name in names:
if _run_service_command('start', name):
msignals.display(m18n.n('service_started', name), 'success')
else:
if service_status(name)['status'] != 'running':
raise MoulinetteError(errno.EPERM,
m18n.n('service_start_failed', name))
msignals.display(m18n.n('service_already_started', name))
def service_stop(names):
"""
Stop one or more services
Keyword argument:
name -- Services name to stop
"""
if isinstance(names, str):
names = [names]
for name in names:
if _run_service_command('stop', name):
msignals.display(m18n.n('service_stopped', name), 'success')
else:
if service_status(name)['status'] != 'inactive':
raise MoulinetteError(errno.EPERM,
m18n.n('service_stop_failed', name))
msignals.display(m18n.n('service_already_stopped', name))
def service_enable(names):
"""
Enable one or more services
Keyword argument:
names -- Services name to enable
"""
if isinstance(names, str):
names = [names]
for name in names:
if _run_service_command('enable', name):
msignals.display(m18n.n('service_enabled', name), 'success')
else:
raise MoulinetteError(errno.EPERM,
m18n.n('service_enable_failed', name))
def service_disable(names):
"""
Disable one or more services
Keyword argument:
names -- Services name to disable
"""
if isinstance(names, str):
names = [names]
for name in names:
if _run_service_command('disable', name):
msignals.display(m18n.n('service_disabled', name), 'success')
else:
raise MoulinetteError(errno.EPERM,
m18n.n('service_disable_failed', name))
def service_status(names=[]):
"""
Show status information about one or more services (all by default)
Keyword argument:
names -- Services name to show
"""
services = _get_services()
check_names = True
result = {}
if isinstance(names, str):
names = [names]
elif len(names) == 0:
names = services.keys()
check_names = False
for name in names:
if check_names and name not in services.keys():
raise MoulinetteError(errno.EINVAL,
m18n.n('service_unknown', name))
status = None
if services[name]['status'] == 'service':
status = 'service %s status' % name
else:
status = str(services[name]['status'])
runlevel = 5
if 'runlevel' in services[name].keys():
runlevel = int(services[name]['runlevel'])
result[name] = { 'status': 'unknown', 'loaded': 'unknown' }
# Retrieve service status
try:
ret = subprocess.check_output(status, stderr=subprocess.STDOUT,
shell=True)
except subprocess.CalledProcessError as e:
if 'usage:' in e.output.lower():
msignals.display(m18n.n('service_status_failed', name),
'warning')
else:
result[name]['status'] = 'inactive'
else:
result[name]['status'] = 'running'
# Retrieve service loading
rc_path = glob.glob("/etc/rc%d.d/S[0-9][0-9]%s" % (runlevel, name))
if len(rc_path) == 1 and os.path.islink(rc_path[0]):
result[name]['loaded'] = 'enabled'
elif os.path.isfile("/etc/init.d/%s" % name):
result[name]['loaded'] = 'disabled'
else:
result[name]['loaded'] = 'not-found'
if len(names) == 1:
return result[names[0]]
return result
def service_log(name, number=50):
"""
Log every log files of a service
Keyword argument:
name -- Service name to log
number -- Number of lines to display
"""
services = _get_services()
if name not in services.keys():
raise MoulinetteError(errno.EINVAL, m18n.n('service_unknown', name))
if 'log' in services[name]:
log_list = services[name]['log']
result = {}
if not isinstance(log_list, list):
log_list = [log_list]
for log_path in log_list:
if os.path.isdir(log_path):
for log in [ f for f in os.listdir(log_path) if os.path.isfile(os.path.join(log_path, f)) and f[-4:] == '.log' ]:
result[os.path.join(log_path, log)] = _tail(os.path.join(log_path, log), int(number))
else:
result[log_path] = _tail(log_path, int(number))
else:
raise MoulinetteError(errno.EPERM, m18n.n('service_no_log', name))
return result
def _run_service_command(action, service):
"""
Run services management command (start, stop, enable, disable)
Keyword argument:
action -- Action to perform
service -- Service name
"""
if service not in _get_services().keys():
raise MoulinetteError(errno.EINVAL, m18n.n('service_unknown',
service))
cmd = None
if action in ['start', 'stop']:
cmd = 'service %s %s' % (service, action)
elif action in ['enable', 'disable']:
arg = 'defaults' if action == 'enable' else 'remove'
cmd = 'update-rc.d %s %s' % (service, arg)
else:
raise ValueError("Unknown action '%s'" % action)
try:
ret = subprocess.check_output(cmd.split(), stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as e:
# TODO: Log output?
msignals.display(m18n.n('service_cmd_exec_failed', ' '.join(e.cmd)),
'warning')
return False
return True
def _get_services():
"""
Get a dict of managed services with their parameters
"""
try:
with open('/etc/yunohost/services.yml', 'r') as f:
services = yaml.load(f)
except:
return {}
else:
return services
def _save_services(services):
"""
Save managed services to files
Keyword argument:
services -- A dict of managed services with their parameters
"""
# TODO: Save to custom services.yml
with open('/etc/yunohost/services.yml', 'w') as f:
yaml.safe_dump(services, f, default_flow_style=False)
def _tail(file, n, offset=None):
"""
Reads a n lines from f with an offset of offset lines. The return
value is a tuple in the form ``(lines, has_more)`` where `has_more` is
an indicator that is `True` if there are more lines in the file.
"""
avg_line_length = 74
to_read = n + (offset or 0)
try:
with open(file, 'r') as f:
while 1:
try:
f.seek(-(avg_line_length * to_read), 2)
except IOError:
# woops. apparently file is smaller than what we want
# to step back, go to the beginning instead
f.seek(0)
pos = f.tell()
lines = f.read().splitlines()
if len(lines) >= to_read or pos == 0:
return lines[-to_read:offset and -offset or None]
avg_line_length *= 1.3
except IOError: return []

View file

@ -1,471 +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_tools.py
Specific tools
"""
import os
import sys
import yaml
import re
import getpass
import requests
import json
import errno
import logging
import apt
import apt.progress
from moulinette.core import MoulinetteError
from moulinette.utils.log import getActionLogger
apps_setting_path= '/etc/yunohost/apps/'
logger = getActionLogger('yunohost.tools')
def tools_ldapinit(auth):
"""
YunoHost LDAP initialization
"""
with open('/usr/share/yunohost/yunohost-config/moulinette/ldap_scheme.yml') as f:
ldap_map = yaml.load(f)
for rdn, attr_dict in ldap_map['parents'].items():
try: auth.add(rdn, attr_dict)
except: pass
for rdn, attr_dict in ldap_map['children'].items():
try: auth.add(rdn, attr_dict)
except: pass
admin_dict = {
'cn': 'admin',
'uid': 'admin',
'description': 'LDAP Administrator',
'gidNumber': '1007',
'uidNumber': '1007',
'homeDirectory': '/home/admin',
'loginShell': '/bin/bash',
'objectClass': ['organizationalRole', 'posixAccount', 'simpleSecurityObject'],
'userPassword': 'yunohost'
}
auth.update('cn=admin', admin_dict)
msignals.display(m18n.n('ldap_initialized'), 'success')
def tools_adminpw(auth, new_password):
"""
Change admin password
Keyword argument:
new_password
"""
try:
auth.con.passwd_s('cn=admin,dc=yunohost,dc=org', None, new_password)
except:
logger.exception('unable to change admin password')
raise MoulinetteError(errno.EPERM,
m18n.n('admin_password_change_failed'))
else:
msignals.display(m18n.n('admin_password_changed'), 'success')
def tools_maindomain(auth, old_domain=None, new_domain=None, dyndns=False):
"""
Main domain change tool
Keyword argument:
new_domain
old_domain
"""
from yunohost.domain import domain_add, domain_list
from yunohost.dyndns import dyndns_subscribe
if not old_domain:
with open('/etc/yunohost/current_host', 'r') as f:
old_domain = f.readline().rstrip()
if not new_domain:
return { 'current_main_domain': old_domain }
if not new_domain:
raise MoulinetteError(errno.EINVAL, m18n.n('new_domain_required'))
if new_domain not in domain_list(auth)['domains']:
domain_add(auth, new_domain)
config_files = [
'/etc/postfix/main.cf',
'/etc/metronome/metronome.cfg.lua',
'/etc/dovecot/dovecot.conf',
'/usr/share/yunohost/yunohost-config/others/startup',
'/etc/amavis/conf.d/05-node_id',
'/etc/amavis/conf.d/50-user'
]
config_dir = []
for dir in config_dir:
for file in os.listdir(dir):
config_files.append(dir + '/' + file)
for file in config_files:
with open(file, "r") as sources:
lines = sources.readlines()
with open(file, "w") as sources:
for line in lines:
sources.write(re.sub(r''+ old_domain +'', new_domain, line))
## Update DNS zone file for old and new domains
main_subdomains = ['pubsub', 'muc', 'vjud']
try:
with open('/var/lib/bind/%s.zone' % old_domain, 'r') as f:
old_zone = f.read()
except IOError:
pass
else:
# Remove unneeded subdomains entries
for sub in main_subdomains:
old_zone = re.sub(
r'^({sub}.{domain}.|{sub})[\ \t]+(IN).*$[\n]?'.format(
sub=sub, domain=old_domain),
'', old_zone, 1, re.MULTILINE)
with open('/var/lib/bind/%s.zone' % old_domain, 'w') as f:
f.write(old_zone)
try:
with open('/var/lib/bind/%s.zone' % new_domain, 'r') as f:
new_zone = f.read()
except IOError:
msignals.display(m18n.n('domain_zone_not_found', new_domain), 'warning')
else:
# Add main subdomains entries
for sub in main_subdomains:
new_zone += '{sub} IN CNAME {domain}.\n'.format(
sub=sub, domain=new_domain)
with open('/var/lib/bind/%s.zone' % new_domain, 'w') as f:
f.write(new_zone)
os.system('rm /etc/ssl/private/yunohost_key.pem')
os.system('rm /etc/ssl/certs/yunohost_crt.pem')
command_list = [
'rm -f /etc/nginx/conf.d/%s.d/yunohost_local.conf' % old_domain,
'cp /usr/share/yunohost/yunohost-config/nginx/yunohost_local.conf /etc/nginx/conf.d/%s.d/' % new_domain,
'ln -s /etc/yunohost/certs/%s/key.pem /etc/ssl/private/yunohost_key.pem' % new_domain,
'ln -s /etc/yunohost/certs/%s/crt.pem /etc/ssl/certs/yunohost_crt.pem' % new_domain,
'echo %s > /etc/yunohost/current_host' % new_domain,
'service metronome restart',
'service postfix restart',
'service dovecot restart',
'service amavis restart',
'service nginx restart',
]
for command in command_list:
if os.system(command) != 0:
raise MoulinetteError(errno.EPERM,
m18n.n('maindomain_change_failed'))
if dyndns and len(new_domain.split('.')) >= 3:
try:
r = requests.get('https://dyndns.yunohost.org/domains')
except requests.ConnectionError:
pass
else:
dyndomains = json.loads(r.text)
dyndomain = '.'.join(new_domain.split('.')[1:])
if dyndomain in dyndomains:
dyndns_subscribe(domain=new_domain)
msignals.display(m18n.n('maindomain_changed'), 'success')
def tools_postinstall(domain, password, ignore_dyndns=False):
"""
YunoHost post-install
Keyword argument:
domain -- YunoHost main domain
ignore_dyndns -- Do not subscribe domain to a DynDNS service
password -- YunoHost admin password
"""
from moulinette.core import init_authenticator
from yunohost.app import app_ssowatconf
from yunohost.firewall import firewall_upnp, firewall_reload
dyndns = not ignore_dyndns
try:
with open('/etc/yunohost/installed') as f: pass
except IOError:
msignals.display(m18n.n('yunohost_installing'))
else:
raise MoulinetteError(errno.EPERM, m18n.n('yunohost_already_installed'))
if len(domain.split('.')) >= 3 and not ignore_dyndns:
try:
r = requests.get('https://dyndns.yunohost.org/domains')
except requests.ConnectionError:
pass
else:
dyndomains = json.loads(r.text)
dyndomain = '.'.join(domain.split('.')[1:])
if dyndomain in dyndomains:
if requests.get('https://dyndns.yunohost.org/test/%s' % domain).status_code == 200:
dyndns=True
else:
raise MoulinetteError(errno.EEXIST,
m18n.n('dyndns_unavailable'))
# Create required folders
folders_to_create = [
'/etc/yunohost/apps',
'/etc/yunohost/certs',
'/var/cache/yunohost/repo',
'/home/yunohost.backup',
'/home/yunohost.app'
]
for folder in folders_to_create:
try: os.listdir(folder)
except OSError: os.makedirs(folder)
# Change folders permissions
os.system('chmod 755 /home/yunohost.app')
# Set hostname to avoid amavis bug
if os.system('hostname -d') != 0:
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 IOError:
ssowat_conf = {}
if 'redirected_urls' not in ssowat_conf:
ssowat_conf['redirected_urls'] = {}
ssowat_conf['redirected_urls']['/'] = domain +'/yunohost/admin'
with open('/etc/ssowat/conf.json.persistent', 'w+') as f:
json.dump(ssowat_conf, f, sort_keys=True, indent=4)
os.system('chmod 644 /etc/ssowat/conf.json.persistent')
# Create SSL CA
ssl_dir = '/usr/share/yunohost/yunohost-config/ssl/yunoCA'
command_list = [
'echo "01" > %s/serial' % ssl_dir,
'rm %s/index.txt' % ssl_dir,
'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),
'cp %s/ca/cacert.pem /etc/ssl/certs/ca-yunohost_crt.pem' % ssl_dir,
'update-ca-certificates'
]
for command in command_list:
if os.system(command) != 0:
raise MoulinetteError(errno.EPERM,
m18n.n('yunohost_ca_creation_failed'))
# Instantiate LDAP Authenticator
auth = init_authenticator(('ldap', 'default'),
{ 'uri': "ldap://localhost:389",
'base_dn': "dc=yunohost,dc=org",
'user_rdn': "cn=admin" })
auth.authenticate('yunohost')
# Initialize YunoHost LDAP base
tools_ldapinit(auth)
# New domain config
tools_maindomain(auth, old_domain='yunohost.org', new_domain=domain, dyndns=dyndns)
# Generate SSOwat configuration file
app_ssowatconf(auth)
# Change LDAP admin password
tools_adminpw(auth, password)
# Enable UPnP silently and reload firewall
firewall_upnp('enable', no_refresh=True)
firewall_reload()
# Enable iptables at boot time
os.system('update-rc.d yunohost-firewall defaults')
os.system('touch /etc/yunohost/installed')
msignals.display(m18n.n('yunohost_configured'), 'success')
def tools_update(ignore_apps=False, ignore_packages=False):
"""
Update apps & package cache, then display changelog
Keyword arguments:
ignore_apps -- Ignore app list update and changelog
ignore_packages -- Ignore apt cache update and changelog
"""
from yunohost.app import app_fetchlist, app_info
packages = []
if not ignore_packages:
cache = apt.Cache()
# Update APT cache
msignals.display(m18n.n('updating_apt_cache'))
if not cache.update():
raise MoulinetteError(errno.EPERM, m18n.n('update_cache_failed'))
msignals.display(m18n.n('done'))
cache.open(None)
cache.upgrade(True)
# Add changelogs to the result
for pkg in cache.get_changes():
packages.append({
'name': pkg.name,
'fullname': pkg.fullname,
'changelog': pkg.get_changelog()
})
apps = []
if not ignore_apps:
try:
app_fetchlist()
except MoulinetteError:
pass
app_list = os.listdir(apps_setting_path)
if len(app_list) > 0:
for app_id in app_list:
if '__' in app_id:
original_app_id = app_id[:app_id.index('__')]
else:
original_app_id = app_id
current_app_dict = app_info(app_id, raw=True)
new_app_dict = app_info(original_app_id, raw=True)
# Custom app
if 'lastUpdate' not in new_app_dict or 'git' not in new_app_dict:
continue
if (new_app_dict['lastUpdate'] > current_app_dict['lastUpdate']) \
or ('update_time' not in current_app_dict['settings'] \
and (new_app_dict['lastUpdate'] > current_app_dict['settings']['install_time'])) \
or ('update_time' in current_app_dict['settings'] \
and (new_app_dict['lastUpdate'] > current_app_dict['settings']['update_time'])):
apps.append({
'id': app_id,
'label': current_app_dict['settings']['label']
})
if len(apps) == 0 and len(packages) == 0:
msignals.display(m18n.n('packages_no_upgrade'))
return { 'packages': packages, 'apps': apps }
def tools_upgrade(auth, ignore_apps=False, ignore_packages=False):
"""
Update apps & package cache, then display changelog
Keyword arguments:
ignore_apps -- Ignore apps upgrade
ignore_packages -- Ignore APT packages upgrade
"""
from yunohost.app import app_upgrade
failure = False
# Retrieve interface
is_api = True if msettings.get('interface') == 'api' else False
if not ignore_packages:
cache = apt.Cache()
cache.open(None)
cache.upgrade(True)
# If API call
if is_api:
critical_packages = ("moulinette", "moulinette-yunohost",
"yunohost-admin", "yunohost-config-nginx", "ssowat", "python")
critical_upgrades = set()
for pkg in cache.get_changes():
if pkg.name in critical_packages:
critical_upgrades.add(pkg.name)
# Temporarily keep package ...
pkg.mark_keep()
# ... and set a hourly cron up to upgrade critical packages
if critical_upgrades:
msignals.display(m18n.n('packages_upgrade_critical_later',
', '.join(critical_upgrades)))
with open('/etc/cron.d/yunohost-upgrade', 'w+') as f:
f.write('00 * * * * root PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin apt-get install %s -y && rm -f /etc/cron.d/yunohost-upgrade\n' % ' '.join(critical_upgrades))
if cache.get_changes():
msignals.display(m18n.n('upgrading_packages'))
try:
# Apply APT changes
# TODO: Logs output for the API
cache.commit(apt.progress.text.AcquireProgress(),
apt.progress.base.InstallProgress())
except Exception as e:
failure = True
logging.warning('unable to upgrade packages: %s' % str(e))
msignals.display(m18n.n('packages_upgrade_failed'), 'error')
else:
msignals.display(m18n.n('done'))
else:
msignals.display(m18n.n('packages_no_upgrade'))
if not ignore_apps:
try:
app_upgrade(auth)
except Exception as e:
failure = True
logging.warning('unable to upgrade apps: %s' % str(e))
msignals.display(m18n.n('app_upgrade_failed'), 'error')
if not failure:
msignals.display(m18n.n('system_upgraded'), 'success')
# Return API logs if it is an API call
if is_api:
from yunohost.service import service_log
return { "log": service_log('yunohost-api', number="100").values()[0] }

View file

@ -1,406 +0,0 @@
# -*- coding: utf-8 -*-
""" License
Copyright (C) 2014 YUNOHOST.ORG
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program; if not, see http://www.gnu.org/licenses
"""
""" yunohost_user.py
Manage users
"""
import os
import sys
import crypt
import random
import string
import json
import errno
import subprocess
import math
import re
from moulinette.core import MoulinetteError
def user_list(auth, fields=None, filter=None, limit=None, offset=None):
"""
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
"""
user_attrs = { 'uid': 'username',
'cn': 'fullname',
'mail': 'mail',
'maildrop': 'mail-forward',
'mailuserquota': 'mailbox-quota' }
attrs = [ 'uid' ]
users = {}
# Set default arguments values
if offset is None:
offset = 0
if limit is None:
limit = 1000
if filter is None:
filter = '(&(objectclass=person)(!(uid=root))(!(uid=nobody)))'
if fields:
keys = user_attrs.keys()
for attr in fields:
if attr in keys:
attrs.append(attr)
else:
raise MoulinetteError(errno.EINVAL,
m18n.n('field_invalid', attr))
else:
attrs = [ 'uid', 'cn', 'mail', 'mailuserquota' ]
result = auth.search('ou=users,dc=yunohost,dc=org', filter, attrs)
if len(result) > offset and limit > 0:
for user in result[offset:offset+limit]:
entry = {}
for attr, values in user.items():
try:
entry[user_attrs[attr]] = values[0]
except:
pass
uid = entry[user_attrs['uid']]
users[uid] = entry
return { 'users' : users }
def user_create(auth, username, firstname, lastname, mail, password,
mailbox_quota=0):
"""
Create user
Keyword argument:
firstname
lastname
username -- Must be unique
mail -- Main mail address must be unique
password
mailbox_quota -- Mailbox size quota
"""
import pwd
from yunohost.domain import domain_list
from yunohost.hook import hook_callback
from yunohost.app import app_ssowatconf
# Validate uniqueness of username and mail in LDAP
auth.validate_uniqueness({
'uid' : username,
'mail' : mail
})
# Validate uniqueness of username in system users
try:
pwd.getpwnam(username)
except KeyError:
pass
else:
raise MoulinetteError(errno.EEXIST, m18n.n('system_username_exists'))
# Check that the mail domain exists
if mail[mail.find('@')+1:] not in domain_list(auth)['domains']:
raise MoulinetteError(errno.EINVAL,
m18n.n('mail_domain_unknown',
mail[mail.find('@')+1:]))
# Get random UID/GID
uid_check = gid_check = 0
while uid_check == 0 and gid_check == 0:
uid = str(random.randint(200, 99999))
uid_check = os.system("getent passwd %s" % uid)
gid_check = os.system("getent group %s" % uid)
# Adapt values for LDAP
fullname = '%s %s' % (firstname, lastname)
rdn = 'uid=%s,ou=users' % username
char_set = string.ascii_uppercase + string.digits
salt = ''.join(random.sample(char_set,8))
salt = '$1$' + salt + '$'
pwd = '{CRYPT}' + crypt.crypt(str(password), salt)
attr_dict = {
'objectClass' : ['mailAccount', 'inetOrgPerson', 'posixAccount'],
'givenName' : firstname,
'sn' : lastname,
'displayName' : fullname,
'cn' : fullname,
'uid' : username,
'mail' : mail,
'maildrop' : username,
'mailuserquota' : mailbox_quota,
'userPassword' : pwd,
'gidNumber' : uid,
'uidNumber' : uid,
'homeDirectory' : '/home/' + username,
'loginShell' : '/bin/false'
}
# If it is the first user, add some aliases
if not auth.search(base='ou=users,dc=yunohost,dc=org', filter='uid=*'):
with open('/etc/yunohost/current_host') as f:
main_domain = f.readline().rstrip()
aliases = [
'root@'+ main_domain,
'admin@'+ main_domain,
'webmaster@'+ main_domain,
'postmaster@'+ main_domain,
]
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()))
if 'redirected_urls' in ssowat_conf and '/' in ssowat_conf['redirected_urls']:
del ssowat_conf['redirected_urls']['/']
with open('/etc/ssowat/conf.json.persistent', 'w+') as f:
json.dump(ssowat_conf, f, sort_keys=True, indent=4)
except IOError: pass
if auth.add(rdn, attr_dict):
# Update SFTP user group
memberlist = auth.search(filter='cn=sftpusers', attrs=['memberUid'])[0]['memberUid']
memberlist.append(username)
if auth.update('cn=sftpusers,ou=groups', { 'memberUid': memberlist }):
os.system("su - %s -c ''" % username)
app_ssowatconf(auth)
#TODO: Send a welcome mail to user
msignals.display(m18n.n('user_created'), 'success')
hook_callback('post_user_create',
args=[username, mail, password, firstname, lastname])
return { 'fullname' : fullname, 'username' : username, 'mail' : mail }
raise MoulinetteError(169, m18n.n('user_creation_failed'))
def user_delete(auth, username, purge=False):
"""
Delete user
Keyword argument:
username -- Username to delete
purge
"""
from yunohost.app import app_ssowatconf
if auth.remove('uid=%s,ou=users' % username):
# Update SFTP user group
memberlist = auth.search(filter='cn=sftpusers', attrs=['memberUid'])[0]['memberUid']
try: memberlist.remove(username)
except: pass
if auth.update('cn=sftpusers,ou=groups', { 'memberUid': memberlist }):
if purge:
os.system('rm -rf /home/%s' % username)
else:
raise MoulinetteError(169, m18n.n('user_deletion_failed'))
app_ssowatconf(auth)
msignals.display(m18n.n('user_deleted'), 'success')
def user_update(auth, username, firstname=None, lastname=None, mail=None,
change_password=None, add_mailforward=None, remove_mailforward=None,
add_mailalias=None, remove_mailalias=None, mailbox_quota=None):
"""
Update user informations
Keyword argument:
lastname
mail
firstname
add_mailalias -- Mail aliases to add
remove_mailforward -- Mailforward addresses to remove
username -- Username of user to update
add_mailforward -- Mailforward addresses to add
change_password -- New password to set
remove_mailalias -- Mail aliases to remove
"""
from yunohost.domain import domain_list
from yunohost.app import app_ssowatconf
attrs_to_fetch = ['givenName', 'sn', 'mail', 'maildrop']
new_attr_dict = {}
domains = domain_list(auth)['domains']
# Populate user informations
result = auth.search(base='ou=users,dc=yunohost,dc=org', filter='uid=' + username, attrs=attrs_to_fetch)
if not result:
raise MoulinetteError(errno.EINVAL, m18n.n('user_unknown'))
user = result[0]
# Get modifications from arguments
if firstname:
new_attr_dict['givenName'] = firstname # TODO: Validate
new_attr_dict['cn'] = new_attr_dict['displayName'] = firstname + ' ' + user['sn'][0]
if lastname:
new_attr_dict['sn'] = lastname # TODO: Validate
new_attr_dict['cn'] = new_attr_dict['displayName'] = user['givenName'][0] + ' ' + lastname
if lastname and firstname:
new_attr_dict['cn'] = new_attr_dict['displayName'] = firstname + ' ' + lastname
if change_password:
char_set = string.ascii_uppercase + string.digits
salt = ''.join(random.sample(char_set,8))
salt = '$1$' + salt + '$'
new_attr_dict['userPassword'] = '{CRYPT}' + crypt.crypt(str(change_password), salt)
if mail:
auth.validate_uniqueness({ 'mail': mail })
if mail[mail.find('@')+1:] not in domains:
raise MoulinetteError(errno.EINVAL,
m18n.n('mail_domain_unknown',
mail[mail.find('@')+1:]))
del user['mail'][0]
new_attr_dict['mail'] = [mail] + user['mail']
if add_mailalias:
if not isinstance(add_mailalias, list):
add_mailalias = [ add_mailalias ]
for mail in add_mailalias:
auth.validate_uniqueness({ 'mail': mail })
if mail[mail.find('@')+1:] not in domains:
raise MoulinetteError(errno.EINVAL,
m18n.n('mail_domain_unknown',
mail[mail.find('@')+1:]))
user['mail'].append(mail)
new_attr_dict['mail'] = user['mail']
if remove_mailalias:
if not isinstance(remove_mailalias, list):
remove_mailalias = [ remove_mailalias ]
for mail in remove_mailalias:
if len(user['mail']) > 1 and mail in user['mail'][1:]:
user['mail'].remove(mail)
else:
raise MoulinetteError(errno.EINVAL,
m18n.n('mail_alias_remove_failed', mail))
new_attr_dict['mail'] = user['mail']
if add_mailforward:
if not isinstance(add_mailforward, list):
add_mailforward = [ add_mailforward ]
for mail in add_mailforward:
if mail in user['maildrop'][1:]:
continue
user['maildrop'].append(mail)
new_attr_dict['maildrop'] = user['maildrop']
if remove_mailforward:
if not isinstance(remove_mailforward, list):
remove_mailforward = [ remove_mailforward ]
for mail in remove_mailforward:
if len(user['maildrop']) > 1 and mail in user['maildrop'][1:]:
user['maildrop'].remove(mail)
else:
raise MoulinetteError(errno.EINVAL,
m18n.n('mail_forward_remove_failed', mail))
new_attr_dict['maildrop'] = user['maildrop']
if mailbox_quota is not None:
new_attr_dict['mailuserquota'] = mailbox_quota
if auth.update('uid=%s,ou=users' % username, new_attr_dict):
msignals.display(m18n.n('user_updated'), 'success')
app_ssowatconf(auth)
return user_info(auth, username)
else:
raise MoulinetteError(169, m18n.n('user_update_failed'))
def user_info(auth, username):
"""
Get user informations
Keyword argument:
username -- Username or mail to get informations
"""
user_attrs = [
'cn', 'mail', 'uid', 'maildrop', 'givenName', 'sn', 'mailuserquota'
]
if len(username.split('@')) is 2:
filter = 'mail='+ username
else:
filter = 'uid='+ username
result = auth.search('ou=users,dc=yunohost,dc=org', filter, user_attrs)
if result:
user = result[0]
else:
raise MoulinetteError(errno.EINVAL, m18n.n('user_unknown'))
result_dict = {
'username': user['uid'][0],
'fullname': user['cn'][0],
'firstname': user['givenName'][0],
'lastname': user['sn'][0],
'mail': user['mail'][0]
}
if len(user['mail']) > 1:
result_dict['mail-aliases'] = user['mail'][1:]
if len(user['maildrop']) > 1:
result_dict['mail-forward'] = user['maildrop'][1:]
if 'mailuserquota' in user:
if user['mailuserquota'][0] != '0':
cmd = 'doveadm -f flow quota get -u %s' % user['uid'][0]
userquota = subprocess.check_output(cmd,stderr=subprocess.STDOUT,
shell=True)
quotavalue = re.findall(r'\d+', userquota)
result = '%s (%s%s)' % ( _convertSize(eval(quotavalue[0])),
quotavalue[2], '%')
result_dict['mailbox-quota'] = {
'limit' : user['mailuserquota'][0],
'use' : result
}
else:
result_dict['mailbox-quota'] = m18n.n('unlimit')
if result:
return result_dict
else:
raise MoulinetteError(167, m18n.n('user_info_failed'))
def _convertSize(num, suffix=''):
for unit in ['K','M','G','T','P','E','Z']:
if abs(num) < 1024.0:
return "%3.1f%s%s" % (num, unit, suffix)
num /= 1024.0
return "%.1f%s%s" % (num, 'Yi', suffix)

47
locales/ar.json Normal file
View file

@ -0,0 +1,47 @@
{
"argument_required": "المُعامِل '{argument}' مطلوب",
"authentication_required": "المصادقة مطلوبة",
"confirm": "تأكيد {prompt}",
"deprecated_command": "'{prog} {command}' تم التخلي عنه و سوف تتم إزالته مستقبلا",
"deprecated_command_alias": "'{prog} {old}' تم التخلي عنه و سوف يتم إزالته مستقبلا، إستخدم '{prog} {new}' بدلا من ذلك",
"error": "خطأ :",
"file_not_exist": "الملف غير موجود : '{path}'",
"folder_exists": "إنّ المجلد موجود من قبل : '{path}'",
"instance_already_running": "هناك بالفعل عملية YunoHost جارية. الرجاء الانتظار حتى ينتهي الأمر قبل تشغيل آخر.",
"invalid_argument": "المُعامِل غير صالح '{argument}': {error}",
"invalid_usage": "إستعمال غير صالح، إستخدم --help لعرض المساعدة",
"logged_in": "مُتّصل",
"logged_out": "تم تسجيل خروجك",
"not_logged_in": "لم تقم بعدُ بتسجيل دخولك",
"operation_interrupted": "تم توقيف العملية",
"password": "كلمة السر",
"pattern_not_match": "لا يتطابق مع النموذج",
"root_required": "يتوجب عليك أن تكون مدير الجذر root للقيام بهذا الإجراء",
"server_already_running": "هناك خادم يشتغل على ذاك المنفذ",
"success": "تم بنجاح !",
"unable_authenticate": "تعذرت المصادقة",
"unknown_group": "الفريق '{group}' مجهول",
"unknown_user": "المستخدم '{user}' مجهول",
"values_mismatch": "القيمتين غير متطابقتين",
"warning": "تحذير :",
"websocket_request_expected": "كان ينتظر طلبًا عبر الويب سوكت WebSocket",
"cannot_open_file": "ليس بالإمكان فتح الملف {file} (السبب : {error})",
"cannot_write_file": "لا يمكن الكتابة في الملف {file} (السبب : {error})",
"unknown_error_reading_file": "طرأ هناك خطأ ما أثناء عملية قراءة الملف {file} (السبب: {error})",
"corrupted_json": "قراءة ملف JSON مُشوّهة مِن {ressource} (السبب : {error})",
"error_writing_file": "طرأ هناك خطأ أثناء الكتابة في الملف {file}: {error}",
"error_removing": "خطأ أثناء عملية حذف {path}: {error}",
"error_changing_file_permissions": "خطأ أثناء عملية تعديل التصريحات لـ {path}: {error}",
"invalid_url": "فشل الاتصال بـ {url}… ربما تكون الخدمة معطلة ، أو أنك غير متصل بشكل صحيح بالإنترنت في IPv4 / IPv6.",
"download_ssl_error": "خطأ في الاتصال الآمن عبر الـ SSL أثناء محاولة الربط بـ {url}",
"download_timeout": "{url} استغرق مدة طويلة جدا للإستجابة، فتوقّف.",
"download_unknown_error": "خطأ أثناء عملية تنزيل البيانات مِن {url} : {error}",
"download_bad_status_code": "{url} أعاد رمز الحالة {code}",
"corrupted_yaml": "قراءة مُشوّهة لملف YAML مِن {ressource} (السبب : {error})",
"info": "معلومة:",
"warn_the_user_about_waiting_lock_again": "جارٍ الانتظار…",
"warn_the_user_that_lock_is_acquired": "لقد انتهى تنفيذ ذاك الأمر للتوّ ، جارٍ تنفيذ هذا الأمر",
"warn_the_user_about_waiting_lock": "هناك أمر لـ YunoHost قيد التشغيل حاليا. في انتظار انتهاء تنفيذه قبل تشغيل التالي",
"edit_text_question": "{}. تعديل هذا النص؟ [yN]: ",
"corrupted_toml": "قراءة مُشوّهة لملف TOML مِن {ressource} (السبب : {error})"
}

4
locales/bn_BD.json Normal file
View file

@ -0,0 +1,4 @@
{
"logged_out": "প্রস্থান",
"password": "পাসওয়ার্ড"
}

1
locales/br.json Normal file
View file

@ -0,0 +1 @@
{}

47
locales/ca.json Normal file
View file

@ -0,0 +1,47 @@
{
"argument_required": "Es requereix l'argument «{argument}»",
"authentication_required": "Es requereix autenticació",
"confirm": "Confirmar {prompt}",
"deprecated_command": "{prog}{command}és obsolet i es desinstal·larà en el futur",
"deprecated_command_alias": "{prog}{old}és obsolet i es desinstal·larà en el futur, utilitzeu {prog}{new}en el seu lloc",
"error": "Error:",
"file_not_exist": "El fitxer no existeix: '{path}'",
"folder_exists": "La carpeta ja existeix: '{path}'",
"instance_already_running": "Ja hi ha una operació de YunoHost en curs. Espereu a que s'acabi abans d'executar-ne una altra.",
"invalid_argument": "Argument invàlid '{argument}': {error}",
"invalid_usage": "Utilització invàlida, utilitzeu --help per veure l'ajuda",
"logged_in": "Sessió iniciada",
"logged_out": "Sessió tancada",
"not_logged_in": "No ha iniciat sessió",
"operation_interrupted": "Operació interrompuda",
"password": "Contrasenya",
"pattern_not_match": "No coincideix amb el patró",
"root_required": "Ha de ser root per realitzar aquesta acció",
"server_already_running": "Ja s'està executant un servidor en aquest port",
"success": "Èxit!",
"unable_authenticate": "No s'ha pogut autenticar",
"unknown_group": "Grup '{group}' desconegut",
"unknown_user": "Usuari '{user}' desconegut",
"values_mismatch": "Els valors no coincideixen",
"warning": "Atenció:",
"websocket_request_expected": "S'esperava una petició WebSocket",
"cannot_open_file": "No s'ha pogut obrir el fitxer {file} (motiu: {error})",
"cannot_write_file": "No s'ha pogut escriure el fitxer {file} (motiu: {error})",
"unknown_error_reading_file": "Error desconegut al intentar llegir el fitxer {file} (motiu: {error})",
"corrupted_json": "JSON corrupte llegit des de {ressource} (motiu: {error})",
"corrupted_yaml": "YAML corrupte llegit des de {ressource} (motiu: {error})",
"error_writing_file": "Error al escriure el fitxer {file}: {error}",
"error_removing": "Error al eliminar {path}: {error}",
"error_changing_file_permissions": "Error al canviar els permisos per {path}: {error}",
"invalid_url": "No s'ha pogut connectar a {url}… pot ser que el servei estigui caigut, o que no hi hagi connexió a Internet amb IPv4/IPv6.",
"download_ssl_error": "Error SSL al connectar amb {url}",
"download_timeout": "{url} ha tardat massa en respondre, s'ha deixat d'esperar.",
"download_unknown_error": "Error al baixar dades des de {url}: {error}",
"download_bad_status_code": "{url} ha retornat el codi d'estat {code}",
"info": "Info:",
"corrupted_toml": "El fitxer TOML ha estat corromput en la lectura des de {ressource} (motiu: {error})",
"warn_the_user_about_waiting_lock": "Hi ha una altra ordre de YunoHost en execució, s'executarà aquesta ordre un cop l'anterior hagi acabat",
"warn_the_user_about_waiting_lock_again": "Encara en espera…",
"warn_the_user_that_lock_is_acquired": "L'altra ordre tot just ha acabat, ara s'executarà aquesta ordre",
"edit_text_question": "{}. Edita aquest text ? [yN]: "
}

1
locales/ckb.json Normal file
View file

@ -0,0 +1 @@
{}

47
locales/cmn.json Normal file
View file

@ -0,0 +1,47 @@
{
"argument_required": "参数“{argument}”是必须的",
"authentication_required": "需要验证",
"confirm": "确认 {prompt}",
"deprecated_command": "{prog}{command}已经放弃使用,将来会删除",
"deprecated_command_alias": "{prog}{old}已经放弃使用,将来会删除,请使用{prog}{new}代替",
"error": "错误:",
"file_not_exist": "文件不存在: '{path}'",
"folder_exists": "目录已存在: '{path}'",
"info": "信息:",
"instance_already_running": "已经有一个YunoHost操作正在运行。 请等待它完成再运行另一个。",
"invalid_argument": "参数错误{argument}{error}",
"invalid_usage": "用法错误,输入 --help 查看帮助信息",
"logged_in": "登录",
"logged_out": "登出",
"not_logged_in": "您未登录",
"operation_interrupted": "操作中断",
"password": "密码",
"pattern_not_match": "模式匹配失败",
"root_required": "必须以root身份进行此操作",
"server_already_running": "服务已运行在指定端口",
"success": "成功!",
"unable_authenticate": "认证失败",
"unknown_group": "未知组{group}",
"unknown_user": "未知用户{user}",
"values_mismatch": "值不匹配",
"warning": "警告:",
"websocket_request_expected": "期望一个WebSocket请求",
"cannot_open_file": "不能打开文件{file}(原因:{error}",
"cannot_write_file": "写入文件{file}失败(原因:{error}",
"unknown_error_reading_file": "尝试读取文件{file}时发生未知错误(原因:{error}",
"corrupted_json": "从{ressource}读取的JSON损坏原因{error}",
"corrupted_yaml": "从{ressource}读取的YMAL损坏原因{error}",
"error_writing_file": "写入文件{file}失败:{error}",
"error_removing": "删除路径{path}失败:{error}",
"error_changing_file_permissions": "目录{path}权限修改失败:{error}",
"invalid_url": "{url} 连接失败… 可能是服务中断了或者你没有正确连接到IPv4/IPv6的互联网。",
"download_ssl_error": "连接{url}时发生SSL错误",
"download_timeout": "{url}响应超时,放弃。",
"download_unknown_error": "下载{url}失败:{error}",
"download_bad_status_code": "{url}返回状态码:{code}",
"warn_the_user_that_lock_is_acquired": "另一个命令刚刚完成,现在启动此命令",
"warn_the_user_about_waiting_lock_again": "仍在等待…",
"warn_the_user_about_waiting_lock": "目前正在运行另一个YunoHost命令我们在运行此命令之前等待它完成",
"corrupted_toml": "从{ressource}读取的TOML已损坏原因{error}",
"edit_text_question": "{}.编辑此文本?[yN]: "
}

47
locales/cs.json Normal file
View file

@ -0,0 +1,47 @@
{
"password": "Heslo",
"logged_out": "Jste odhlášen/a",
"warn_the_user_that_lock_is_acquired": "Předchozí operace dokončena, nyní spouštíme tuto",
"warn_the_user_about_waiting_lock_again": "Stále čekáme…",
"warn_the_user_about_waiting_lock": "Jiná YunoHost operace právě probíhá, před spuštěním této čekáme na její dokončení",
"download_bad_status_code": "{url} vrátil stavový kód {code}",
"download_unknown_error": "Chyba při stahování dat z {url}: {error}",
"download_timeout": "{url} příliš dlouho neodpovídá, akce přerušena.",
"download_ssl_error": "SSL chyba při spojení s {url}",
"invalid_url": "Špatný odkaz {url} (je vůbec dostupný?).",
"error_changing_file_permissions": "Chyba při nastavování oprávnění pro {path}: {error}",
"error_removing": "Chyba při přesunu {path}: {error}",
"error_writing_file": "Chyba při zápisu souboru/ů {file}: {error}",
"corrupted_toml": "Nepodařilo se načíst TOML z {ressource} (reason: {error})",
"corrupted_yaml": "Nepodařilo se načíst YAML z {ressource} (reason: {error})",
"corrupted_json": "Nepodařilo se načíst JSON {ressource} (reason: {error})",
"unknown_error_reading_file": "Vyskytla se neznámá chyba při čtení souboru/ů {file} (reason: {error})",
"cannot_write_file": "Nelze zapsat soubor/y {file} (reason: {error})",
"cannot_open_file": "Nelze otevřít soubor/y {file} (reason: {error})",
"websocket_request_expected": "Očekáván WebSocket požadavek",
"warning": "Varování:",
"values_mismatch": "Hodnoty nesouhlasí",
"unknown_user": "Neznámý '{user}' uživatel",
"unknown_group": "Neznámá '{group}' skupina",
"unable_authenticate": "Není možné ověřit",
"success": "Zadařilo se!",
"server_already_running": "Na tomto portu je server již provozován",
"root_required": "Pro provedení této akce musíte být root",
"pattern_not_match": "Neodpovídá výrazu",
"operation_interrupted": "Operace přerušena",
"not_logged_in": "Nejste přihlášen",
"logged_in": "Přihlášení",
"invalid_usage": "Nesprávné použití, pass --help pro zobrazení nápovědy",
"invalid_argument": "Nesprávný argument '{argument}': {error}",
"instance_already_running": "Právě probíhá jiná YunoHost operace. Před spuštěním další operace vyčkejte na její dokončení.",
"info": "Info:",
"folder_exists": "Adresář již existuje: '{path}'",
"file_not_exist": "Soubor neexistuje: '{path}'",
"error": "Chyba:",
"deprecated_command_alias": "'{prog} {old}' je zastaralý a bude odebrán v budoucích verzích, použijte '{prog} {new}'",
"deprecated_command": "'{prog} {command}' je zastaralý a bude odebrán v budoucích verzích",
"confirm": "Potvrdit {prompt}",
"authentication_required": "Vyžadováno ověření",
"argument_required": "Je vyžadován argument '{argument}'",
"edit_text_question": "{}. Upravit tento text? [yN]: "
}

1
locales/da.json Normal file
View file

@ -0,0 +1 @@
{}

View file

@ -1,75 +1,47 @@
{
"yunohost_not_installed": "YunoHost ist nicht (oder nicht recht) installiert. Bitte 'yunohost tools postinstall' ablaufen.",
"upgrade_complete": "Upgrade erfolgreich beendet",
"installation_complete": "erfolgreich installiert",
"installation_failed": "Fehler beim Installation",
"app_unknown": "unbekannte App",
"app_no_upgrade": "Keine App zu updaten",
"app_not_installed": "{:s} ist nicht intalliert",
"custom_app_url_required": "Bitte eine URL geben, um deine nüzterspezifische App {:s} zu updaten",
"app_recent_version_required": "{:s} braucht eine jüngstere Fassung von \"moulinette\"",
"app_upgraded": "{:s} erfolgreich updaten",
"app_id_invalid": "Falsche App-ID",
"app_already_installed": "{:s} ist schon installiert",
"app_removed": "{:s} erfolgreich gelöscht",
"app_location_already_used": "Eine App ist auf diesem Ort schon installiert",
"app_location_install_failed": "Diese App ist auf diesem Ort nicht Installbar",
"app_extraction_failed": "Installationsdateien nicht extrahierbar",
"app_install_files_invalid": "Ungültige Installationsdateien",
"app_sources_fetch_failed": "Quelledateien nicht abrufbar",
"ssowat_conf_updated": "SSOwat beständige Einstellung erfolgreich upgedatet",
"ssowat_conf_generated": "SSOwat-einstellung erfolgreich erzeugt",
"mysql_db_creation_failed": "Fehler beim MySQL-datenbankerzeugung",
"mysql_db_init_failed": "Fehler beim MySQL-datenbankinitialisierung",
"mysql_db_initialized": "MySQL-datenbank erfolgreich initialisiert",
"extracting": "Extrahierend...",
"downloading": "Herunterladend...",
"done": "Erledigt.",
"domain_unknown": "Unbekannte Domain",
"domain_dyndns_invalid": "Domain mit DynDNS nicht nützbar",
"domain_dyndns_already_subscribed": "Du hast dich schon für einen DynDNS-domain angemeldet",
"domain_dyndns_root_unknown": "Unbekannte DynDNS-hauptdomain",
"domain_cert_gen_failed": "Zertifizierung nicht erzeugbar",
"domain_exists": "diese Domain existiert schon",
"domain_creation_failed": "Domain nicht erzeugbar",
"domain_created": "Domain erfolgreich erzeugt",
"domain_uninstall_app_first": "Mindestens eine App ist schon auf diese Domain installiert. Bitte erst die Apps deinstallieren, und nur dann die Domain löschen.",
"domain_deletion_failed": "Domain nicht löschbar",
"domain_deleted": "Domain erfolgreich gelöscht",
"no_internet_connection": "Server not connected to the Internet",
"dyndns_key_generating": "DNS key is being generated, it may take a while...",
"dyndns_unavailable": "DynDNS-subdomain nicht verfügbar",
"dyndns_registration_failed": "DynDNS-domain {:s} nicht registrierbar",
"dyndns_registered": "DynDNS-domain erfolgreich registriert",
"dyndns_ip_update_failed": "IP-adress auf DynDNS nicht updatbar",
"dyndns_ip_updated": "IP-adress auf DynDNS erfolgreich upgedatet",
"dyndns_cron_installed": "DynDNS cron job erfolgreich installiert",
"dyndns_cron_remove_failed": "DynDNS cron job nicht löschbar",
"dyndns_cron_removed": "DynDNS cron job erfolgreich gelöscht",
"iptables_unavailable": "Du kannst nicht hier die IP-Tabelle bearbeiten. Entweder bist du in einen Container oder deinen Systemkern erhält es nicht.",
"firewall_reloaded": "Firewall erfolgreich neu geladen",
"hook_choice_invalid": "ungültige Wahl '{:s}'",
"hook_argument_missing": "Fehlend Argument '{:s}'",
"mountpoint_unknown": "unbekannten Einhängepunkt",
"unit_unknown": "unbekannte Einheit '{:s}'",
"monitor_period_invalid": "Falschen Zeitraum",
"monitor_stats_no_update": "Keine Monitoringstatistik zu updaten",
"monitor_stats_file_not_found": "Statistikdatei nicht gefunden",
"monitor_stats_period_unavailable": "Keine verfügbare Statistik für diese Zeitraum",
"monitor_enabled": "Servermonitoring erfolgreich aktiviert",
"monitor_disabled": "Servermonitoring erfolgreich deaktiviert",
"monitor_not_enabled": "Servermonitoring ist nicht aktiviert",
"monitor_glances_con_failed": "Verbindung mit Glances-server nicht möglich",
"service_unknown": "Unbekannte Dienst '{:s}'",
"service_start_failed": "Kann nicht '{:s}' -dienst starten",
"service_already_started": "'{:s}' -dienst ist schon im Betrieb",
"service_started": "'{:s}' -dienst erfolgreich gestartet",
"service_stop_failed": "Kann nicht '{:s}' -dienst stoppen",
"service_already_stopped": "'{:s}' -dienst ist schon abgestoppt",
"service_stopped": "'{:s}' -dienst erfolgreich abgestoppt",
"service_enable_failed": "Kann nicht '{:s}' -dienst aktivieren",
"service_enabled": "'{:s}' -dienst erfolgreich aktiviert",
"service_disable_failed": "Kann nicht '{:s}' -dienst deaktivieren",
"service_disabled": "'{:s}' -dienst erfolgreich deaktiviert",
"service_status_failed": "Kann nicht '{:s}' -dienststatus feststellen"
}
"argument_required": "Der Parameter {argument} ist erforderlich",
"authentication_required": "Anmeldung erforderlich",
"confirm": "Bestätigen Sie {prompt}",
"error": "Fehler:",
"file_not_exist": "Datei ist nicht vorhanden: '{path}'",
"folder_exists": "Ordner existiert bereits: '{path}'",
"instance_already_running": "Es läuft bereits eine YunoHost-Operation. Bitte warte, bis sie fertig ist, bevor du eine weitere startest.",
"invalid_argument": "Argument ungültig '{argument}': {error}",
"invalid_usage": "Falscher Aufruf, verwende --help für den Hilfstext",
"logged_in": "Angemeldet",
"logged_out": "Abgemeldet",
"not_logged_in": "Du bist nicht angemeldet",
"operation_interrupted": "Vorgang unterbrochen",
"password": "Passwort",
"pattern_not_match": "Entspricht nicht dem Muster",
"root_required": "Nur der Nutzer root kann diesen Vorgang ausführen",
"server_already_running": "Einen anderer Dienst arbeitet bereits auf diesem Port",
"success": "Erfolg!",
"unable_authenticate": "Anmelden fehlgeschlagen",
"values_mismatch": "Die Werte passen nicht zusammen",
"warning": "Warnung:",
"websocket_request_expected": "Eine WebSocket-Anfrage wurde erwartet",
"deprecated_command": "'{prog} {command}' ist veraltet und wird bald entfernt werden",
"deprecated_command_alias": "'{prog} {old}' ist veraltet und wird bald entfernt werden, benutze '{prog} {new}' stattdessen",
"unknown_group": "Gruppe '{group}' ist unbekannt",
"unknown_user": "Konto '{user}' ist unbekannt",
"info": "Info:",
"corrupted_json": "Beschädigtes JSON gelesen von {ressource} (reason: {error})",
"unknown_error_reading_file": "Unbekannter Fehler beim Lesen der Datei {file} (reason: {error})",
"cannot_write_file": "Kann Datei {file} nicht schreiben (reason: {error})",
"cannot_open_file": "Datei {file} konnte nicht geöffnet werden (Ursache: {error})",
"corrupted_yaml": "Beschädigtes YAML gelesen von {ressource} (reason: {error})",
"warn_the_user_that_lock_is_acquired": "Der andere Befehl wurde gerade abgeschlossen, starte jetzt diesen Befehl",
"warn_the_user_about_waiting_lock_again": "Immer noch wartend…",
"warn_the_user_about_waiting_lock": "Ein anderer YunoHost Befehl läuft gerade, wir warten bis er fertig ist, bevor dieser laufen kann",
"download_bad_status_code": "{url} lieferte folgende(n) Status Code(s) {code}",
"download_unknown_error": "Fehler beim Herunterladen von Daten von {url}: {error}",
"download_timeout": "{url} brauchte zu lange zum Antworten, hab aufgegeben.",
"download_ssl_error": "SSL Fehler beim Verbinden zu {url}",
"invalid_url": "Konnte keine Verbindung zu {url} herstellen… vielleicht ist der Dienst ausgefallen, oder Sie sind nicht richtig mit dem Internet in IPv4/IPv6 verbunden.",
"error_changing_file_permissions": "Fehler beim Ändern der Berechtigungen für {path}: {error}",
"error_removing": "Fehler beim Entfernen {path}: {error}",
"error_writing_file": "Fehler beim Schreiben von Datei {file}: {error}",
"corrupted_toml": "Beschädigtes TOML gelesen von {ressource} (reason: {error})",
"edit_text_question": "{}. Diesen Text bearbeiten? [yN]: "
}

4
locales/el.json Normal file
View file

@ -0,0 +1,4 @@
{
"logged_out": "Αποσυνδέθηκα",
"password": "Κωδικός πρόσβασης"
}

View file

@ -1,197 +1,47 @@
{
"yunohost_not_installed" : "YunoHost is not or not correctly installed. Please execute 'yunohost tools postinstall'.",
"upgrade_complete" : "Upgrade complete",
"installation_complete" : "Installation complete",
"installation_failed" : "Installation failed",
"unexpected_error" : "An unexpected error occured",
"action_invalid" : "Invalid action '{:s}'",
"license_undefined" : "undefined",
"no_appslist_found" : "No apps list found",
"custom_appslist_name_required" : "You must provide a name for your custom apps list",
"appslist_retrieve_error" : "Unable to retrieve the remote apps list",
"appslist_fetched" : "Apps list successfully fetched",
"appslist_unknown" : "Unknown apps list",
"appslist_removed" : "Apps list successfully removed",
"app_unknown" : "Unknown app",
"app_no_upgrade" : "No app to upgrade",
"app_not_installed" : "{:s} is not installed",
"custom_app_url_required" : "You must provide an URL to upgrade your custom app {:s}",
"app_recent_version_required" : "{:s} requires a more recent version of the moulinette",
"app_upgraded" : "{:s} successfully upgraded",
"app_upgrade_failed" : "Unable to upgrade all apps",
"app_id_invalid" : "Invalid app id",
"app_already_installed" : "{:s} is already installed",
"app_removed" : "{:s} successfully removed",
"app_location_already_used" : "An app is already installed on this location",
"app_location_install_failed" : "Unable to install the app on this location",
"app_extraction_failed" : "Unable to extract installation files",
"app_install_files_invalid" : "Invalid installation files",
"app_manifest_invalid" : "Invalid app manifest",
"app_sources_fetch_failed" : "Unable to fetch sources files",
"ssowat_conf_updated" : "SSOwat persistent configuration successfully updated",
"ssowat_conf_generated" : "SSOwat configuration successfully generated",
"mysql_db_creation_failed" : "MySQL database creation failed",
"mysql_db_init_failed" : "MySQL database init failed",
"mysql_db_initialized" : "MySQL database successfully initialized",
"extracting" : "Extracting...",
"downloading" : "Downloading...",
"executing_script": "Executing script...",
"done" : "Done.",
"path_removal_failed" : "Unable to remove path {:s}",
"domain_unknown" : "Unknown domain",
"domain_dyndns_invalid" : "Invalid domain to use with DynDNS",
"domain_dyndns_already_subscribed" : "You already have subscribed to a DynDNS domain",
"domain_dyndns_root_unknown" : "Unknown DynDNS root domain",
"domain_cert_gen_failed" : "Unable to generate certificate",
"domain_exists" : "Domain already exists",
"dnsmasq_isnt_installed" : "dnsmasq does not seem to be installed, please run 'apt-get remove bind9 && apt-get install dnsmasq'",
"domain_zone_exists" : "DNS zone file already exists",
"domain_zone_not_found" : "DNS zone file not found for domain {:s}",
"domain_creation_failed" : "Unable to create domain",
"domain_created" : "Domain successfully created",
"domain_uninstall_app_first" : "One or more apps are installed on this domain. Please uninstall them before proceed to domain removal.",
"domain_deletion_failed" : "Unable to delete domain",
"domain_deleted" : "Domain successfully deleted",
"no_internet_connection": "Server not connected to the Internet",
"dyndns_key_generating" : "DNS key is being generated, it may take a while...",
"dyndns_unavailable" : "Unavailable DynDNS subdomain",
"dyndns_registration_failed" : "Unable to register DynDNS domain: {:s}",
"dyndns_registered" : "DynDNS domain successfully registered",
"dyndns_ip_update_failed" : "Unable to update IP address on DynDNS",
"dyndns_ip_updated" : "IP address successfully updated on DynDNS",
"dyndns_cron_installed" : "DynDNS cron job successfully installed",
"dyndns_cron_remove_failed" : "Unable to remove DynDNS cron job",
"dyndns_cron_removed" : "DynDNS cron job successfully removed",
"port_available" : "Port {} is available",
"port_unavailable" : "Port {} is not available",
"port_already_opened" : "Port {} is already opened for {:s} connections",
"port_already_closed" : "Port {} is already closed for {:s} connections",
"iptables_unavailable" : "You cannot play with iptables here. You are either in a container or your kernel does not support it.",
"ip6tables_unavailable" : "You cannot play with ip6tables here. You are either in a container or your kernel does not support it.",
"upnp_dev_not_found" : "No UPnP device found",
"upnp_port_open_failed" : "Unable to open UPnP ports",
"upnp_enabled" : "UPnP successfully enabled",
"upnp_disabled" : "UPnP successfully disabled",
"firewall_rules_cmd_failed" : "Some firewall rules commands have failed. For more information, see the log.",
"firewall_reload_failed" : "Unable to reload firewall",
"firewall_reloaded" : "Firewall successfully reloaded",
"hook_list_by_invalid" : "Invalid property to list hook by",
"hook_name_unknown" : "Unknown hook name '{:s}'",
"hook_choice_invalid" : "Invalid choice '{:s}'",
"hook_argument_missing" : "Missing argument '{:s}'",
"mountpoint_unknown" : "Unknown mountpoint",
"unit_unknown" : "Unknown unit '{:s}'",
"monitor_period_invalid" : "Invalid time period",
"monitor_stats_no_update" : "No monitoring statistics to update",
"monitor_stats_file_not_found" : "Statistics file not found",
"monitor_stats_period_unavailable" : "No available statistics for the period",
"monitor_enabled" : "Server monitoring successfully enabled",
"monitor_disabled" : "Server monitoring successfully disabled",
"monitor_not_enabled" : "Server monitoring is not enabled",
"monitor_glances_con_failed" : "Unable to connect to Glances server",
"service_unknown" : "Unknown service '{:s}'",
"service_add_failed" : "Unable to add service '{:s}'",
"service_added" : "Service successfully added",
"service_remove_failed" : "Unable to remove service '{:s}'",
"service_removed" : "Service successfully removed",
"service_start_failed" : "Unable to start service '{:s}'",
"service_already_started" : "Service '{:s}' is already started",
"service_started" : "Service '{:s}' successfully started",
"service_stop_failed" : "Unable to stop service '{:s}'",
"service_already_stopped" : "Service '{:s}' is already stopped",
"service_stopped" : "Service '{:s}' successfully stopped",
"service_enable_failed" : "Unable to enable service '{:s}'",
"service_enabled" : "Service '{:s}' successfully enabled",
"service_disable_failed" : "Unable to disable service '{:s}'",
"service_disabled" : "Service '{:s}' successfully disabled",
"service_status_failed" : "Unable to determine status of service '{:s}'",
"service_no_log" : "No log to display for service '{:s}'",
"service_cmd_exec_failed" : "Unable to execute command '{:s}'",
"network_check_smtp_ok" : "Outbound mail (SMTP port 25) is not blocked",
"network_check_smtp_ko" : "Outbound mail (SMTP port 25) seems to be blocked by your network",
"network_check_mx_ko" : "DNS MX record is not set",
"ldap_initialized" : "LDAP successfully initialized",
"admin_password_change_failed" : "Unable to change password",
"admin_password_changed" : "Administration password successfully changed",
"new_domain_required" : "You must provide the new main domain",
"maindomain_change_failed" : "Unable to change main domain",
"maindomain_changed" : "Main domain successfully changed",
"yunohost_installing" : "Installing YunoHost...",
"yunohost_already_installed" : "YunoHost is already installed",
"yunohost_ca_creation_failed" : "Unable to create certificate authority",
"yunohost_configured" : "YunoHost successfully configured",
"updating_apt_cache" : "Updating the lists of available packages...",
"update_cache_failed" : "Unable to update APT cache",
"packages_no_upgrade" : "There is no package to upgrade",
"packages_upgrade_critical_later" : "Critical packages ({:s}) will be upgraded later",
"upgrading_packages" : "Upgrading packages...",
"packages_upgrade_failed" : "Unable to upgrade all packages",
"system_upgraded" : "System successfully upgraded",
"backup_output_directory_required" : "You must provide an output directory for the backup",
"backup_output_directory_forbidden" : "Forbidden output directory",
"backup_output_directory_not_empty" : "Output directory is not empty",
"backup_running_hooks" : "Running backup hooks...",
"backup_creating_archive" : "Creating the backup archive...",
"backup_extracting_archive" : "Extracting the backup archive...",
"backup_archive_open_failed" : "Unable to open the backup archive",
"backup_archive_name_unknown" : "Unknown local backup archive name",
"backup_archive_name_exists" : "Backup archive name already exists",
"backup_complete" : "Backup complete",
"backup_invalid_archive" : "Invalid backup archive",
"restore_confirm_yunohost_installed" : "Do you really want to restore an already installed system? [{answers:s}]",
"restore_running_hooks" : "Running restoration hooks...",
"restore_failed" : "Unable to restore the system",
"restore_complete" : "Restore complete",
"unbackup_app" : "App '{:s}' will not be saved",
"unrestore_app" : "App '{:s}' will not be restored",
"field_invalid" : "Invalid field '{:s}'",
"mail_domain_unknown" : "Unknown mail address domain '{:s}'",
"mail_alias_remove_failed" : "Unable to remove mail alias '{:s}'",
"mail_forward_remove_failed" : "Unable to remove mail forward '{:s}'",
"user_unknown" : "Unknown user",
"system_username_exists" : "Username already exists in the system users",
"user_creation_failed" : "Unable to create user",
"user_created" : "User successfully created",
"user_deletion_failed" : "Unable to delete user",
"user_deleted" : "User successfully deleted",
"user_update_failed" : "Unable to update user",
"user_updated" : "User successfully updated",
"user_info_failed" : "Unable to retrieve user information",
"admin_password" : "Administration password",
"ask_firstname" : "First name",
"ask_lastname" : "Last name",
"ask_email" : "Email address",
"ask_password" : "Password",
"ask_current_admin_password" : "Current administration password",
"ask_new_admin_password" : "New administration password",
"ask_main_domain" : "Main domain",
"ask_list_to_remove" : "List to remove",
"pattern_username" : "Must be lower-case alphanumeric and underscore characters only",
"pattern_firstname" : "Must be a valid first name",
"pattern_lastname" : "Must be a valid last name",
"pattern_email" : "Must be a valid email address (e.g. someone@domain.org)",
"pattern_password" : "Must be at least 3 characters long",
"pattern_mailbox_quota" : "Must be a size with b/k/M/G/T suffix or 0 to disable the quota",
"unlimit" : "No quota",
"pattern_domain" : "Must be a valid domain name (e.g. my-domain.org)",
"pattern_listname" : "Must be alphanumeric and underscore characters only",
"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_backup_archive_name" : "Must be a valid filename with alphanumeric and -_. characters only",
"format_datetime_short" : "%m/%d/%Y %I:%M %p"
"argument_required": "Argument '{argument}' is required",
"authentication_required": "Authentication required",
"confirm": "Confirm {prompt}",
"deprecated_command": "'{prog} {command}' is deprecated and will be removed in the future",
"deprecated_command_alias": "'{prog} {old}' is deprecated and will be removed in the future, use '{prog} {new}' instead",
"edit_text_question": "{}. Edit this text ? [yN]: ",
"error": "Error:",
"file_not_exist": "File does not exist: '{path}'",
"folder_exists": "Folder already exists: '{path}'",
"info": "Info:",
"instance_already_running": "There is already a YunoHost operation running. Please wait for it to finish before running another one.",
"invalid_argument": "Invalid argument '{argument}': {error}",
"invalid_usage": "Invalid usage, pass --help to see help",
"logged_in": "Logged in",
"logged_out": "Logged out",
"not_logged_in": "You are not logged in",
"operation_interrupted": "Operation interrupted",
"password": "Password",
"pattern_not_match": "Does not match pattern",
"root_required": "You must be root to perform this action",
"server_already_running": "A server is already running on that port",
"success": "Success!",
"unable_authenticate": "Unable to authenticate",
"unknown_group": "Unknown '{group}' group",
"unknown_user": "Unknown '{user}' user",
"values_mismatch": "Values don't match",
"warning": "Warning:",
"websocket_request_expected": "Expected a WebSocket request",
"cannot_open_file": "Could not open file {file} (reason: {error})",
"cannot_write_file": "Could not write file {file} (reason: {error})",
"unknown_error_reading_file": "Unknown error while trying to read file {file} (reason: {error})",
"corrupted_json": "Corrupted JSON read from {ressource} (reason: {error})",
"corrupted_yaml": "Corrupted YAML read from {ressource} (reason: {error})",
"corrupted_toml": "Corrupted TOML read from {ressource} (reason: {error})",
"error_writing_file": "Error when writing file {file}: {error}",
"error_removing": "Error when removing {path}: {error}",
"error_changing_file_permissions": "Error when changing permissions for {path}: {error}",
"invalid_url": "Failed to connect to {url}... maybe the service is down, or you are not properly connected to the Internet in IPv4/IPv6.",
"download_ssl_error": "SSL error when connecting to {url}",
"download_timeout": "{url} took too long to answer, gave up.",
"download_unknown_error": "Error when downloading data from {url}: {error}",
"download_bad_status_code": "{url} returned status code {code}",
"warn_the_user_about_waiting_lock": "Another YunoHost command is running right now, we are waiting for it to finish before running this one",
"warn_the_user_about_waiting_lock_again": "Still waiting...",
"warn_the_user_that_lock_is_acquired": "The other command just completed, now starting this command"
}

46
locales/eo.json Normal file
View file

@ -0,0 +1,46 @@
{
"password": "Pasvorto",
"warn_the_user_that_lock_is_acquired": "La alia komando ĵus kompletigis, nun komencante ĉi tiun komandon",
"warn_the_user_about_waiting_lock_again": "Ankoraŭ atendanta…",
"warn_the_user_about_waiting_lock": "Alia komando de YunoHost funkcias ĝuste nun, ni atendas, ke ĝi finiĝos antaŭ ol funkcii ĉi tiu",
"download_bad_status_code": "{url} redonita statuskodo {code}",
"download_unknown_error": "Eraro dum elŝutado de datumoj de {url}: {error}",
"download_timeout": "{url} prenis tro da tempo por respondi, rezignis.",
"download_ssl_error": "SSL-eraro dum konekto al {url}",
"invalid_url": "Nevalida URL{url} (ĉu ĉi tiu retejo ekzistas?)",
"error_changing_file_permissions": "Eraro dum ŝanĝo de permesoj por {path}: {error}",
"error_removing": "Eraro dum la forigo de {path}: {error}",
"error_writing_file": "Eraro skribinte dosieron {file}: {error}",
"corrupted_toml": "Korupta TOML legita el {ressource} (kialo: {error})",
"corrupted_yaml": "Korupta YAML legita de {ressource} (kialo: {error})",
"corrupted_json": "Koruptita JSON legis de {ressource} (Kialo: {error})",
"unknown_error_reading_file": "Nekonata eraro dum provi legi dosieron {file} (kialo: {error})",
"cannot_write_file": "Ne povis skribi dosieron {file} (kialo: {error})",
"cannot_open_file": "Ne povis malfermi dosieron {file} (kialo: {error})",
"websocket_request_expected": "Atendis ret-peto",
"warning": "Averto:",
"values_mismatch": "Valoroj ne kongruas",
"unknown_user": "Nekonata uzanto '{user}'",
"unknown_group": "Nekonata grupo \"{group}\"",
"unable_authenticate": "Ne eblas aŭtentiĝi",
"success": "Sukceson!",
"server_already_running": "Servilo jam funkcias sur tiu haveno",
"root_required": "Vi devas esti 'root' por plenumi ĉi tiun agon",
"pattern_not_match": "Ne kongruas kun ŝablono",
"operation_interrupted": "Operacio interrompita",
"not_logged_in": "Vi ne estas ensalutinta",
"logged_in": "Ensalutinta",
"invalid_usage": "Nevalida uzado, preterpase '--help' por vidi helpon",
"invalid_argument": "Nevalida argumento '{argument}': {error}",
"instance_already_running": "Jam funkcias YunoHost-operacio. Bonvolu atendi, ke ĝi finiĝos antaŭ ol funkcii alia.",
"info": "informoj:",
"folder_exists": "Dosierujo jam ekzistas: '{path}'",
"file_not_exist": "Dosiero ne ekzistas: '{path}'",
"error": "Eraro:",
"deprecated_command_alias": "'{prog} {old}' malakceptas kaj estos forigita estonte, uzu anstataŭe '{prog} {new}'",
"deprecated_command": "'{prog} {command}' malakceptas kaj estos forigita estonte",
"confirm": "Konfirmu {prompt}",
"authentication_required": "Aŭtentigo bezonata",
"argument_required": "Argumento '{argument}' estas bezonata",
"logged_out": "Ensalutinta"
}

View file

@ -1,176 +1,47 @@
{
"yunohost_not_installed": "YunoHost no está instalado o la instilación ha cumplido con errores. Por favor, ejecute 'yunohost tools postinstall'.",
"upgrade_complete": "La actualización se ha completado",
"installation_complete": "La instalación se ha completado",
"installation_failed": "La Instalación se ha fracasado",
"unexpected_error": "Un error ha ocurrido",
"action_invalid": "Acción inválida '{:s}'",
"license_undefined": "indefinido",
"no_appslist_found": "No se encontró ninguna lista de Apps",
"custom_appslist_name_required": "Debe proporcionar un nombre para la lista de aplicaciones personalizadas ",
"appslist_retrieve_error": "No se pudo recuperar la lista de aplicaciones a distancia ",
"appslist_fetched": "Lista de aplicaciones se trajo con éxito",
"appslist_unknown": "Lista de aplicaciones desconocidas",
"appslist_removed": "Lista de aplicaciones se eliminó con éxito",
"app_unknown": "App desconocida",
"app_no_upgrade": "Ninguna app a actualizar",
"app_not_installed": "{:s} no está instalado.",
"custom_app_url_required": " Debe proporcionar una URL para actualizar su aplicación personalizada {:s} ",
"app_recent_version_required": "{:s} requiere una versión más reciente de moulinette ",
"app_upgraded": "{:s} actualizado con éxito",
"app_upgrade_failed": "No se pudo actualizar todas las aplicaciones ",
"app_id_invalid": "id de la aplicación inválida ",
"app_already_installed": "{:s} ya está instalado ",
"app_removed": "{:s} era eliminado con éxito ",
"app_location_already_used": "Una aplicación ya está instalado en este lugar",
"app_location_install_failed": "No se pudo instalar la aplicación en esta lugar",
"app_extraction_failed": "No se pudo extraer los archivos de instalación ",
"app_install_files_invalid": "Archivos de instalación inválidos ",
"app_manifest_invalid": "Manifesto de la aplicación es inválido",
"app_sources_fetch_failed": "No se pudo descargar los archivos de códigos fuentes",
"ssowat_conf_updated": "Configuración persistente SSOwat actualizada con éxito",
"ssowat_conf_generated": "Configuración SSOwat generado con éxito ",
"mysql_db_creation_failed": "No se pudo crear el base de datos MySQL",
"mysql_db_init_failed": "No se pudo inicializar el base de datos MySQL.",
"mysql_db_initialized": "Base de datos MySQL inicializado con éxito",
"extracting": "Extrayendo...",
"downloading": "Descargando...",
"executing_script": "Ejecutando script...",
"done": "Completo.",
"path_removal_failed": "No se pudo quitar la ruta {:s}",
"domain_unknown": "Dominio desconocido",
"domain_dyndns_invalid": "El dominio no es valido para usar con DynDNS",
"domain_dyndns_already_subscribed": "Ya te has suscrito a un dominio DynDNS.",
"domain_dyndns_root_unknown": "Dominio raíz DynDNS desconocido ",
"domain_cert_gen_failed": "No se pudo crear certificado",
"domain_exists": "El dominio ya existe",
"dnsmasq_isnt_installed": "Parece que dnsmasq no está instalado, por favor, ejecuta 'apt-get remove bind9 && apt-get install dnsmasq'",
"domain_zone_exists": "El archivo de zonas DNS ya existe.",
"domain_zone_not_found": "Archivo de zonas DNS por el dominio [:s] no estaba encontrado",
"domain_creation_failed": "No se pudo crear el dominio",
"domain_created": "Dominio creado con éxito.",
"domain_uninstall_app_first": "Uno o más apps están instalados en este dominio. Por favor, desinstalarlos antes de quitar este dominio.",
"domain_deletion_failed": "No se pudo borrar el dominio.",
"domain_deleted": "Dominio borrado con éxito.",
"no_internet_connection": "El servidor no está conectado al Internet.",
"dyndns_key_generating": "Generación del llave de DNS está en curso. Este podría durar unos momentos...",
"dyndns_unavailable": "Subdominio DynDNS no disponible",
"dyndns_registration_failed": "No se pudo registrar el dominio DynDNS: {:s}",
"dyndns_registered": "El dominio DynDNS era registrado con éxito.",
"dyndns_ip_update_failed": "No se pudo actualizar la dirección IP en DynDNS",
"dyndns_ip_updated": "La dirección IP era actualizado en DynDNS con éxito",
"dyndns_cron_installed": "El trabajo cron de DynDNS se ha instalado con éxito",
"dyndns_cron_remove_failed": "No se pudo quitar el trabajo cron DynDNS",
"dyndns_cron_removed": "Trabajo cron DynDNS se quitó con éxito",
"port_available": "El puerto {} está disponible",
"port_unavailable": "El puerto {} no está disponible",
"port_already_opened": "El puerto {} ya está abierto por {:s} connecciones",
"port_already_closed": "El puerto {} ya está cerrado por {:s} connecciones.",
"iptables_unavailable": "No puedes modificar los iptables aquí. Eres en un contenedor o su kernel no soporte este opción.",
"ip6tables_unavailable": "No puedes modificar los ip6tables aquí. Eres en un contenedor o su kernel no soporte este opción.",
"upnp_dev_not_found": "No se encontró ninguno dispositivo UPnP ",
"upnp_port_open_failed": "No se pudo abrir puertos por UPnP",
"upnp_enabled": "UPnP activado con éxito",
"upnp_disabled": "UPnP deshabilitado con éxito",
"firewall_rules_cmd_failed": "Algunos reglas del cortafuegos han fracasado. Para más información, vea al archivo historial.",
"firewall_reload_failed": "No se pudo recargar el cortafuegos",
"firewall_reloaded": "Cortafuegos recargado con éxito",
"hook_list_by_invalid": "La propiedad de este hook es inválida",
"hook_name_unknown": "Hook desconocido '{:s}'",
"hook_choice_invalid": "Selección inválida '{:s}'",
"hook_argument_missing": "Falta un parámetro '{:s}'",
"mountpoint_unknown": "Punto de montaje desconocido",
"unit_unknown": "Unidad '{:s}' desconocido",
"monitor_period_invalid": "Período de tiempo inválido",
"monitor_stats_no_update": "No hay ninguna estadísticos de la supervisión del sistema a realizar",
"monitor_stats_file_not_found": "No se pudo encontrar el archivo de estadísticos",
"monitor_stats_period_unavailable": "No hay estadísticos del período del tiempo",
"monitor_enabled": "Supervisión del sistema activado con éxito",
"monitor_disabled": "Supervisión del sistema era desactivado con éxito",
"monitor_not_enabled": "Supervisión del sistema no está activado",
"monitor_glances_con_failed": "No se pudo conectar al servidor de Glances",
"service_unknown": "Servicio desconocido '{:s}'",
"service_add_failed": "No se pudo añadir el servicio '{:s}'",
"service_added": "Servicio añadido con éxito",
"service_remove_failed": "No se pudo quitar el servicio '{:s}'",
"service_removed": "Servicio quitado con éxito",
"service_start_failed": "No se pudo empezar el servicio '{:s}'",
"service_already_started": "El servicio '{:s}' ya se ha empezado",
"service_started": "El servicio '{:s}' se empezó con éxito",
"service_stop_failed": "No se pudo parar el servicio '{:s}'",
"service_already_stopped": "El servicio '{:s}' ya está parado ",
"service_stopped": "Servicio '{:s}' parado con éxito",
"service_enable_failed": "No se pudo activar el servicio '{:s}'",
"service_enabled": "Servicio '{:s}' activado con éxito",
"service_disable_failed": "No se pudo desactivar el servicio '{:s}'",
"service_disabled": "Servicio '{:s}' desactivado con éxito",
"service_status_failed": "No se pudo discernir el estado del servicio '{:s}'",
"service_no_log": "No hay archivo historial del servicio '{:s}' a exhibir",
"service_cmd_exec_failed": "No se pudo ejecutar comando '{:s}'",
"ldap_initialized": "LDAP se inició con éxito",
"admin_password_change_failed": "No se pudo cambiar la contraseña",
"admin_password_changed": "Contraseña administrativa se cambió con éxito",
"new_domain_required": "Debe proporcionar el dominio principal nuevo",
"maindomain_change_failed": "No se pudo cambiar el dominio principal",
"maindomain_changed": "Dominio principal se cambió con éxito",
"yunohost_installing": "Instalando YunoHost...",
"yunohost_already_installed": "YunoHost ya está instalado",
"yunohost_ca_creation_failed": "No se pudo crear un autoridad de certificación nuevo",
"yunohost_configured": "YunoHost se configuró con éxito.",
"updating_apt_cache": "Actualizando la lista de paquetes disponibles...",
"update_cache_failed": "No se pudo actualizar el cache APT",
"packages_no_upgrade": "No hay actualización por ningun paquete",
"packages_upgrade_critical_later": "Los paquetes críticos ({:s}) se actualizarán más tarde",
"upgrading_packages": "Actualizando paquetes...",
"packages_upgrade_failed": "No se pudo actualizar todo de los paquetes",
"system_upgraded": "Actualización del sistema se ha completado con éxito.",
"backup_output_directory_required": "Debe proporcionar un directorio de salida para el backup",
"backup_output_directory_forbidden": "Carpeta de salida prohibida",
"backup_output_directory_not_empty": "La carpeta de salida no está vacía",
"backup_running_hooks": "Ejecutando los hooks de backup...",
"backup_creating_archive": "Creando el archivo backup...",
"backup_extracting_archive": "Extrayendo el archivo backup...",
"backup_archive_open_failed": "No se pudo abrir el archivo backup",
"backup_archive_name_unknown": "El nombre archivo local de backup está desconocido",
"backup_archive_name_exists": "Un archivo ya existe con el nombre del archivo de backup",
"backup_complete": "El backup se ha completado",
"backup_invalid_archive": "Archivo de backup es inválido",
"restore_confirm_yunohost_installed": "Estás seguro que quieres restaurar a un sistema que ya está instalado? [{answers:s}]",
"restore_running_hooks": "Ejecutando hooks de restauración...",
"restore_failed": "No se pudo restaurar el sistema",
"restore_complete": "Restauración se ha completado",
"unbackup_app": "La App '{:s}' no será guardada",
"unrestore_app": "La App '{:s}' no será restaurada",
"field_invalid": "Campo inválido '{:s}'",
"mail_domain_unknown": "El dominio de correos '{:s}' es desconocido",
"mail_alias_remove_failed": "No se pudo quitar el alias de correos '{:s}'",
"mail_forward_remove_failed": "No se pudo quitar la reenvía de correos '{:s}'",
"user_unknown": "usuario desconocido",
"system_username_exists": "Nombre de usuario ya existe en los usuarios del sistema",
"user_creation_failed": "No se pudo crear un usuario nuevo",
"user_created": "Usuario creado con éxito",
"user_deletion_failed": "No se pudo quitar el usuario",
"user_deleted": "Usuario creado con éxito",
"user_update_failed": "No se pudo actualizar el usuario",
"user_updated": "Usuario actualizado con éxito",
"user_info_failed": "No se pudo traer la información del usuario. ",
"admin_password": "Contraseña administrativa",
"ask_firstname": "Nombre",
"ask_lastname": "Apellido",
"ask_email": "Correo electrónico",
"ask_password": "Contraseña",
"ask_current_admin_password": "Contraseña administrativa presente",
"ask_new_admin_password": "Contraseña administrativa nueva",
"ask_main_domain": "Dominio principal",
"ask_list_to_remove": "Lista a quitar",
"pattern_username": "Debe contener solamente caracteres alfanuméricos o la guion bajo",
"pattern_firstname": "Debe ser un nombre válido",
"pattern_lastname": "Debe ser un apellido válido",
"pattern_email": "Debe ser una direccion de email válido (e.g. alguien@dominio.org)",
"pattern_password": "Debe ser a menos de 3 caracteres",
"pattern_domain": "El nombre de dominio debe ser válido (e.g. mi-dominio.org)",
"pattern_listname": "Los caracteres deben ser alfanuméricos o el guion bajo.",
"pattern_port": "El numéro del puerto debe ser válido (i.e. 0-65535)",
"pattern_port_or_range": "El numéro del puerto debe ser válido (i.e. 0-65535) o un intervalo de puertos (e.g. 100:200)",
"pattern_backup_archive_name": "Debe que ser un nombre de archivo válido con los caracteres alfanumericos, o los -_."
"argument_required": "Se requiere el argumento «{argument}»",
"authentication_required": "Se requiere autentificación",
"confirm": "Confirmar {prompt}",
"deprecated_command": "«{prog} {command}» está obsoleto y será eliminado en el futuro",
"deprecated_command_alias": "«{prog} {old}» está obsoleto y se eliminará en el futuro, use «{prog} {new}» en su lugar",
"error": "Error:",
"file_not_exist": "El archivo no existe: «{path}»",
"folder_exists": "El directorio ya existe: «{path}»",
"instance_already_running": "Ya se está ejecutando una instancia de YunoHost. Espere a que termine antes de ejecutar otra.",
"invalid_argument": "Argumento no válido «{argument}»: {error}",
"invalid_usage": "Uso no válido, utilice --help para ver la ayuda",
"logged_in": "Sesión iniciada",
"logged_out": "Sesión cerrada",
"not_logged_in": "No ha iniciado sesión",
"operation_interrupted": "Operación interrumpida",
"password": "Contraseña",
"pattern_not_match": "No coincide con el patrón",
"root_required": "Solo root puede realizar esta acción",
"server_already_running": "Ya se está ejecutando un servidor en ese puerto",
"success": "¡Éxito!",
"unable_authenticate": "No se puede autentificar",
"unknown_group": "Grupo «{group}» desconocido",
"unknown_user": "Usuario «{user}» desconocido",
"values_mismatch": "Los valores no coinciden",
"warning": "Advertencia:",
"websocket_request_expected": "Se esperaba una petición WebSocket",
"cannot_open_file": "No se pudo abrir el archivo {file} (motivo: {error})",
"cannot_write_file": "No se pudo escribir el archivo {file} (motivo: {error})",
"unknown_error_reading_file": "Error desconocido al intentar leer el archivo {file} (motivo: {error})",
"corrupted_json": "Lectura corrupta de JSON desde {ressource} (motivo: {error})",
"error_writing_file": "Error al escribir el archivo {file}: {error}",
"error_removing": "Error al eliminar {path}: {error}",
"error_changing_file_permissions": "Error al cambiar los permisos para {path}: {error}",
"invalid_url": "Imposible de conectarse a {url} (¿ la URL esta correcta, existe este sitio, o internet esta accesible?).",
"download_ssl_error": "Error SSL al conectar con {url}",
"download_timeout": "{url} tardó demasiado en responder, abandono.",
"download_unknown_error": "Error al descargar datos desde {url} : {error}",
"download_bad_status_code": "{url} devolvió el código de estado {code}",
"corrupted_yaml": "Lectura corrupta de YAML desde {ressource} (motivo: {error})",
"info": "Información:",
"corrupted_toml": "Lectura corrupta de TOML desde {ressource} (motivo: {error})",
"warn_the_user_that_lock_is_acquired": "La otra orden recién terminó, iniciando esta orden ahora",
"warn_the_user_about_waiting_lock_again": "Aún esperando…",
"warn_the_user_about_waiting_lock": "Otra orden de YunoHost se está ejecutando ahora, estamos esperando a que termine antes de ejecutar esta",
"edit_text_question": "{}. Editar este texto ? [sN]: "
}

47
locales/eu.json Normal file
View file

@ -0,0 +1,47 @@
{
"argument_required": "'{argument}' argumentua ezinbestekoa da",
"logged_out": "Saioa amaituta",
"password": "Pasahitza",
"authentication_required": "Autentifikazioa behar da",
"confirm": "{prompt} baieztatu",
"edit_text_question": "{}. Testua editatu nahi al duzu? [yN]: ",
"deprecated_command": "'{prog} {command}' zaharkitua dago eta etorkizunean kenduko da",
"deprecated_command_alias": "'{prog} {old} zaharkitua dago eta etorkizunean kenduko da, erabili '{prog} {new}' haren ordez",
"error": "Errorea:",
"file_not_exist": "Fitxategia ez da existitzen: '{path}'",
"error_changing_file_permissions": "Errorea {path}-i eragiten dioten baimenak aldatzean: {error}",
"invalid_argument": "'{argument}' argumentua ez da egokia: {error}",
"success": "Arrakasta!",
"info": "Informazioa:",
"logged_in": "Saioa hasita",
"not_logged_in": "Ez duzu saiorik hasi",
"operation_interrupted": "Eragiketa geldiarazi da",
"unknown_group": "'{group}' taldea ezezaguna da",
"unknown_user": "'{user}' erabiltzailea ezezaguna da",
"cannot_write_file": "Ezinezkoa izan da {file} fitxategia idaztea (zergatia: {error})",
"download_ssl_error": "SSL errorea {url}-(e)ra konektatzean",
"corrupted_toml": "{ressource}-eko/go TOMLa kaltetuta dago (zergatia: {error})",
"warn_the_user_about_waiting_lock": "YunoHosten beste komando bat ari da exekutatzen, horrek amaitu arte gaude zain",
"warn_the_user_about_waiting_lock_again": "Zain...",
"folder_exists": "Direktorioa existitzen da dagoeneko: '{path}'",
"instance_already_running": "YunoHosten eragiketa bat exekutatzen ari da dagoeneko. Itxaron amaitu arte beste eragiketa bat abiarazi baino lehen.",
"invalid_usage": "Erabilera okerra, idatzi --help aukerak ikusteko",
"pattern_not_match": "Ez dator ereduarekin bat",
"root_required": "Ezinbestekoa da 'root' izatea eragiketa hau exekutatzeko",
"server_already_running": "Zerbitzari bat martxan dago dagoeneko ataka horretan",
"unable_authenticate": "Ezin da autentifikatu",
"values_mismatch": "Balioak ez datoz bat",
"warning": "Adi:",
"cannot_open_file": "Ezinezkoa izan da {file} fitxategia irekitzea (zergatia: {error})",
"corrupted_json": "{ressource}-eko/go JSONa kaltetuta dago (zergatia: {error})",
"corrupted_yaml": "{ressource}-eko/go YAMLa kaltetuta dago (zergatia: {error})",
"websocket_request_expected": "WebSocket eskaera bat espero zen",
"unknown_error_reading_file": "Errore ezezaguna {file} fitxategia irakurtzen saiatzerakoan (zergatia: {error})",
"download_unknown_error": "Errorea {url}(e)tik deskargatzerakoan: {error}",
"warn_the_user_that_lock_is_acquired": "Aurreko komandoa amaitu berri, orain komando hau abiarazten",
"error_writing_file": "Errorea {file} fitxategia idazterakoan: {error}",
"error_removing": "Errorea {path} ezabatzerakoan: {error}",
"download_bad_status_code": "{url} helbideak {code} egoera kodea agertu du",
"invalid_url": "{url}-(e)ra konektatzeak huts egin du... agian zerbitzua ez dago martxan, edo ez zaude IPv4/IPv6 bidez ondo konektatuta internetera.",
"download_timeout": "{url}(e)k denbora gehiegi behar izan du erantzuteko, bertan behera utzi du zerbitzariak."
}

46
locales/fa.json Normal file
View file

@ -0,0 +1,46 @@
{
"logged_in": "وارد شده",
"invalid_usage": "استفاده نامعتبر ، برای مشاهده راهنما --help را ارسال کنید",
"invalid_argument": "استدلال نامعتبر '{argument}': {error}",
"instance_already_running": "در حال حاضر یک عملیات YunoHost در حال اجرا است. لطفاً قبل از اجرای یکی دیگر ، منتظر بمانید تا آن به پایان برسد.",
"info": "اطلاعات:",
"folder_exists": "پوشه موجود است: '{path}'",
"file_not_exist": "فایل وجود ندارد: '{path}'",
"error": "ایراد:",
"deprecated_command_alias": "'{prog} {old}' منسوخ شده است و در آینده حذف خواهد شد ، بجای آن از '{prog} {new}' استفاده کنید",
"deprecated_command": "'{prog} {command}' منسوخ شده است و در آینده حذف خواهد شد",
"confirm": "تایید کردن {prompt}",
"authentication_required": "احراز هویّت الزامی است",
"argument_required": "استدلال '{argument}' ضروری است",
"password": "کلمه عبور",
"warn_the_user_that_lock_is_acquired": "فرمان دیگر به تازگی تکمیل شده است ، اکنون این دستور را شروع کنید",
"warn_the_user_about_waiting_lock_again": "هنوز در انتظار…",
"warn_the_user_about_waiting_lock": "یکی دیگر از دستورات YunoHost در حال اجرا است ، ما منتظر هستیم تا قبل از اجرای این دستور به پایان برسد",
"download_bad_status_code": "{url} کد وضعیّت بازگشتی {code}",
"download_unknown_error": "خطا هنگام بارگیری داده ها از {url}: {error}",
"download_timeout": "پاسخ {url} خیلی طول کشید ، منصرف شو.",
"download_ssl_error": "خطای SSL هنگام اتصال به {url}",
"invalid_url": "اتصال به {url} انجام نشد … شاید سرویس خاموش باشد یا در IPv4/IPv6 به درستی به اینترنت متصل نشده باشید.",
"error_changing_file_permissions": "خطا هنگام تغییر مجوزهای {path}: {error}",
"error_removing": "خطا هنگام حذف {path}: {error}",
"error_writing_file": "خطا هنگام نوشتن فایل {file}: {error}",
"corrupted_toml": "TOML خراب از {ressource} (دلیل: {error})",
"corrupted_yaml": "YAML خراب از {ressource} (دلیل: {error})",
"corrupted_json": "جی سان خراب شده از {ressource} میخواند (دلیل: {error})",
"unknown_error_reading_file": "خطای ناشناخته هنگام تلاش برای خواندن فایل {file} (دلیل: {error})",
"cannot_write_file": "نمی توان فایل {file} را نوشت (دلیل: {error})",
"cannot_open_file": "فایل {file} باز نشد (دلیل: {error})",
"websocket_request_expected": "در انتظار درخواست وب سوکت",
"warning": "هشدار:",
"values_mismatch": "مقدار ها مطابقت ندارند",
"unknown_user": "کاربر'{user}' ناشناخته",
"unknown_group": "گروه '{group}' ناشناخته",
"unable_authenticate": "احراز هویّت امکان پذیر نیست",
"success": "موفقیّت!",
"server_already_running": "در حال حاضر یک سرور روی آن پورت کار می کند",
"root_required": "برای انجام این عمل باید کاربر ریشه باشید",
"pattern_not_match": "با الگو مطابقت ندارد",
"operation_interrupted": "عملیات قطع شده است",
"not_logged_in": "‌شما وارد نشده اید",
"logged_out": "خارج شده"
}

4
locales/fi.json Normal file
View file

@ -0,0 +1,4 @@
{
"password": "Salasana",
"logged_out": "Kirjauduttu ulos"
}

View file

@ -1,177 +1,47 @@
{
"yunohost_not_installed": "YunoHost n'est pas ou pas correctement installé. Veuillez exécuter 'yunohost tools postinstall'.",
"upgrade_complete": "Mise à jour terminée",
"installation_complete": "Installation terminée",
"installation_failed": "Échec de l'installation",
"unexpected_error": "Une erreur inattendue est survenue",
"action_invalid": "Action '{:s}' incorrecte",
"license_undefined": "indéfinie",
"no_appslist_found": "Aucune liste d'applications trouvée",
"custom_appslist_name_required": "Vous devez spécifier un nom pour votre liste d'applications personnalisée",
"appslist_retrieve_error": "Impossible de récupérer la liste d'applications distante",
"appslist_fetched": "Liste d'applications récupérée avec succès",
"appslist_unknown": "Liste d'applications inconnue",
"appslist_removed": "Liste d'applications supprimée avec succès",
"app_unknown": "Application inconnue",
"app_no_upgrade": "Aucune application à mettre à jour",
"app_not_installed": "{:s} n'est pas installé",
"custom_app_url_required": "Vous devez spécifier une URL pour mettre à jour votre application locale {:s}",
"app_recent_version_required": "{:s} nécessite une version plus récente de la moulinette",
"app_upgraded": "{:s} mis à jour avec succès",
"app_upgrade_failed": "Impossible de mettre à jour toutes les applications",
"app_id_invalid": "Id d'application incorrect",
"app_already_installed": "{:s} est déjà installé",
"app_removed": "{:s} supprimé avec succès",
"app_location_already_used": "Une application est déjà installée à cet emplacement",
"app_location_install_failed": "Impossible d'installer l'application à cet emplacement",
"app_extraction_failed": "Impossible d'extraire les fichiers d'installation",
"app_install_files_invalid": "Fichiers d'installation incorrects",
"app_manifest_invalid": "Manifeste d'application incorrect",
"app_sources_fetch_failed": "Impossible de récupérer les fichiers sources",
"ssowat_conf_updated": "Configuration persistante de SSOwat mise à jour avec succès",
"ssowat_conf_generated": "Configuration de SSOwat générée avec succès",
"mysql_db_creation_failed": "Impossible de créer la base de donnée MySQL",
"mysql_db_init_failed": "Impossible d'initialiser la base de donnée MySQL",
"mysql_db_initialized": "Base de donnée MySQL initialisée avec succès",
"extracting": "Extraction...",
"downloading": "Téléchargement...",
"executing_script": "Exécution du script...",
"done": "Terminé.",
"path_removal_failed": "Impossible de supprimer le chemin {:s}",
"domain_unknown": "Domaine inconnu",
"domain_dyndns_invalid": "Domaine incorrect pour un usage avec DynDNS",
"domain_dyndns_already_subscribed": "Vous avez déjà souscris à un domaine DynDNS",
"domain_dyndns_root_unknown": "Domaine DynDNS principal inconnu",
"domain_cert_gen_failed": "Impossible de générer le certificat",
"domain_exists": "Le domaine existe déjà",
"dnsmasq_isnt_installed": "dnsmasq ne semble pas être installé, veuillez lancer 'apt-get remove bind9 && apt-get install dnsmasq'",
"domain_zone_exists": "Le fichier de zone DNS existe déjà",
"domain_zone_not_found": "Fichier de zone DNS introuvable pour le domaine {:s}",
"domain_creation_failed": "Impossible de créer le domaine",
"domain_created": "Domaine créé avec succès",
"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_deletion_failed": "Impossible de supprimer le domaine",
"domain_deleted": "Domaine supprimé avec succès",
"no_internet_connection": "Le serveur n'est pas connecté à Internet",
"dyndns_key_generating": "La clé DNS est en cours de génération, cela peut prendre du temps...",
"dyndns_unavailable": "Sous-domaine DynDNS indisponible",
"dyndns_registration_failed": "Impossible d'enregistrer le domaine DynDNS : {:s}",
"dyndns_registered": "Domaine DynDNS enregistré avec succès",
"dyndns_ip_update_failed": "Impossible de mettre à jour l'adresse IP sur le domaine DynDNS",
"dyndns_ip_updated": "Adresse IP mise à jour avec succès sur le domaine DynDNS",
"dyndns_cron_installed": "Tâche cron pour DynDNS installée avec succès",
"dyndns_cron_remove_failed": "Impossible d'enlever la tâche cron pour DynDNS",
"dyndns_cron_removed": "Tâche cron pour DynDNS enlevée avec succès",
"port_available": "Le port {} est disponible",
"port_unavailable": "Le port {} n'est pas disponible",
"port_already_opened": "Le port {} est déjà ouvert pour les connexions {:s}",
"port_already_closed": "Le port {} est déjà fermé pour les connexions {:s}",
"iptables_unavailable": "Vous ne pouvez pas faire joujou avec iptables ici. Vous êtes sûrement dans un conteneur, autrement votre noyau ne le supporte pas.",
"ip6tables_unavailable": "Vous ne pouvez pas faire joujou avec ip6tables ici. Vous êtes sûrement dans un conteneur, autrement votre noyau ne le supporte pas.",
"upnp_dev_not_found": "Aucun périphérique compatible UPnP trouvé",
"upnp_port_open_failed": "Impossible d'ouvrir les ports avec UPnP",
"upnp_enabled": "UPnP activé avec succès",
"upnp_disabled": "UPnP désactivé avec succès",
"firewall_rules_cmd_failed": "Certaines règles du pare-feu n'ont pas pu être appliquées. Pour plus d'informations, consultez le journal.",
"firewall_reload_failed": "Impossible de recharger le pare-feu",
"firewall_reloaded": "Pare-feu rechargé avec succès",
"hook_list_by_invalid": "Propriété pour lister les scripts incorrecte",
"hook_name_unknown": "Nom de script '{:s}' inconnu",
"hook_choice_invalid": "Choix incorrect : '{:s}'",
"hook_argument_missing": "Argument manquant : '{:s}'",
"mountpoint_unknown": "Point de montage inconnu",
"unit_unknown": "Unité '{:s}' inconnue",
"monitor_period_invalid": "Période de temps incorrect",
"monitor_stats_no_update": "Aucune donnée de l'état du serveur à mettre à jour",
"monitor_stats_file_not_found": "Fichier de données de l'état du serveur introuvable",
"monitor_stats_period_unavailable": "Aucune donnée de l'état du serveur disponible pour la période",
"monitor_enabled": "Suivi de l'état du serveur activé avec succès",
"monitor_disabled": "Suivi de l'état du serveur désactivé avec succès",
"monitor_not_enabled": "Le suivi de l'état du serveur n'est pas activé",
"monitor_glances_con_failed": "Impossible de se connecter au serveur Glances",
"service_unknown": "Service '{:s}' inconnu",
"service_add_failed": "Impossible d'ajouter le service '{:s}'",
"service_added": "Service ajouté avec succès",
"service_remove_failed": "Impossible d'enlever le service '{:s}'",
"service_removed": "Service enlevé avec succès",
"service_start_failed": "Impossible de démarrer le service '{:s}'",
"service_already_started": "Le service '{:s}' est déjà démarré",
"service_started": "Service '{:s}' démarré avec succès",
"service_stop_failed": "Impossible d'arrêter le service '{:s}'",
"service_already_stopped": "Le service '{:s}' est déjà arrêté",
"service_stopped": "Service '{:s}' arrêté avec succès",
"service_enable_failed": "Impossible d'activer le service '{:s}'",
"service_enabled": "Service '{:s}' activé avec succès",
"service_disable_failed": "Impossible de désactiver le service '{:s}'",
"service_disabled": "Service '{:s}' désactivé avec succès",
"service_status_failed": "Impossible de déterminer le statut du service '{:s}'",
"service_no_log": "Aucun journal a afficher pour le service '{:s}'",
"service_cmd_exec_failed": "Impossible d'exécuter la commande '{:s}'",
"ldap_initialized": "Répertoire LDAP initialisé avec succès",
"admin_password_change_failed": "Impossible de modifier le mot de passe d'administration",
"admin_password_changed": "Mot de passe d'administration modifié avec succès",
"new_domain_required": "Vous devez spécifier le nouveau domaine principal",
"maindomain_change_failed": "Impossible de modifier le domaine principal",
"maindomain_changed": "Domaine principal modifié avec succès",
"yunohost_installing": "Installation de YunoHost...",
"yunohost_already_installed": "YunoHost est déjà installé",
"yunohost_ca_creation_failed": "Impossible de créer l'autorité de certification",
"yunohost_configured": "YunoHost configuré avec succès",
"updating_apt_cache": "Mise à jour de la liste des paquets disponibles...",
"update_cache_failed": "Impossible de mettre à jour le cache de l'APT",
"packages_no_upgrade": "Il n'y a aucun paquet à mettre à jour",
"packages_upgrade_critical_later": "Les paquets critiques ({:s}) seront mis à jour plus tard",
"upgrading_packages": "Mise à jour des paquets...",
"packages_upgrade_failed": "Impossible de mettre à jour tous les paquets",
"system_upgraded": "Système mis à jour avec succès",
"backup_output_directory_required": "Vous devez spécifier un dossier de sortie pour la sauvegarde",
"backup_output_directory_forbidden": "Dossier de sortie interdit",
"backup_output_directory_not_empty": "Le dossier de sortie n'est pas vide",
"backup_running_hooks": "Exécution des scripts de sauvegarde...",
"backup_creating_archive": "Création de l'archive de sauvegarde...",
"backup_extracting_archive": "Extraction de l'archive de sauvegarde...",
"backup_archive_open_failed": "Impossible d'ouvrir l'archive de sauvegarde",
"backup_archive_name_unknown": "Nom d'archive de sauvegarde locale inconnu",
"backup_archive_name_exists": "Une archive de sauvegarde avec ce nom existe déjà",
"backup_complete": "Sauvegarde terminée",
"backup_invalid_archive": "Archive de sauvegarde incorrecte",
"restore_confirm_yunohost_installed": "Voulez-vous vraiment restaurer un système déjà installé ? [{answers:s}]",
"restore_running_hooks": "Exécution des scripts de restauration...",
"restore_failed": "Impossible de restaurer le système",
"restore_complete": "Restauration terminée",
"unbackup_app": "L'application '{:s}' ne sera pas sauvegardée",
"unrestore_app": "L'application '{:s}' ne sera pas restaurée",
"field_invalid": "Champ incorrect : {:s}",
"mail_domain_unknown": "Domaine '{:s}' de l'adresse mail inconnu",
"mail_alias_remove_failed": "Impossible de supprimer l'adresse mail supplémentaire '{:s}'",
"mail_forward_remove_failed": "Impossible de supprimer l'adresse mail de transfert '{:s}'",
"system_username_exists": "Le nom d'utilisateur existe déjà dans les utilisateurs système",
"user_unknown": "Utilisateur inconnu",
"user_creation_failed": "Impossible de créer l'utilisateur",
"user_created": "Utilisateur créé avec succès",
"user_deletion_failed": "Impossible de supprimer l'utilisateur",
"user_deleted": "Utilisateur supprimé avec succès",
"user_update_failed": "Impossible de modifier l'utilisateur",
"user_updated": "Utilisateur modifié avec succès",
"user_info_failed": "Impossible de récupérer les informations de l'utilisateur",
"admin_password": "Mot de passe d'administration",
"ask_firstname": "Prénom",
"ask_lastname": "Nom",
"ask_email": "Adresse mail",
"ask_password": "Mot de passe",
"ask_current_admin_password": "Mot de passe d'administration actuel",
"ask_new_admin_password": "Nouveau mot de passe d'administration",
"ask_main_domain": "Domaine principal",
"ask_list_to_remove": "Liste à supprimer",
"pattern_username": "Doit être composé uniquement de caractères alphanumérique minuscule et de tiret bas",
"pattern_firstname": "Doit être un prénom valide",
"pattern_lastname": "Doit être un nom valide",
"pattern_email": "Doit être une adresse mail valide (ex. : someone@domain.org)",
"pattern_password": "Doit être composé d'au moins 3 caractères",
"pattern_domain": "Doit être un nom de domaine valide (ex : mon-domaine.org)",
"pattern_listname": "Doit être composé uniquement de caractères alphanumérique et de tiret bas",
"pattern_port": "Doit être un numéro de port valide (0-65535)",
"pattern_port_or_range": "Doit être un numéro de port valide (0-65535) ou une gamme de ports (ex : 100:200)",
"pattern_backup_archive_name": "Doit être un nom de fichier valide composé de caractères alphanumérique et -_. uniquement",
"format_datetime_short": "%d/%m/%Y %H:%M"
"argument_required": "L'argument '{argument}' est requis",
"authentication_required": "Authentification requise",
"confirm": "Confirmez {prompt}",
"deprecated_command": "'{prog} {command}' est déprécié et sera bientôt supprimé",
"deprecated_command_alias": "'{prog} {old}' est déprécié et sera bientôt supprimé, utilisez '{prog} {new}' à la place",
"error": "Erreur:",
"file_not_exist": "Le fichier '{path}' n'existe pas",
"folder_exists": "Le dossier existe déjà: '{path}'",
"instance_already_running": "Une instance est déjà en cours d'exécution, merci d'attendre sa fin avant d'en lancer une autre.",
"invalid_argument": "Argument '{argument}' incorrect: {error}",
"invalid_usage": "Utilisation erronée, utilisez --help pour accéder à l'aide",
"logged_in": "Connecté",
"logged_out": "Déconnecté",
"not_logged_in": "Vous n'êtes pas connecté",
"operation_interrupted": "Opération interrompue",
"password": "Mot de passe",
"pattern_not_match": "Ne correspond pas au motif",
"root_required": "Vous devez avoir les droits d'administration pour exécuter cette action",
"server_already_running": "Un serveur est déjà en cours d'exécution sur ce port",
"success": "Succès!",
"unable_authenticate": "Impossible de vous authentifier",
"unknown_group": "Le groupe '{group}' est inconnu",
"unknown_user": "Le compte '{user}' est inconnu",
"values_mismatch": "Les valeurs ne correspondent pas",
"warning": "Attention:",
"websocket_request_expected": "Une requête WebSocket est attendue",
"cannot_open_file": "Impossible d'ouvrir le fichier {file} (raison: {error})",
"cannot_write_file": "Ne peut pas écrire le fichier {file} (raison: {error})",
"unknown_error_reading_file": "Erreur inconnue en essayant de lire le fichier {file} (raison:{error})",
"corrupted_json": "Fichier JSON corrompu en lecture depuis {ressource} (raison: {error})",
"error_writing_file": "Erreur en écrivant le fichier {file}: {error}",
"error_removing": "Erreur lors de la suppression {path}: {error}",
"error_changing_file_permissions": "Erreur lors de la modification des autorisations pour {path}: {error}",
"invalid_url": "Impossible de se connecter à {url}... peut-être que le service est en panne ou que vous n'êtes pas correctement connecté à Internet en IPv4/IPv6.",
"download_ssl_error": "Erreur SSL lors de la connexion à {url}",
"download_timeout": "{url} a pris trop de temps pour répondre: abandon.",
"download_unknown_error": "Erreur lors du téléchargement des données à partir de {url}: {error}",
"download_bad_status_code": "{url} renvoie le code d'état {code}",
"corrupted_yaml": "Fichier YAML corrompu en lecture depuis {ressource} (raison: {error})",
"info": "Info:",
"corrupted_toml": "Fichier TOML corrompu en lecture depuis {ressource} (raison: {error})",
"warn_the_user_about_waiting_lock": "Une autre commande YunoHost est actuellement en cours, nous attendons qu'elle se termine avant de démarrer celle là",
"warn_the_user_about_waiting_lock_again": "Toujours en attente...",
"warn_the_user_that_lock_is_acquired": "La commande précédente vient de se terminer, lancement de cette nouvelle commande",
"edit_text_question": "{}. Modifier ce texte? [yN]: "
}

47
locales/gl.json Normal file
View file

@ -0,0 +1,47 @@
{
"invalid_usage": "Uso non válido, pass --help para ver a axuda",
"invalid_argument": "Argumento non válido '{argument}': {error}",
"instance_already_running": "Hai unha operación de YunoHost en execución. Por favor agarda a que remate antes de realizar unha nova.",
"info": "Info:",
"folder_exists": "Xa existe o cartafol: '{path}'",
"file_not_exist": "Non existe o ficheiro: '{path}'",
"error": "Erro:",
"deprecated_command_alias": "'{prog} {old}' xa non se utiliza e será eliminado no futuro, usa '{prog} {new}' no seu lugar",
"deprecated_command": "'{prog} {command}' xa non se utiliza e xa non se usará no futuro",
"confirm": "Confirma {prompt}",
"authentication_required": "Autenticación requerida",
"argument_required": "O argumento '{argument}' é requerido",
"logged_out": "Sesión pechada",
"password": "Contrasinal",
"warning": "Aviso:",
"values_mismatch": "Non concordan os valores",
"unknown_user": "Usuaria '{user}' descoñecida",
"unknown_group": "Grupo '{group}' descoñecido",
"unable_authenticate": "Non se puido autenticar",
"success": "Ben feito!",
"server_already_running": "Xa hai un servidor a funcionar nese porto",
"root_required": "Tes que ser root para facer esta acción",
"pattern_not_match": "Non concorda co patrón",
"operation_interrupted": "Interrumpeuse a operación",
"not_logged_in": "Non iniciaches sesión",
"logged_in": "Sesión iniciada",
"warn_the_user_that_lock_is_acquired": "O outro comando rematou, agora executarase este",
"warn_the_user_about_waiting_lock_again": "Agardando…",
"warn_the_user_about_waiting_lock": "Estase executando outro comando de YunoHost neste intre, estamos agardando a que remate para executar este",
"download_bad_status_code": "{url} devolveu o código de estado {code}",
"download_unknown_error": "Erro ao descargar os datos desde {url}: {error}",
"download_timeout": "{url} está tardando en responder, deixámolo.",
"download_ssl_error": "Erro SSL ao conectar con {url}",
"invalid_url": "Fallou a conexión con {url}... pode que o servizo estea caído, ou que non teñas conexión a Internet con IPv4/IPv6.",
"error_changing_file_permissions": "Erro ao cambiar os permisos de {path}: {error}",
"error_removing": "Erro ao eliminar {path}: {error}",
"error_writing_file": "Erro ao escribir o ficheiro {file}: {error}",
"corrupted_toml": "Lectura corrupta de datos TOML de {ressource} (razón: {error})",
"corrupted_yaml": "Lectura corrupta dos datos YAML de {ressource} (razón: {error})",
"corrupted_json": "Lectura corrupta dos datos JSON de {ressource} (razón: {error})",
"unknown_error_reading_file": "Erro descoñecido ao intentar ler o ficheiro {file} (razón: {error})",
"cannot_write_file": "Non se puido escribir o ficheiro {file} (razón: {error})",
"cannot_open_file": "Non se puido abrir o ficheiro {file} (razón: {error})",
"websocket_request_expected": "Agardábase unha solicitude WebSocket",
"edit_text_question": "{}. Editar este texto ? [yN]: "
}

1
locales/he.json Normal file
View file

@ -0,0 +1 @@
{}

Some files were not shown because too many files have changed in this diff Show more