diff --git a/.vscode/extensions.json b/.vscode/extensions.json
new file mode 100644
index 0000000..b34c1c6
--- /dev/null
+++ b/.vscode/extensions.json
@@ -0,0 +1,8 @@
+{
+ "recommendations": [
+ "mads-hartmann.bash-ide-vscode",
+ "mkhl.shfmt",
+ "samuelcolvin.jinjahtml",
+ "jgclark.vscode-todo-highlight"
+ ],
+}
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..68b9017
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,35 @@
+{
+ "cSpell.words": [
+ "chattr",
+ "dnsmasq",
+ "dpkg",
+ "echoerr",
+ "mkdir",
+ "resolv",
+ "rfkill",
+ "tera"
+ ],
+ "cSpell.enableFiletypes": [
+ "!plaintext"
+ ],
+ "ltex.language": "en",
+ "outline.showVariables": false,
+ "editor.formatOnSave": true,
+ "todohighlight.enableDiagnostics": true,
+ "todohighlight.include": [
+ "**/*.js",
+ "**/*.jsx",
+ "**/*.ts",
+ "**/*.tsx",
+ "**/*.html",
+ "**/*.css",
+ "**/*.scss",
+ "**/*.php",
+ "**/*.rb",
+ "**/*.txt",
+ "**/*.mdown",
+ "**/*.md",
+ "**/*.sh",
+ "**/scripts/*"
+ ]
+}
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..9ed5473
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,661 @@
+GNU AFFERO GENERAL PUBLIC LICENSE
+ Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ 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.
+
+ zourit-admin
+ Copyright (C) 2021 zourit
+
+ 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 .
+
+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
+.
diff --git a/README.md b/README.md
index 2c9fedc..0b18507 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
-MIAOU SERVER
-============
+MIAOU
+=====
provisioning tool for building opinionated architecture following these principles:
* free software: AGPLv3
@@ -14,12 +14,10 @@ TODO
* [ ] less interactive command
* [ ] on lxd init during then install process
* [ ] backup postgresql missing out on **saturday**
-* [ ] TOOLBOOX/nc (as binary)
-* [ ] nginx root domain redirects
- * [ ] update dnsmasq as well
+* [ ] TOOLBOOX/nc (binary)
* [ ] final ansible-like indicators: same/new
* [ ] patched editor (backup+editor+diff+patch)
-* [ ] improve log journal for each `recipe` (apache, for example) in order to shorten disk space
+* [ ] to improve log journal for each `recipe` (apache, for example) in order to shorten disk space
ORIGIN
------
diff --git a/lib/functions.sh b/lib/functions.sh
new file mode 100644
index 0000000..d960c93
--- /dev/null
+++ b/lib/functions.sh
@@ -0,0 +1,652 @@
+#!/bin/bash
+
+RED='\e[0;41m\e[1;37m'
+GREEN='\033[0;32m'
+YELLOW='\033[0;33m'
+PURPLE='\033[0;35m'
+DARK='\e[100m'
+NC='\033[0m' # No Color
+TO_BE_DEFINED="TO BE DEFINED"
+
+# BOLD='\033[1m'
+# DIM='\e[2m\e[0;90m'
+
+function echo() {
+ [[ -n ${PREFIX:-} ]] && printf "${DARK}%25.25s${NC} " "${PREFIX}"
+ builtin echo "$@"
+}
+
+function check_normal_user() {
+ [[ $(id -u) -lt 1000 ]] && echoerr "normal user (>1000) expected, please connect as a normal user then call again!" && exit 100
+ return 0
+}
+
+function sudo_required() {
+ check_normal_user
+ command -v sudo &>/dev/null &&
+ id -G | grep -q sudo && echoerr "command not found, please install as so: \`apt install -y sudo\`" && exit 1
+ if ! sudo -n true &>/dev/null; then
+ if [[ -n "${1:-}" ]]; then
+ echowarnn "[sudo] requiring authorized access for: [ $1 ]"
+ else
+ echowarnn "[sudo] requiring authorized access for further processing"
+ fi
+ fi
+ sudo -vp ' : '
+}
+
+# idempotent cargo install
+function idem_cargo_install() {
+ for i in "$@"; do
+ if [ ! -f ~/.cargo/bin/"$i" ]; then
+ cargo install "$i"
+ fi
+ done
+}
+
+# display error in red
+function echoerr() {
+ echo -e "${RED}$*${NC}" >&2
+}
+
+function echoerrn() {
+ echo -en "${RED}$*${NC}" >&2
+}
+
+# display warn in yellow
+function echowarn() {
+ echo -e "${YELLOW}$*${NC}" >&2
+}
+
+function echowarnn() {
+ echo -en "${YELLOW}$*${NC}" >&2
+}
+
+# display error in green
+function echoinfo() {
+ echo -e "${GREEN}$*${NC}" >&2
+}
+
+function echoinfon() {
+ echo -en "${GREEN}$*${NC}" >&2
+}
+
+# test whether is a valid ipv4 address?
+function valid_ipv4() {
+ local ip="$1"
+ if [[ $ip =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
+ IFS='.' read -ra ADDR <<<"$ip"
+ [[ ${ADDR[0]} -le 255 && ${ADDR[1]} -le 255 && ${ADDR[2]} -le 255 && ${ADDR[3]} -le 255 ]]
+ return $?
+ fi
+ return 1
+}
+
+function enable_trace() {
+ trap 'trap_error $? ${LINENO:-0} ${BASH_LINENO:-0} ${BASH_COMMAND:-empty} $(printf "::%s" ${FUNCNAME[@]})' ERR
+}
+
+function disable_trace() {
+ trap - ERR
+}
+
+function prepare_nftables() {
+ local PREFIX="miaou:nftables"
+
+ if [[ ! -f /etc/nftables.rules.d/firewall.table ]]; then
+ echo "installing nftables ..."
+ sudo apt install -y nftables
+ sudo cp -f "$MIAOU_BASEDIR/templates/hardened/nftables.conf" /etc/
+ sudo mkdir -p /etc/nftables.rules.d
+ sudo cp -f "$MIAOU_BASEDIR/templates/hardened/firewall.table" /etc/nftables.rules.d/
+ sudo systemctl restart nftables
+ sudo systemctl enable nftables
+ echo "OK"
+ else
+ echo "nftables already installed!"
+ fi
+
+}
+
+function miaou_init() {
+ # shellcheck source=/dev/null
+ [[ -f /opt/debian-bash/lib/functions.sh ]] && source /opt/debian-bash/lib/functions.sh
+
+ # shellcheck source=/dev/null
+ . "$MIAOU_BASEDIR/lib/functions.sh"
+
+ export MIAOU_CONFIGDIR="$HOME/.config/miaou"
+
+ set -Eeuo pipefail
+ enable_trace
+ trap 'ctrl_c $? ${LINENO:-0} ${BASH_LINENO:-0} ${BASH_COMMAND:-empty} $(printf "::%s" ${FUNCNAME[@]})' INT
+}
+
+function ctrl_c() {
+ PREFIX="miaou:trap" echoerr "Ctrl + C happened, exiting!!! $*"
+ exit 125
+}
+
+# extract source code error triggered on trap error
+function trap_error() {
+ ERRORS_COUNT=0
+ if [[ -f "$MIAOU_CONFIGDIR"/error_count ]]; then
+ ERRORS_COUNT=$(cat "$MIAOU_CONFIGDIR"/error_count)
+ else
+ mkdir -p "$MIAOU_CONFIGDIR"
+ printf 0 >"$MIAOU_CONFIGDIR"/error_count
+ fi
+ ERRORS_COUNT=$((ERRORS_COUNT + 1))
+ printf '%s' $ERRORS_COUNT >"$MIAOU_CONFIGDIR"/error_count
+
+ local PREFIX=""
+
+ # local file="${0:-}"
+ local err=$1 # error status
+ local line=$2 # LINENO
+ local linecallfunc=${3:-}
+ local command="${4:-}"
+ local funcstack="${5:-}"
+ local caller
+
+ caller=$(caller | cut -d' ' -f2)
+
+ # echo >&2
+ # if [ "$funcstack" != "::" ]; then
+ # echo -e "${RED}ERROR <$err>, due to command <$command> at line $line from <$caller>, stack=${funcstack}${NC}" >&2
+ # else
+ # echo >&2 "ERROR DETECTED"
+ # fi
+
+ # echo
+ # echo -e "${PURPLE}$caller:$line ${NC}EXIT ${RED}<$err>${NC}" >&2
+ # echo -e "${PURPLE}------------------------------------------ ${NC}" >&2
+ if [[ $ERRORS_COUNT == 1 ]]; then
+ echo
+ echo -e "${RED}ERROR <$err>, due to command <$command $funcstack>${NC}" >&2
+ fi
+ echo -e "${PURPLE}$ERRORS_COUNT: $caller:$line ${RED}$command $funcstack${NC}" >&2
+ # echo -e "${PURPLE}----------------------------- ${PURPLE}EXIT CODE ${PURPLE}--------------${PURPLE} $err ${NC}" >&2
+
+ # if [[ $line -gt 2 ]]; then
+ # sed "$((line - 2))q;d" "$caller" >&2
+ # sed "$((line - 1))q;d" "$caller" >&2
+ # fi
+ # echo -ne "${BOLD}" >&2
+ # sed "${line}q;d" "$caller" >&2
+ # echo -e "${PURPLE}------------------------------------------ ${NC}" >&2
+}
+
+# exist_command(cmd1, ...)
+# test all commands exist, else fail
+function exist_command() {
+ for i in "$@"; do
+ command -v "$i" &>/dev/null || return 50
+ done
+}
+
+# test whether container is up and running?
+function container_running() {
+ arg1_required "$@"
+ container_exists "$1" && lxc list "$1" -c ns -f csv | head -n1 | grep -q "$1,RUNNING"
+ lxc exec "$1" -- bash </dev/null
+ fi
+EOF
+}
+
+# test arg1 required
+function arg1_required() {
+ [[ -z "${1:-}" ]] && echoerr "ERROR: arg#1 expected!" && return 125
+ return 0
+}
+
+# test arg2 required
+function arg2_required() {
+ [[ -z "${2:-}" ]] && echoerr "ERROR: arg#2 expected!" && return 125
+ return 0
+}
+
+# test whether container exists yet?
+function container_exists() {
+ arg1_required "$@"
+ lxc list "$1" -c n -f csv | grep -q "^$1\$"
+}
+
+# build debian image with prebuild debian-bash and various useful settings
+# ARG1=release [bullseye, buster]
+function build_miaou_image() {
+ local RELEASE="$1"
+ local IMAGE_LABEL="$RELEASE-miaou"
+ local PREFIX="miaou:image"
+
+ local DEB_REPOSITORY
+ DEB_REPOSITORY=$(grep ^deb /etc/apt/sources.list | head -n1 | cut -d ' ' -f2 | cut -d '/' -f3)
+
+ if ! lxc image -cl list -f csv | grep -q "$IMAGE_LABEL"; then
+
+ echo "building lxc image <$IMAGE_LABEL> ... "
+ echo "image will reuse same local repository <$DEB_REPOSITORY>"
+ creation_date=$(date +%s)
+ sudo /opt/debian-bash/tools/idem_apt_install debootstrap
+
+ cat </etc/apt/sources.list
+deb http://$DEB_REPOSITORY/debian $RELEASE main contrib
+deb http://$DEB_REPOSITORY/debian $RELEASE-updates main contrib
+deb http://$DEB_REPOSITORY/debian-security/ $RELEASE/updates main contrib
+EOF3
+ else
+ cat </etc/apt/sources.list
+deb http://$DEB_REPOSITORY/debian $RELEASE main contrib
+deb http://$DEB_REPOSITORY/debian $RELEASE-updates main contrib
+deb http://$DEB_REPOSITORY/debian-security/ $RELEASE-security main contrib
+EOF3
+ fi
+
+ echo APT UPDATE
+
+ apt update && apt dist-upgrade -y
+ apt install -y curl wget file git sudo bash-completion
+ curl https://git.artcode.re/pvincent/debian-bash/raw/branch/master/install.sh | sudo bash -s -- --host
+ ln -sf /usr/share/zoneinfo/Indian/Reunion /etc/localtime
+ cat </etc/network/interfaces
+# This file describes the network interfaces available on your system
+# and how to activate them. For more information, see interfaces(5).
+
+# The loopback network interface
+auto lo
+iface lo inet loopback
+
+auto eth0
+iface eth0 inet dhcp
+
+source /etc/network/interfaces.d/*
+EOF3
+ echo "deboostrap ready!"
+EOF2
+ cd /tmp/$IMAGE_LABEL-image
+ tar -czf rootfs.tar.gz -C /tmp/$IMAGE_LABEL .
+ cat <metadata.yaml
+architecture: "x86_64"
+creation_date: $creation_date
+properties:
+architecture: "x86_64"
+description: "Debian $RELEASE for miaou instances"
+os: "debian"
+release: "$RELEASE"
+EOF2
+ tar -czf metadata.tar.gz metadata.yaml
+EOF1
+ lxc image import "/tmp/$IMAGE_LABEL-image/metadata.tar.gz" "/tmp/$IMAGE_LABEL-image/rootfs.tar.gz" --alias "$IMAGE_LABEL"
+ echo "image <$IMAGE_LABEL> successfully built!"
+ echo DONE
+ else
+ echo "image <$IMAGE_LABEL> already built!"
+ fi
+}
+
+# execute remote scripting onto one LXC container [COMMANDS, ...]
+# may use one command like: `lxc_exec ct1 uname -a`
+# or pipe like so: `
+# cat < about to be created ..."
+ local extra_release="${2:-}"
+ if [[ -n "$extra_release" ]] && ! lxc image info "${extra_release}-miaou" >/dev/null; then
+ echoerrn "unknown extra_release <${extra_release}-miaou>!\nHINT : please add it into /etc/miaou/defaults.yaml, then re-install miaou!"
+ exit 128
+ fi
+
+ if [[ -n "$extra_release" ]]; then
+ echoerrn "FIXME: lxc-miaou-create -o release=bookworm should be implemented ...."
+ lxc-miaou-create "$ct" "$extra_release"
+ else
+ lxc-miaou-create "$ct"
+ fi
+ echo "DONE"
+ fi
+
+ if ! container_running "$ct"; then
+ echowarn "container <$ct> seems to be asleep, starting ..."
+ lxc start "$ct"
+ echowarn DONE
+ fi
+}
+
+function load_yaml_from_expanded {
+ arg1_required "$@"
+ yaml_key="$1"
+ yaml_file="$MIAOU_CONFIGDIR/miaou.expanded.yaml"
+ yaml_value=$(yq ".$yaml_key" "$yaml_file")
+ if [[ -n "$yaml_value" ]] && [[ "$yaml_value" != "null" ]] && [[ "$yaml_value" != "$TO_BE_DEFINED" ]]; then
+ PREFIX="" echo "$yaml_value"
+ else
+ echoerr "undefined value for key: <$yaml_key> from file: <$yaml_file>"
+ return 98
+ fi
+}
+
+function check_yaml_defined_value {
+ yaml_file="$1"
+ yaml_key="$2"
+ yaml_value=$(yq ".$yaml_key" "$yaml_file")
+ if [[ -n "$yaml_value" ]] && [[ "$yaml_value" != "null" ]] && [[ "$yaml_value" != "$TO_BE_DEFINED" ]]; then
+ return 0
+ else
+ echoerr "undefined value for key: <$yaml_key> from file: <$yaml_file>"
+ return 99
+ fi
+}
+
+# halt unless current user is root
+function root_required() {
+ [[ $(id -u) == 0 ]] || (echoerr "root required" && return 1)
+}
+
+# arg#1: environment variable
+# read from environment or ask entry before exporting new variable
+function env_or_ask {
+ if [[ -n ${1+x} ]]; then
+ if printenv "$1" >/dev/null; then
+ echo "value defined as $(printenv "$1")"
+ else
+ printf "Please define %20s: " "$1"
+ read -r
+ export "$1=\"$REPLY\"" >/dev/null
+ fi
+ else
+ echoerr "env_or_ask requires one argument: " && exit 5
+ fi
+}
+
+# install_debian_bash()
+# grab and install related project
+function install_debian_bash() {
+ local PREFIX="debian-bash:install"
+ if [[ ! -d /opt/debian-bash ]]; then
+ echo "installing curl wget commands ..."
+ apt install -y curl wget
+
+ echo "installing debian-bash..."
+ curl https://git.artcode.re/pvincent/debian-bash/raw/branch/master/install.sh | sudo bash -s -- --host
+ export PATH=$PATH:/opt/debian-bash/tools/
+ echo "OK"
+ else
+ # /opt/debian-bash/tools/debian_bash_upgrade
+ echo "addon already installed!"
+ fi
+ # shellcheck source=/dev/null
+ source /etc/bash.bashrc
+
+ sudo /opt/debian-bash/tools/idem_apt_install bash-completion
+}
+
+function add_toolbox_sudoers {
+ local PREFIX="toolbox:sudoers"
+ echo -n "creating sudoers file to allow sudo as command from /TOOLBOX... "
+ sudo mkdir -p /etc/sudoers.d
+ if [[ ! -f /etc/sudoers.d/add_TOOLBOX_to_PATH ]]; then
+ sudo tee /etc/sudoers.d/add_TOOLBOX_to_PATH &>/dev/null </dev/null; then
+ echo -n "installing ... "
+ curl -sSf https://sh.rustup.rs | sh -s -- -y
+ # shellcheck source=/dev/null
+ source "$HOME/.cargo/env"
+ /opt/debian-bash/tools/append_or_replace "^PATH=\$PATH:\$HOME/\\.cargo/bin" "PATH=\$PATH:\$HOME/.cargo/bin" ~/.bashrc
+ PREFIX="" echo "OK"
+ else
+ echo "command already installed!"
+ fi
+
+ echo -n "installing ... "
+ if [ ! -f "/TOOLBOX/fd" ]; then
+ idem_cargo_install fd-find
+ sudo cp "$HOME"/.cargo/bin/fd /TOOLBOX/fd
+ PREFIX="" echo "successfully installed!"
+ else
+ PREFIX="" echo "already done!"
+ fi
+
+ echo -n "installing ... "
+ if [ ! -f "/TOOLBOX/viu" ]; then
+ idem_cargo_install viu
+ sudo cp "$HOME"/.cargo/bin/viu /TOOLBOX/
+ PREFIX="" echo "successfully installed!"
+ else
+ PREFIX="" echo "already done!"
+ fi
+
+ echo -n "installing alias ... "
+ if [ ! -f "/TOOLBOX/rg" ]; then
+
+ sudo /opt/debian-bash/tools/idem_apt_install ripgrep
+ sudo ln /usr/bin/rg /TOOLBOX/
+ PREFIX="" echo "successfully installed"
+ else
+ PREFIX="" echo "already done!"
+ fi
+
+ echo -n "installing alias ... "
+ if [ ! -f "/TOOLBOX/ag" ]; then
+ sudo /opt/debian-bash/tools/idem_apt_install silversearcher-ag
+ sudo ln /usr/bin/ag /TOOLBOX/
+ PREFIX="" echo "successfully installed"
+ else
+ PREFIX="" echo "already done!"
+ fi
+
+ echo -n "installing ... "
+ if [ ! -f "/TOOLBOX/bandwhich" ]; then
+ idem_cargo_install bandwhich
+ sudo cp "$HOME"/.cargo/bin/bandwhich /TOOLBOX/bandwhich
+ PREFIX="" echo "successfully installed"
+ else
+ PREFIX="" echo "already done!"
+ fi
+
+ echo -n "installing alias ... "
+ if [ ! -f "/TOOLBOX/btm" ]; then
+ VERSION=$(wget_semver github ClementTsang/bottom)
+ cd /tmp
+ wget "https://github.com/ClementTsang/bottom/releases/download/$VERSION/bottom_x86_64-unknown-linux-musl.tar.gz"
+ tar -xzvf bottom_x86_64-unknown-linux-musl.tar.gz
+ sudo cp btm /usr/local/bin/
+ sudo ln /usr/local/bin/btm /TOOLBOX/
+ PREFIX="" echo "successfully installed"
+ else
+ PREFIX="" echo "already done!"
+ fi
+
+ echo -n "installing ... "
+ if [ ! -f "/TOOLBOX/micro" ]; then
+ cd /tmp || (echoerr "/tmp wrong permission" && exit 101)
+ curl -q https://getmic.ro | GETMICRO_REGISTER=n sh
+ sudo mv micro /TOOLBOX/micro
+ sudo chown root:root /TOOLBOX/micro
+ PREFIX="" echo "successfully installed"
+ else
+ PREFIX="" echo "already done!"
+ fi
+
+ echo -n "installing ... "
+ if [ ! -f "/TOOLBOX/ncdu" ]; then
+ sudo /opt/debian-bash/tools/idem_apt_install ncdu
+ sudo cp /usr/bin/ncdu /TOOLBOX/ncdu
+ PREFIX="" echo "successfully installed"
+ else
+ PREFIX="" echo "already done!"
+ fi
+
+ echo -n "installing ... "
+ if [ ! -f "/TOOLBOX/unzip" ]; then
+ sudo /opt/debian-bash/tools/idem_apt_install unzip
+ sudo cp /usr/bin/unzip /TOOLBOX/unzip
+ PREFIX="" echo "successfully installed"
+ else
+ PREFIX="" echo "already done!"
+ fi
+
+ echo -n "installing ... "
+ if [ ! -f "/TOOLBOX/tree" ]; then
+ sudo /opt/debian-bash/tools/idem_apt_install tree
+ sudo cp /bin/tree /TOOLBOX/tree
+ PREFIX="" echo "successfully installed"
+ else
+ PREFIX="" echo "already done!"
+ fi
+
+ echo -n "installing ... "
+ if [ ! -f "/TOOLBOX/duf" ]; then
+ VERSION=$(/opt/debian-bash/tools/wget_semver github muesli/duf)
+ VERSION_WITHOUT_V=${VERSION#v}
+ wget -O /tmp/duf.deb "https://github.com/muesli/duf/releases/download/${VERSION}/duf_${VERSION_WITHOUT_V}_linux_amd64.deb"
+ sudo dpkg -i /tmp/duf.deb
+ sudo cp /bin/duf /TOOLBOX/duf
+ PREFIX="" echo "successfully installed"
+ else
+ PREFIX="" echo "already done!"
+ fi
+
+ echo -n "installing ... "
+ if [ ! -f "/TOOLBOX/curl" ]; then
+ sudo wget -O /TOOLBOX/curl "https://github.com/moparisthebest/static-curl/releases/latest/download/curl-amd64"
+ sudo chmod +x /TOOLBOX/curl
+ PREFIX="" echo "successfully installed"
+ else
+ PREFIX="" echo "already done!"
+ fi
+
+ echo -n "installing ... "
+ if [ ! -f "/TOOLBOX/wget" ]; then
+ sudo ln -f /usr/bin/wget /TOOLBOX/wget
+ PREFIX="" echo "successfully installed"
+ else
+ PREFIX="" echo "already done!"
+ fi
+
+}
+
+# install_mandatory_commands
+function install_mandatory_commands() {
+ local PREFIX="mandatory:commands"
+
+ sudo /opt/debian-bash/tools/idem_apt_install dnsutils build-essential curl mariadb-client postgresql-client
+
+ if ! exist_command tera; then
+ echo "installing ..."
+
+ local version=v0.2.4
+ wget -q "https://github.com/chevdor/tera-cli/releases/download/${version}/tera-cli_linux_amd64.deb" -O /tmp/tera-cli_linux_amd64.deb
+ sudo dpkg -i /tmp/tera-cli_linux_amd64.deb
+ else
+ echo "command already installed!"
+ fi
+
+ if ! exist_command yq; then
+ local version binary
+ version='v4.35.2'
+ binary='yq_linux_amd64'
+
+ sudo sh -c "wget https://github.com/mikefarah/yq/releases/download/${version}/${binary}.tar.gz -O - |\
+ tar -xz ./${binary} && sudo mv ${binary} /usr/bin/yq"
+ else
+ echo "command already installed!"
+ fi
+
+}
+
+# flatten array, aka remove duplicated elements in array
+# return: `mapfile -t OUTPUT_ARRAY < <(sort_array "${INPUT_ARRAY[@]}")`
+function flatten_array {
+ declare -a array=("$@")
+ IFS=" " read -r -a array <<<"$(tr ' ' '\n' <<<"${array[@]}" | sort -u | tr '\n' ' ')"
+ printf '%s\n' "${array[@]}"
+}
+
+function prepare_nftables() {
+ local PREFIX="miaou:firewall"
+
+ if [[ ! -f /etc/nftables.rules.d/firewall.table ]]; then
+ echo "installing nftables ..."
+ sudo apt install -y nftables
+ sudo cp -f "$MIAOU_BASEDIR/templates/hardened/nftables.conf" /etc/
+ sudo mkdir -p /etc/nftables.rules.d
+ sudo cp -f "$MIAOU_BASEDIR/templates/hardened/firewall.table" /etc/nftables.rules.d/
+ sudo systemctl restart nftables
+ sudo systemctl enable nftables
+ echo "OK"
+ else
+ echo "nftables already installed!"
+ fi
+}
diff --git a/lib/harden.sh b/lib/harden.sh
new file mode 100755
index 0000000..a14a3b4
--- /dev/null
+++ b/lib/harden.sh
@@ -0,0 +1,294 @@
+#!/bin/bash
+
+### FUNCTIONS
+### ---------
+
+function prepare_config_hardened() {
+ mkdir -p "$HARDEN_CONFIGDIR"
+}
+
+function pubkey_authorize() {
+ local PREFIX="harden:pubkey:authorize"
+
+ if [[ ! -d $HOME/.ssh ]]; then
+ echo -n "create .ssh folder for the first time ..."
+ mkdir -m 700 ~/.ssh
+ PREFIX="" echo "OK"
+ else
+ local security_issue_in_ssh_folder
+ security_issue_in_ssh_folder=$(find "$HOME/.ssh" -perm -go=r | wc -l)
+ if [[ $security_issue_in_ssh_folder -gt 0 ]]; then
+ echo -n "force security in .ssh folder for <$CURRENT_USER> ..."
+ chmod -R u+rwX,go-rwx "/home/$CURRENT_USER/.ssh"
+ PREFIX="" echo "OK"
+ else
+ echo "security in .ssh folder for <$CURRENT_USER> approved!"
+ fi
+ fi
+
+ pubkey_value=$(yq ".authorized.pubkey" "$HARDEN_CONFIGFILE")
+ if [[ ! -f /home/$CURRENT_USER/.ssh/authorized_keys ]]; then
+ echo -n "authorized_keys first time ..."
+ PREFIX="" echo "$pubkey_value" >"$HOME/.ssh/authorized_keys"
+ chmod u+rw,go-rwx "/home/$CURRENT_USER/.ssh/authorized_keys"
+ PREFIX="" echo "OK"
+ else
+ if ! grep -q "^$pubkey_value" "/home/$CURRENT_USER/.ssh/authorized_keys"; then
+ echo -n "pubkey <$CURRENT_USER> appended to <.ssh/authorized_keys> ..."
+ echo "$pubkey_value" >>"$HOME/.ssh/authorized_keys"
+ PREFIX="" echo "OK"
+ else
+ echo "pubkey <$CURRENT_USER> already authorized!"
+ fi
+ fi
+}
+
+function sudoers() {
+ local PREFIX="harden:sudoers"
+ if [[ -d /etc/sudoers.d ]]; then
+ echo -n "add $CURRENT_USER and no more ..."
+
+ sudo env current_user="$CURRENT_USER" tera -e --env-key env --env-only -o /etc/sudoers -t "$MIAOU_BASEDIR/templates/hardened/sudoers.j2" >/dev/null
+
+ rm /etc/sudoers.d -rf
+ grep -Eq "^debian" /etc/passwd && userdel -rf debian
+ grep -Eq "^sudo" /etc/group && groupdel sudo
+ passwd -dq root
+ passwd -dq "$CURRENT_USER"
+
+ PREFIX="" echo "OK"
+ else
+ echo "sudo authorized for <$CURRENT_USER> only!"
+ fi
+}
+
+function sshd() {
+ local PREFIX="harden:sshd"
+ if [[ ! -f /etc/ssh/sshd_config ]]; then
+ sudo apt install -y openssh-server
+ else
+ echo "sshd already installed!"
+ fi
+
+ if ! grep -Eq "^Port 2222" /etc/ssh/sshd_config; then
+ echo -n "replacing sshd ..."
+
+ sudo env current_user="$CURRENT_USER" tera -e --env-key env --env-only -o /etc/ssh/sshd_config -t "$MIAOU_BASEDIR/templates/hardened/sshd_config.j2" >/dev/null
+ sudo systemctl restart sshd
+
+ PREFIX="" echo "OK"
+ else
+ echo "already done!"
+ fi
+}
+
+function prepare_proxy() {
+ local PREFIX="harden:proxy"
+
+ if ! grep -Eq "^precedence ::ffff:0:0/96.*" /etc/gai.conf; then
+ echo "prefer ipv4 ..."
+ sudo /opt/debian-bash/tools/append_or_replace "^precedence ::ffff:0:0/96.*" "precedence ::ffff:0:0/96 100" /etc/gai.conf
+ echo "OK"
+ else
+ echo "ipv4 already prefered!"
+ fi
+
+ if ! grep -Eq "^net.ipv4.ip_forward=1" /etc/sysctl.conf; then
+ echo "allow forwarding from kernel ..."
+ sudo /opt/debian-bash/tools/append_or_replace "^net.ipv4.ip_forward=1.*" "net.ipv4.ip_forward=1" /etc/sysctl.conf
+ sudo sysctl -p
+ echo "OK"
+ else
+ echo "kernel forwarding already allowed!"
+ fi
+}
+
+function set_current_user {
+ local PREFIX="harden:environment"
+
+ CURRENT_USER=$(id -un)
+ echo "current user is <$CURRENT_USER>"
+}
+
+function load_configuration {
+ local PREFIX="harden:configuration:load"
+
+ if [[ ! -f "$HARDEN_CONFIGFILE" ]]; then
+ echo "configuration requires further details ..."
+ cp "$MIAOU_BASEDIR/templates/hardened/hardened.yaml.sample" "$HARDEN_CONFIGFILE"
+ echo "OK"
+ fi
+
+ editor "$HARDEN_CONFIGFILE"
+}
+
+function check_configuration {
+ local PREFIX="harden:configuration:check"
+
+ check_yaml_defined_value "$HARDEN_CONFIGFILE" 'authorized.pubkey'
+ check_yaml_defined_value "$HARDEN_CONFIGFILE" 'alert.to'
+ check_yaml_defined_value "$HARDEN_CONFIGFILE" 'alert.from'
+ check_yaml_defined_value "$HARDEN_CONFIGFILE" 'alert.smtp.server'
+}
+
+function set_timezone_if_defined {
+ local PREFIX="harden:timezone"
+ timezone=$(yq ".timezone" "$HARDEN_CONFIGFILE")
+ if [[ "$timezone" != null ]]; then
+ if ! grep -q "$timezone" /etc/timezone; then
+ if [[ -f "/usr/share/zoneinfo/$timezone" ]]; then
+ echo "set timezone to $timezone ..."
+ ln -fs "/usr/share/zoneinfo/$timezone" /etc/localtime
+ dpkg-reconfigure -f noninteractive tzdata
+ echo OK
+ else
+ echoerr "unkown timezone: <$timezone>, please edit <$HARDEN_CONFIGFILE> and change to a correct value" && exit 98
+ fi
+ else
+ echo "timezone <$timezone> already set!"
+ fi
+ fi
+}
+
+function mailer_alert() {
+ local PREFIX="harden:mailer"
+
+ if [[ ! -f /etc/msmtprc ]]; then
+ for i in exim4-config libevent-2.1-7 libgnutls-dane0 libunbound8; do
+ if dpkg -l "$i" 2>/dev/null | grep -q ^ii && echo 'installed'; then
+ echo "purging package <$i> ..."
+ apt purge -y "$i"
+ echo "OK"
+ fi
+ done
+
+ echo "installing ..."
+ sudo /opt/debian-bash/tools/idem_apt_install msmtp msmtp-mta mailutils bsd-mailx
+ echo "OK"
+
+ echo "configuring "
+ sudo env current_user="$CURRENT_USER" tera -e --env-key env -o /etc/aliases -t "$MIAOU_BASEDIR/templates/hardened/mailer/aliases.j2" "$HARDEN_CONFIGDIR/hardened.yaml" >/dev/null
+ echo "OK"
+
+ # populate environment variable with fqdn
+ fqdn=$(hostname -f)
+
+ echo "configuring "
+ sudo env current_user="$CURRENT_USER" fqdn="$fqdn" tera -e --env-key env -o /etc/mail.rc -t "$MIAOU_BASEDIR/templates/hardened/mailer/mail.rc.j2" "$HARDEN_CONFIGDIR/hardened.yaml" >/dev/null
+ echo "OK"
+
+ echo "generating configuration file ..."
+ sudo env fqdn="$fqdn" tera -e --env-key env -o /etc/msmtprc -t "$MIAOU_BASEDIR/templates/hardened/mailer/msmtprc.j2" "$HARDEN_CONFIGDIR/hardened.yaml" >/dev/null
+ sudo chown root:msmtp /etc/msmtprc
+ sudo chmod 640 /etc/msmtprc
+ echo "OK"
+ else
+ echo "mailer already configured!"
+ fi
+
+}
+
+function alert_at_boot() {
+ local PREFIX="harden:alert:boot"
+ if ! systemctl is-enabled --quiet on_startup.service 2>/dev/null; then
+ echo "installing on systemd..."
+ sudo cp "$MIAOU_BASEDIR/templates/hardened/systemd/on_startup.service" /etc/systemd/system/on_startup.service
+ sudo systemctl daemon-reload
+ sudo systemctl enable on_startup.service
+ REBOOT=true
+ echo "OK"
+ else
+ echo "systemd already enabled!"
+ fi
+}
+
+function show_reboot_on_purpose() {
+ if "$REBOOT"; then
+ PREFIX="harden:reboot" echowarn "we recommend reboot on purpose, Reboot NOW?"
+ else
+ PREFIX="harden" echo "success"
+ fi
+}
+
+function disable_systemd_resolved() {
+ PREFIX="harden:systemd:resolved"
+ if file /etc/resolv.conf | grep -q /run/systemd/resolve/stub-resolv.conf; then
+ echo "disabling systemd-resolved..."
+ sudo systemctl stop systemd-resolved.service
+ sudo systemctl disable systemd-resolved.service
+ sudo rm /etc/resolv.conf
+ cat </dev/null || load_configuration
+check_configuration
+pubkey_authorize
+sshd
+prepare_proxy
+prepare_nftables
+disable_systemd_resolved
+set_timezone_if_defined
+mailer_alert
+alert_at_boot
+alert_at_ssh_password
+customize_motd
+
+show_reboot_on_purpose
diff --git a/lib/images/bullseye-miaou.sh b/lib/images/bullseye-miaou.sh
new file mode 100755
index 0000000..cfffb38
--- /dev/null
+++ b/lib/images/bullseye-miaou.sh
@@ -0,0 +1,18 @@
+#!/bin/bash
+
+MIAOU_DIR="$(dirname "$0")/../.."
+readonly MIAOU_DIR
+
+function init_strict() {
+ set -Eeuo pipefail
+ # shellcheck source=/dev/null
+ source "$MIAOU_DIR/lib/functions.sh"
+ # shellcheck source=/dev/null
+ source "/opt/debian-bash/lib/functions.sh"
+ trap 'trap_error $? $LINENO $BASH_LINENO "$BASH_COMMAND" $(printf "::%s" ${FUNCNAME[@]})' ERR
+}
+
+## main
+init_strict
+sudo_required
+build_miaou_image "bullseye"
diff --git a/lib/images/buster-miaou.sh b/lib/images/buster-miaou.sh
new file mode 100755
index 0000000..6731c6a
--- /dev/null
+++ b/lib/images/buster-miaou.sh
@@ -0,0 +1,19 @@
+#!/bin/bash
+
+MIAOU_DIR="$(dirname "$0")/../.."
+readonly MIAOU_DIR
+
+function init_strict() {
+ set -Eeuo pipefail
+ # shellcheck source=/dev/null
+ source "$MIAOU_DIR/lib/functions.sh"
+ # shellcheck source=/dev/null
+ source "/opt/debian-bash/lib/functions.sh"
+
+ trap 'trap_error $? $LINENO $BASH_LINENO "$BASH_COMMAND" $(printf "::%s" ${FUNCNAME[@]})' ERR
+}
+
+## main
+init_strict
+sudo_required
+build_miaou_image "buster"
diff --git a/lib/init.sh b/lib/init.sh
new file mode 100644
index 0000000..2ccf3dc
--- /dev/null
+++ b/lib/init.sh
@@ -0,0 +1,5 @@
+#!/bin/bash
+
+# shellcheck source=/dev/null
+. "$MIAOU_BASEDIR/lib/functions.sh"
+miaou_init
diff --git a/lib/install.sh b/lib/install.sh
new file mode 100755
index 0000000..04c1687
--- /dev/null
+++ b/lib/install.sh
@@ -0,0 +1,409 @@
+#!/bin/bash
+
+MIAOU_BASEDIR=$(readlink -f "$(dirname "$0")/..")
+# shellcheck source=/dev/null
+. "$MIAOU_BASEDIR/lib/functions.sh"
+readonly MIAOU_BASEDIR
+
+miaou_init
+
+EXPANDED_CONF="$MIAOU_CONFIGDIR/miaou.expanded.yaml"
+NEW_GROUP=lxd
+readonly NEW_GROUP EXPANDED_CONF
+
+on_exit() {
+ if [[ "$SESSION_RELOAD_REQUIRED" == true ]]; then
+ echo "======================================================"
+ echo "Session Reload is required (due to new group <$NEW_GROUP>)"
+ echo "======================================================"
+ fi
+ if [ -n "${1:-}" ]; then
+ echo "Aborted by $1"
+ elif [ "${status:-}" -ne 0 ]; then
+ echo "Failure (status $status)"
+ fi
+}
+
+function prepare_lxd {
+ local PREFIX="lxd:prepare"
+
+ # test group lxd assign to current user
+ if ! groups | grep -q lxd; then
+ echo "define lxd and assign to user <$USER>"
+ sudo groupadd --force "$NEW_GROUP"
+ sudo usermod --append --groups "$NEW_GROUP" "$(whoami)"
+ exec sg "$NEW_GROUP" "exec '$0' $(printf "'%s' " SESSION_RELOAD_REQUIRED "$@")"
+ # no further processing because exec has been called!
+ else
+ echo "user <$USER> already belongs to group !"
+ fi
+
+ sudo /opt/debian-bash/tools/idem_apt_install lxd btrfs-progs
+
+ # test lxdbr0
+ if ! lxc network info lxdbr0 &>/dev/null; then
+ echo "bridge down, so initialization will use default preseed..."
+ sudo lxd init
+ # cat < found implies it has been already initialized!"
+ fi
+
+ set_alias 'sameuser' "exec @ARG1@ -- su --whitelist-environment container_hostname - $(whoami)"
+ set_alias 'login' 'exec @ARGS@ --mode interactive -- /bin/bash -c $@${user:-root} - exec su --whitelist-environment container_hostname - '
+ set_alias 'll' 'list -c ns4mDN'
+
+ # test environment container hostname
+ local env_container_hostname=$(lxc profile get default environment.container_hostname)
+ if [[ -z "$env_container_hostname" ]]; then
+ env_container_hostname=$(hostname -s)
+ if env | grep -q container_hostname; then
+ local previous_container_hostname=$(env | grep container_hostname | cut -d '=' -f2)
+ env_container_hostname="$previous_container_hostname $env_container_hostname"
+ fi
+
+ echo -n "set environment container_hostname to <$env_container_hostname> ... "
+ lxc profile set default environment.container_hostname "$env_container_hostname"
+ PREFIX="" echoinfo OK
+ else
+ echo "environment container_hostname <$env_container_hostname> already defined!"
+ fi
+
+ if ! grep -q "root:$(id -u):1" /etc/subuid; then
+ echo -n "subuid, subgid allowing <$(whoami)> ..."
+ printf "root:$(id -u):1\n" | sudo tee -a /etc/subuid /etc/subgid
+ PREFIX="" echoinfo DONE
+
+ # root:1000:1
+ # root:100000:65536
+ # _lxd:100000:65536
+ # :100000:65536
+
+ else
+ echo "subuid, subgid allowing <$(whoami)> already done!"
+ fi
+
+ if [[ ! -d "$HOME/LXD/SHARED" ]]; then
+ echo -n "$HOME/LXD/SHARED creating ... "
+ mkdir "$HOME/LXD/SHARED" -p
+ PREFIX="" echoinfo DONE
+ else
+ echo "folder <$HOME/LXD/SHARED> already created!"
+ fi
+
+ if [[ ! -d "$HOME/LXD/BACKUP" ]]; then
+ echo -n "$HOME/LXD/SHARED creating ... "
+ mkdir "$HOME/LXD/SHARED" -p
+ PREFIX="" echoinfo DONE
+ else
+ echo "folder <$HOME/LXD/BACKUP> already created!"
+ fi
+
+}
+
+function set_alias {
+ local name="$1"
+ local command="$2"
+ if ! lxc alias list -f csv | grep -q "^$name,"; then
+ echo -n "define lxc alias $name ..."
+ lxc alias add "$name" "$command"
+ PREFIX="" echoinfo OK
+ else
+ echo "lxc alias "$name" already defined!"
+ fi
+
+}
+
+function miaou_evalfrombashrc() {
+ local PREFIX="miaou:bashrc"
+ output=$(
+ /opt/debian-bash/tools/append_or_replace \
+ "^eval \"\\$\($MIAOU_BASEDIR/lib/install.sh shellenv\)\"$" \
+ "eval \"\$($MIAOU_BASEDIR/lib/install.sh shellenv)\"" \
+ "$HOME/.bashrc"
+ )
+
+ if [[ "$output" == "appended" ]]; then
+ echo "new path <$MIAOU_BASEDIR> created!"
+ SESSION_RELOAD_REQUIRED=true
+ else
+ echo "path <$MIAOU_BASEDIR> already loaded!"
+ fi
+}
+
+function ask_target() {
+ PS3='Choose miaou target purpose: '
+ foods=("Dev" "Beta" "Prod")
+ select ans in "${foods[@]}"; do
+ builtin echo "${ans^^}"
+ break
+ done
+}
+
+function check_credential {
+ local PREFIX="check:credential"
+
+ check_yaml_defined_value /etc/miaou/defaults.yaml 'credential.username' &&
+ check_yaml_defined_value /etc/miaou/defaults.yaml 'credential.shadow' &&
+ check_yaml_defined_value /etc/miaou/defaults.yaml 'credential.email' &&
+ check_yaml_defined_value /etc/miaou/defaults.yaml 'credential.password'
+
+}
+
+function check_target() {
+ case "${TARGET^^}" in
+ DEV) ;;
+ BETA) ;;
+ PROD) ;;
+ *)
+ if [[ -f /etc/miaou/defaults.yaml ]]; then
+ # load already defined target in expanded conf
+ TARGET=$(grep -Es "^target:" /etc/miaou/defaults.yaml | cut -d ' ' -f2)
+ else
+ TARGET=$(ask_target)
+ fi
+ ;;
+ esac
+ TARGET=${TARGET,,} # downcase
+ return 0
+}
+
+function miaou_configfiles() {
+ local PREFIX="miaou:config"
+
+ if [[ ! -d /etc/miaou ]]; then
+ echo -n "configuration initializing ..."
+ sudo mkdir -p /etc/miaou
+ sudo chown "$USER" /etc/miaou
+ PREFIX="" echoinfo OK
+ fi
+
+ if [[ ! -f /etc/miaou/defaults.yaml ]]; then
+ echo -n "building /etc/miaou/defaults.yaml for the first time..."
+ shadow_passwd=$(sudo grep "$CURRENT_USER" /etc/shadow | cut -d ':' -f2)
+ env current_user="$CURRENT_USER" shadow_passwd="$shadow_passwd" tera -e --env-key env --env-only -t "$MIAOU_BASEDIR/templates/etc/defaults.yaml.j2" -o /etc/miaou/defaults.yaml >/dev/null
+ yq ".target=\"$TARGET\"" /etc/miaou/defaults.yaml -i
+ PREFIX="" echoinfo OK
+ fi
+
+ if [[ ! -f /etc/miaou/miaou.yaml ]]; then
+ echo -n "building /etc/miaou/miaou.yaml for the first time..."
+ cp "$MIAOU_BASEDIR/templates/etc/miaou.yaml.j2" /etc/miaou/miaou.yaml
+ PREFIX="" echoinfo OK
+ fi
+
+ PREVIOUS_TARGET=""
+ echo "expanded configuration stored in <$MIAOU_CONFIGDIR>!"
+ [[ -f "$EXPANDED_CONF" ]] && PREVIOUS_TARGET=$(grep -Es "^target:" "$EXPANDED_CONF" | cut -d ' ' -f2)
+
+ if [[ "$PREVIOUS_TARGET" != "$TARGET" ]]; then
+ if [[ -z "$PREVIOUS_TARGET" ]]; then
+ echo "new target defined <$TARGET>"
+ else
+ echowarnn "TARGET has changed from <$PREVIOUS_TARGET> to <$TARGET>, do you agree?"
+ if askConfirmation N; then
+ echowarn "removing previous settings, please restart to apply changes"
+ rm "$MIAOU_CONFIGDIR" -rf
+ else
+ echoerr "TARGET not accepted, exit"
+ exit 102
+ fi
+ fi
+ yq ".target=\"$TARGET\"" /etc/miaou/defaults.yaml -i
+ else
+ echo "target <$TARGET> already defined!"
+ fi
+}
+
+function opt_link() {
+ if [[ $MIAOU_BASEDIR != '/opt/miaou' ]]; then
+ if [[ -L '/opt/miaou' && -d '/opt/miaou' && $(readlink /opt/miaou) == "$MIAOU_BASEDIR" ]]; then
+ echo "symbolic link /opt/miaou already set up!"
+ else
+ sudo rm -f /opt/miaou
+ sudo ln -s "$MIAOU_BASEDIR" /opt/miaou
+ echo "symbolic link /opt/miaou successfully defined!"
+ fi
+ else
+ echo "real path /opt/miaou already set up!"
+ fi
+}
+
+function miaou_resolver() {
+ local PREFIX="miaou:resolver"
+ bridge=$(ip addr show lxdbr0 | grep "inet\b" | awk '{print $2}' | cut -d/ -f1)
+ gateway=$(ip route | grep default | cut -d' ' -f3)
+
+ if command -v nmcli &>/dev/null; then
+ if [[ ! -f /etc/NetworkManager/dispatcher.d/50-miaou-resolver ]]; then
+ echo -n "use NetworkManager dispatcher to deal with LXD bridge automatically..."
+ sudo cp "$MIAOU_BASEDIR/templates/network-manager/50-miaou-resolver" /etc/NetworkManager/dispatcher.d/
+ sudo chmod +x /etc/NetworkManager/dispatcher.d/50-miaou-resolver
+ ACTIVE_CONNECTION=$(nmcli -g NAME connection show --active | head -n1)
+ nmcli connection up "$ACTIVE_CONNECTION" &>/dev/null
+ PREFIX="" echoinfo OK
+ else
+ echo "miaou-resolver in NetworkManager dispatcher already initialized!"
+ fi
+ else
+ if ! grep -q "nameserver $bridge" /etc/resolv.conf; then
+ echo "customize resolv.conf from scratch (SERVER)..."
+ sudo tee /etc/resolv.conf &>/dev/null </dev/null; then
+ echo "further details required, please replace any by a proper value ...press any key to open editor"
+ read -rn1
+ editor /etc/miaou/defaults.yaml
+ fi
+ check_credential
+ echo "successfully checked!"
+}
+
+### MAIN
+
+if [[ "${1:-}" == "SESSION_RELOAD_REQUIRED" ]]; then
+ SESSION_RELOAD_REQUIRED=true
+ shift
+else
+ SESSION_RELOAD_REQUIRED=false
+fi
+
+if [[ "${1:-}" == "shellenv" ]]; then
+ unset PREFIX
+ echo "export MIAOU_BASEDIR=$MIAOU_BASEDIR"
+ echo "export PATH=\"\$MIAOU_BASEDIR/scripts\":\$PATH"
+else
+
+ . "$MIAOU_BASEDIR/lib/init.sh"
+
+ trap 'status=$?; on_exit; exit $status' EXIT
+ trap 'trap - HUP; on_exit SIGHUP; kill -HUP $$' HUP
+ trap 'trap - INT; on_exit SIGINT; kill -INT $$' INT
+ trap 'trap - TERM; on_exit SIGTERM; kill -TERM $$' TERM
+
+ PREFIX="miaou"
+ : $PREFIX
+ TARGET=${1:-}
+ CURRENT_USER=$(id -un)
+
+ check_target
+ sudo_required
+ install_debian_bash
+ install_mandatory_commands
+ prepare_toolbox
+ add_toolbox_sudoers
+ prepare_nftables
+ prepare_lxd "$@"
+ override_lxd_service_to_reload_nftables
+ miaou_resolver
+ miaou_evalfrombashrc
+ miaou_configfiles
+ ask_for_credential
+ prepare_nftables
+ opt_link
+ extra_dev_desktop
+
+ if [[ "$SESSION_RELOAD_REQUIRED" == false ]]; then
+ echoinfo "successful installation"
+ else
+ echowarn "please reload your session, .bashrc needs to be reloaded!"
+ fi
+fi
diff --git a/recipes/cagettepei/crud.sh b/recipes/cagettepei/crud.sh
new file mode 100755
index 0000000..b34692b
--- /dev/null
+++ b/recipes/cagettepei/crud.sh
@@ -0,0 +1,218 @@
+#!/bin/bash
+
+function check_database_exists() {
+ db-maria list | grep -q "$longname"
+}
+
+function check_port_used() {
+ # shellcheck disable=SC2034
+ usedport=$(lxc exec "$container" -- bash -c "grep Listen /etc/apache2/sites-enabled/$longname.conf | cut -d' ' -f2")
+
+ [[ "$usedport" == "$port" ]]
+}
+
+function check_service_running() {
+ lxc exec "$container" -- bash -c "systemctl is-active --quiet apache2.service"
+}
+
+function check_config_defined() {
+ lxc exec "$container" -- bash -c "test -f /var/www/cagettepei/$shortname/config.xml"
+}
+
+function _read() {
+ disable_trace
+ check_database_exists
+ check_container "$container"
+ check_port_used
+ check_config_defined
+ check_service_running
+ enable_trace
+ return 0
+}
+
+function _create() {
+ echo "creating CagettePéi instance for <$shortname> ... "
+
+ mkdir -p "$MIAOU_CONFIGDIR/apps/cagettepei"
+ APP_PORT=$port APP_NAME=$shortname tera -e --env-key env -t "$MIAOU_DIR/templates/apps/cagettepei/cagettepei-host.j2" -o "$MIAOU_CONFIGDIR/apps/cagettepei/$longname.conf" "$MIAOU_CONFIGDIR/miaou.expanded.yaml"
+ echo "creating templates ... OK"
+
+ echo "copying files over container <$container> ... "
+ lxc file push --uid 0 --gid 0 "$MIAOU_CONFIGDIR/apps/cagettepei/$longname.conf" "$container/etc/apache2/sites-available/$longname.conf"
+ echo "copying files over container <$container> ... OK"
+
+ if ! (db-maria list | grep -q "$longname"); then
+ echo "create empty database <$longname> ... "
+ db-maria create "$longname"
+ echo "create empty database <$longname> ... OK"
+ DB_INIT=true
+ else
+ echo "database already exists!"
+ DB_INIT=false
+ fi
+
+ credential_username=$(load_yaml_from_expanded credential.username)
+ credential_email=$(load_yaml_from_expanded credential.email)
+
+ echo "initialize cagettepei $shortname $longname ..."
+ lxc exec "$container" -- bash < /var/www/cagettepei/$shortname/config.xml
+
+EOT2
+ else
+ echo "config.xml already defined!"
+ fi
+
+ a2ensite $longname.conf
+ mkdir -p /var/log/apache2/cagettepei/$shortname
+ systemctl restart apache2
+
+ if [[ $DB_INIT == true ]]; then
+ echo "Force TABLES initialization"
+ curl localhost:$port
+ echo "Set administrator password..."
+ echo "insert into User values (1,'fr','c3513c793b13471f3a49bdb22acb66de',1,'$credential_username','Admin', '$credential_email', null, null, null, null, null, null, null, null, null, now(), now(),6,null, null);" | mariadb cagettepei-$shortname -u cagettepei-$shortname -pcagettepei-$shortname -h ct1.lxd
+ echo "TODO: password \`cagette\` should be changed soon!!!"
+ fi
+EOF
+ echo "initialize cagettepei $shortname $longname ... OK"
+}
+
+function _update() {
+ echo "update"
+}
+
+function _delete() {
+ echo "delete"
+}
+
+function usage() {
+ echo "Usage: $COMMAND_NAME -c|r|u|d --port PORT --container CONTAINER --name NAME"
+ exit 2
+}
+
+### MAIN
+
+# init_strict
+
+COMMAND_NAME=$(basename "$0")
+
+# read the options
+
+TEMP=$(getopt -n "$COMMAND_NAME" -o crud --long port:,container:,name:,fqdn: -- "$@")
+# shellcheck disable=SC2181
+[[ "$?" -eq 0 ]] || usage
+eval set -- "$TEMP"
+
+action="unset"
+port="unset"
+container="unset"
+shortname="unset"
+longname="unset"
+fqdn="unset"
+
+# extract options and their arguments into variables.
+while true; do
+ case "$1" in
+ --port)
+ port=$2
+ shift 2
+ ;;
+ --fqdn)
+ fqdn=$2
+ shift 2
+ ;;
+ --container)
+ container=$2
+ shift 2
+ ;;
+ --name)
+ shortname=$2
+ longname="cagettepei-$shortname"
+ shift 2
+ ;;
+ -c)
+ [[ "$action" == "unset" ]] || usage
+ action="_create"
+ shift 1
+ ;;
+ -r)
+ [[ "$action" == "unset" ]] || usage
+ action="_read"
+ shift 1
+ ;;
+ -u)
+ [[ "$action" == "unset" ]] || usage
+ action="_update"
+ shift 1
+ ;;
+ -d)
+ [[ "$action" == "unset" ]] || usage
+ action="_delete"
+ shift 1
+ ;;
+ --)
+ shift
+ break
+ ;;
+ *)
+ echo "Internal error!"
+ exit 1
+ ;;
+ esac
+done
+
+[[
+ "$action" != unset &&
+ "$port" != unset &&
+ "$container" != unset &&
+ "$fqdn" != unset &&
+ "$shortname" != unset ]] || usage
+
+. "$MIAOU_BASEDIR/lib/init.sh"
+
+$action
diff --git a/recipes/cagettepei/install.sh b/recipes/cagettepei/install.sh
new file mode 100755
index 0000000..1f6afcd
--- /dev/null
+++ b/recipes/cagettepei/install.sh
@@ -0,0 +1,187 @@
+#!/bin/bash
+
+readonly UNWANTED_PACKAGES_STRING="nginx node python haxe"
+readonly MANDATORY_PACKAGES_STRING="wget apache2 make git imagemagick gettext libapache2-mod-neko mariadb-client sendemail libio-socket-ssl-perl libnet-ssleay-perl"
+
+### CHECK
+
+function check() {
+ PREFIX="recipe:cagettepei:check"
+ check_unwanted_packages || return 21
+ check_mandatory_packages || return 22
+ check_apache_modules || return 23
+ check_node8 || return 24
+ check_python2 || return 25
+ check_haxe3 || return 26
+ check_cagettepei_batch || return 35
+ check_cagettepei_timers || return 36
+ echo "container <$CONTAINER> approved successfully!"
+}
+
+function check_apache_modules() {
+ lxc exec "$CONTAINER" -- bash <&1 | grep -q 'Python 2.7.18'
+EOF
+}
+
+function check_haxe3() {
+ lxc exec "$CONTAINER" -- bash <&1 | grep -q '3.4.7'
+EOF
+}
+
+function check_unwanted_packages() {
+ lxc exec "$CONTAINER" -- bash </dev/null | grep -q ^ii)
+ done
+ true # useful because for might return last inexistant package
+EOF
+}
+
+function check_mandatory_packages() {
+ lxc exec "$CONTAINER" -- bash </dev/null | grep -q ^ii
+ done
+EOF
+}
+
+### INSTALL
+
+function install() {
+ PREFIX="recipe:cagettepei:install"
+ : $PREFIX
+
+ launch_container "$CONTAINER"
+ echo "initializing CagettePéi ... "
+
+ echo -n "check unwanted packages..."
+ check_unwanted_packages
+ PREFIX="" echo "OK"
+
+ lxc exec "$CONTAINER" -- bash </,/<\/Directory>/ s/AllowOverride None/AllowOverride All/' /etc/apache2/apache2.conf
+ sed -i 's/^Listen 80$//' /etc/apache2/ports.conf
+
+ echo "prepare folder for cagettepei instances"
+ mkdir -p /var/www/cagettepei
+
+ echo "enable neko and rewrite apache2 modules"
+ a2enmod neko
+ a2enmod rewrite
+EOF
+
+ echo -n "copy cagettepei-batch..."
+ lxc file push --uid 0 --gid 0 "$MIAOU_BASEDIR/templates/apps/cagettepei/cagettepei-batch" "$CONTAINER/var/www/cagettepei/cagettepei-batch"
+ lxc exec "$CONTAINER" -- chmod +x /var/www/cagettepei/cagettepei-batch
+ PREFIX="" echo "OK"
+
+ echo -n "copy cagettepei timers in systemd..."
+ lxc file push --uid 0 --gid 0 "$MIAOU_BASEDIR/templates/apps/cagettepei/systemd/cagettepei-batch-minute.service" "$CONTAINER/etc/systemd/system/cagettepei-batch-minute.service"
+ lxc file push --uid 0 --gid 0 "$MIAOU_BASEDIR/templates/apps/cagettepei/systemd/cagettepei-batch-minute.timer" "$CONTAINER/etc/systemd/system/cagettepei-batch-minute.timer"
+ lxc file push --uid 0 --gid 0 "$MIAOU_BASEDIR/templates/apps/cagettepei/systemd/cagettepei-batch-day.service" "$CONTAINER/etc/systemd/system/cagettepei-batch-day.service"
+ lxc file push --uid 0 --gid 0 "$MIAOU_BASEDIR/templates/apps/cagettepei/systemd/cagettepei-batch-day.timer" "$CONTAINER/etc/systemd/system/cagettepei-batch-day.timer"
+ PREFIX="" echo "OK"
+
+ echo -n "override apache2 service to launch cagettepei timers..."
+ lxc exec "$CONTAINER" -- bash -c "SYSTEMD_EDITOR=tee systemctl edit apache2 < approved successfully!"
+ return 0
+}
+
+function check_reverseproxy() {
+ lxc exec "$CONTAINER" -- bash < ..."
+
+ if ! container_exists "$CONTAINER"; then
+ echowarn "about to create new container <$CONTAINER> ..."
+ lxc-miaou-create "$CONTAINER"
+ echo OK
+ fi
+
+ if ! container_running "$CONTAINER"; then
+ echowarn "about to start asleep container <$CONTAINER> ..."
+ lxc start "$CONTAINER"
+ echo OK
+ fi
+
+ credential_email=$(load_yaml_from_expanded credential.email)
+ lxc exec "$CONTAINER" -- bash <"
+ certbot register --agree-tos --email $credential_email --no-eff-email || echo "already resgistered!"
+
+ rm /etc/nginx/sites-{enabled,available}/default -f
+ systemctl enable nginx
+
+ nginx -tq || rm /etc/nginx/sites-enabled/hosts
+ systemctl start nginx
+EOF
+
+ if [[ "$TARGET" != "prod" ]]; then
+ echo "copying Nginx banner to container <$CONTAINER> ... "
+ lxc file push --uid 0 --gid 0 "$MIAOU_BASEDIR/templates/nginx/snippets/banner_$TARGET.conf" "$CONTAINER/etc/nginx/snippets/banner_$TARGET.conf"
+ echo "copying files over container <$CONTAINER> ... OK"
+ else
+ echo "no Nginx banner on PROD!"
+ fi
+
+ echo "populate nftables entries into yaml"
+ local wan_interface dmz_ip
+ wan_interface=$(ip route show default | cut -d ' ' -f5)
+ dmz_ip=$(host "$CONTAINER.lxd" | cut -d ' ' -f4)
+ yq ".nftables.wan_interface=\"$wan_interface\"" "$EXPANDED_CONF" -i
+ yq ".nftables.dmz_ip=\"$dmz_ip\"" "$EXPANDED_CONF" -i
+
+ local nftables_reloading=false
+ if [[ "$TARGET" != "dev" ]]; then
+ mkdir -p "$MIAOU_CONFIGDIR/nftables.rules.d"
+ echo "nat http/s port to dmz"
+ tera -t "$MIAOU_BASEDIR/templates/nftables/nat.table.j2" "$EXPANDED_CONF" -o "$MIAOU_CONFIGDIR/nftables.rules.d/nat.table" &>/dev/null
+ sudo cp "$MIAOU_CONFIGDIR/nftables.rules.d/nat.table" /etc/nftables.rules.d/nat.table
+ nftables_reloading=true
+ else
+ if [[ -f /etc/nftables.rules.d/nat.table ]]; then
+ sudo_required "remove previous nat.table"
+ sudo rm -f /etc/nftables.rules.d/nat.table
+ nftables_reloading=true
+ fi
+ fi
+ if [[ "$nftables_reloading" == true ]]; then
+ sudo_required "reload nftables"
+ sudo systemctl reload nftables.service
+ fi
+
+}
+
+# MAIN
+. "$MIAOU_BASEDIR/lib/init.sh"
+
+arg1_required "$@"
+readonly CONTAINER="$1"
+
+check || (
+ install
+ check
+)
diff --git a/recipes/dokuwiki/install.sh b/recipes/dokuwiki/install.sh
new file mode 100755
index 0000000..1406ab0
--- /dev/null
+++ b/recipes/dokuwiki/install.sh
@@ -0,0 +1,21 @@
+#!/bin/bash
+
+function check() {
+ echowarn "recipe:dokuwiki not yet checked!"
+ return 0
+}
+
+function install() {
+ echowarn "recipe:dokuwiki not yet initialized!"
+}
+
+. "$MIAOU_BASEDIR/lib/init.sh"
+
+arg1_required "$@"
+readonly CONTAINER="$1"
+launch_container "$CONTAINER"
+
+check || (
+ install
+ check
+)
diff --git a/recipes/dolibarr/crud.sh b/recipes/dolibarr/crud.sh
new file mode 100755
index 0000000..72bd080
--- /dev/null
+++ b/recipes/dolibarr/crud.sh
@@ -0,0 +1,180 @@
+#!/bin/bash
+
+function check_database_exists() {
+ db-psql list | grep -q "$longname"
+}
+
+function check_port_used() {
+ # shellcheck disable=SC2034
+ usedport=$(lxc exec "$container" -- cat /etc/nginx/sites-enabled/"$longname".conf | grep listen | cut -d ' ' -f2)
+ [[ "$usedport" == "$port" ]]
+}
+
+function check_directory_exists() {
+ lxc exec "$container" -- test -d /var/www/"$longname"
+}
+
+function check_service_running() {
+ lxc exec "$container" -- bash -c "systemctl is-active --quiet nginx.service"
+}
+
+function _read() {
+ disable_trace
+ check_database_exists
+ check_container "$container"
+ check_port_used
+ check_directory_exists
+ check_service_running
+ enable_trace
+ return 0
+}
+
+function _create() {
+ PREFIX="recipe:dolibarr:create"
+ : $PREFIX
+
+ echo "create a Dolibarr instance for $shortname"
+
+ lxc exec "$container" -- bash </dev/null
+
+ echo "copying configuration files onto container <$container>... "
+ lxc file push --uid 0 --gid 0 "$MIAOU_CONFIGDIR/apps/dolibarr/$shortname/host.conf" "$container/etc/nginx/sites-available/$longname.conf"
+ echo "copying files over container <$container> ... OK"
+
+ if ! (db-psql list | grep -q "$longname"); then
+ echo "create empty database <$longname> ... "
+ db-psql create "$longname"
+ echo "create empty database <$longname> ... OK"
+
+ else
+ echo "database already exists!"
+ fi
+
+ echo "enable host config..."
+ lxc exec "$container" -- bash < approved successfully!"
+ return 0
+}
+
+function check_mandatory_packages() {
+ lxc exec "$CONTAINER" -- bash </dev/null | grep -q ^ii
+ done
+EOF
+}
+
+function check_one_release() {
+ lxc exec "$CONTAINER" -- /TOOLBOX/fd -1q -tf "dolibarr-" /var/www
+}
+
+function install() {
+ echo "recipe:dolibarr installing..."
+ lxc exec "$CONTAINER" -- bash </dev/null
+ apt update
+ apt install -y $MANDATORY_PACKAGES_STRING
+ cd /var/www
+ PATH="\$PATH:/opt/debian-bash/tools"
+ VERSION="\$(wget_semver github Dolibarr/dolibarr)"
+ if [[ ! -f "dolibarr-\$VERSION.tgz" ]]; then
+ wget_release github Dolibarr/dolibarr
+ else
+ echo "dolibarr version=\$VERSION already downloaded!"
+ fi
+EOF
+}
+
+. "$MIAOU_BASEDIR/lib/init.sh"
+
+arg1_required "$@"
+readonly CONTAINER="$1"
+launch_container "$CONTAINER"
+
+check || (
+ install
+ check
+)
diff --git a/recipes/mariadb/install.sh b/recipes/mariadb/install.sh
new file mode 100755
index 0000000..8129fe6
--- /dev/null
+++ b/recipes/mariadb/install.sh
@@ -0,0 +1,65 @@
+#!/bin/bash
+
+function check() {
+ PREFIX="recipe:mariadb:check"
+ dpkg -l mariadb-client | grep -q ^ii || return 9
+ container_running "$CONTAINER" || return 10
+ cat </dev/null
+ ss -tlnp | grep 0.0.0.0:3306 | grep -q maria
+
+ test -f /etc/default/automysqlbackup
+ grep -q BACKUPDIR=\"/mnt/BACKUP/mariadb\" /etc/default/automysqlbackup
+EOF
+ echo "container <$CONTAINER> approved successfully!"
+ return 0
+}
+
+function build_device_backup() {
+ PREFIX="recipe:mariadb:backup"
+ if ! (lxc config device list "$CONTAINER" | grep -q BACKUP); then
+ local backup_dir="$HOME/LXD/BACKUP/databases-$CONTAINER"
+ mkdir -p "$backup_dir"
+ lxc config device add "$CONTAINER" BACKUP disk source=$backup_dir path=mnt/BACKUP
+ fi
+}
+
+function install() {
+ sudo_required
+ PREFIX="recipe:mariadb:install"
+ : $PREFIX
+
+ sudo /opt/debian-bash/tools/idem_apt_install mariadb-client
+ echowarn "initializing ..."
+ launch_container "$CONTAINER"
+ build_device_backup
+ echowarn "executing various commands onto container <$CONTAINER>, please be patient ..."
+ lxc exec "$CONTAINER" -- bash </dev/null
+ . /opt/debian-bash/lib/functions.sh
+ apt update && apt dist-upgrade -y
+ /opt/debian-bash/tools/idem_apt_install mariadb-server automysqlbackup
+ echo "change bind-adress"
+ /opt/debian-bash/tools/append_or_replace "^bind-address.*$" "bind-address = 0.0.0.0" /etc/mysql/mariadb.conf.d/50-server.cnf
+ systemctl restart mariadb.service
+
+ function systemctl-exists() ([ \$(systemctl list-unit-files "\${1}*" | wc -l) -gt 3 ])
+ systemctl-exists exim4.service && systemctl stop exim4.service && systemctl disable exim4.service
+ /opt/debian-bash/tools/append_or_replace "^BACKUPDIR=.*$" "BACKUPDIR=\"/mnt/BACKUP/mariadb\"" /etc/default/automysqlbackup
+ exit 0
+EOF
+ echo DONE
+}
+
+# MAIN
+. "$MIAOU_BASEDIR/lib/init.sh"
+
+arg1_required "$@"
+readonly CONTAINER="$1"
+
+check || (
+ install
+ check
+)
diff --git a/recipes/odoo12/crud.sh b/recipes/odoo12/crud.sh
new file mode 100755
index 0000000..f4fd0a7
--- /dev/null
+++ b/recipes/odoo12/crud.sh
@@ -0,0 +1,180 @@
+#!/bin/bash
+
+function check_database_exists() {
+ db-psql list | grep -q "$longname"
+}
+
+function check_port_used() {
+ # shellcheck disable=SC2034
+ usedport=$(lxc exec "$container" -- bash -c "grep xmlrpc_port /etc/odoo12/$shortname.conf | cut -d' ' -f3")
+ [[ "$usedport" == "$port" ]]
+}
+
+function check_service_running() {
+ lxc exec "$container" -- bash -c "systemctl is-active --quiet ${longname}.service"
+}
+
+function _read() {
+ disable_trace
+ check_database_exists
+ check_container "$container"
+ check_port_used
+ check_service_running
+ enable_trace
+ return 0
+}
+
+function _create() {
+
+ echo "creating templates ... "
+ mkdir -p "$MIAOU_CONFIGDIR/apps/odoo12"
+
+ longport=$((port + 1000))
+ APP_PORT=$port LONG_PORT=$longport APP_NAME=$shortname tera -e -t "$MIAOU_DIR/templates/apps/odoo12/odoo.conf.j2" -o "$MIAOU_CONFIGDIR/apps/odoo12/$shortname.conf" "$MIAOU_CONFIGDIR/miaou.expanded.yaml" >/dev/null
+ APP_NAME=$shortname tera -t "$MIAOU_DIR/templates/apps/odoo12/odoo.service.j2" --env-only -o "$MIAOU_CONFIGDIR/apps/odoo12/$longname.service" >/dev/null
+ echo "creating templates ... OK"
+
+ echo "copying files over container <$container> ... "
+ lxc file push --uid 0 --gid 0 "$MIAOU_CONFIGDIR/apps/odoo12/$shortname.conf" "$container/etc/odoo12/$shortname.conf"
+ lxc file push --uid 0 --gid 0 "$MIAOU_CONFIGDIR/apps/odoo12/$longname.service" "$container/etc/systemd/system/$longname.service"
+ echo "copying files over container <$container> ... OK"
+
+ if ! (db-psql list | grep -q "$longname"); then
+ echo "create empty database <$longname> ... "
+ db-psql create "$longname"
+ echo "create empty database <$longname> ... OK"
+
+ credential_username=$(load_yaml_from_expanded credential.username)
+ credential_password=$(load_yaml_from_expanded credential.password)
+ cat </dev/null) || return 12
+ return 0
+}
+
+function check_target_bgcolor() {
+ (lxc exec "$CONTAINER" -- grep -Pq "^\\\$o-community-color: $BACKGROUND_COLOR" /home/odoo/odoo12/addons/web/static/src/scss/primary_variables.scss) || return 13
+ return 0
+}
+
+function check_wkhtmltox() {
+ (lxc exec "$CONTAINER" -- dpkg -l | grep -s wkhtmltox | grep -qs $WKHTML_VERSION) || return 1
+}
+
+function check_python() {
+ (lxc exec "$CONTAINER" -- test -d /opt/Python-3.7.13) || return 1
+}
+
+function check_venv() {
+ (lxc exec "$CONTAINER" -- test -d /home/odoo/venv) || return 1
+}
+
+function check_favicon() {
+ lxc exec "$CONTAINER" -- test -L /home/odoo/odoo12/addons/web/static/src/img/favicon.ico
+}
+
+function check_file_odoo-addon-install() {
+ (lxc exec "$CONTAINER" -- test -f /home/odoo/odoo12/odoo12-addon-install) || return 23
+}
+
+function check() {
+ PREFIX="recipe:odoo12:check"
+ check_wkhtmltox || return 10
+ check_python || return 11
+ check_user_odoo || return 12
+ check_target_bgcolor || return 13
+ check_venv || return 14
+ check_favicon || return 15
+ check_file_odoo-addon-install || return 23
+
+ echo "container <$CONTAINER> approved successfully!"
+ return 0
+}
+
+function install() {
+ PREFIX="recipe:odoo12:install"
+ : $PREFIX
+
+ launch_container "$CONTAINER"
+ echo "initializing Odoo12 ... "
+
+ lxc exec "$CONTAINER" -- bash </dev/null; then
+ echo "creating system user "
+ useradd -rms /bin/bash odoo
+ else
+ echo "user already exists!"
+ fi
+
+ cat < added!"
+EOF
+
+ echo "push various target-related favicons..."
+ for favicon in "$MIAOU_BASEDIR"/templates/apps/odoo12/favicon/*.ico; do
+ lxc file push --uid 0 --gid 0 "$favicon" "$CONTAINER/home/odoo/odoo12/addons/web/static/src/img/"
+ done
+ echo "OK"
+
+ echo "adjust symbolic link according to target=<$TARGET>"
+ lxc exec "$CONTAINER" -- rm -f /home/odoo/odoo12/addons/web/static/src/img/favicon.ico
+ lxc exec "$CONTAINER" -- ln -s /home/odoo/odoo12/addons/web/static/src/img/favicon-"$TARGET".ico /home/odoo/odoo12/addons/web/static/src/img/favicon.ico
+ echo "OK"
+}
+
+function compute_bgcolor_target() {
+ case "$TARGET" in
+ dev) echo "#17a2b8" ;;
+ beta) echo "#79A70A" ;;
+ prod) echo "#7C7BAD" ;;
+ *) echoerr "unknown target <$TARGET>" && exit 10 ;;
+ esac
+}
+
+### MAIN
+
+. "$MIAOU_BASEDIR/lib/init.sh"
+arg1_required "$@"
+readonly CONTAINER="$1"
+TARGET=$(yq '.target' "$EXPANDED_CONF")
+readonly TARGET
+BACKGROUND_COLOR=$(compute_bgcolor_target)
+readonly BACKGROUND_COLOR
+
+check || (
+ install
+ check
+)
diff --git a/recipes/odoo15/crud.sh b/recipes/odoo15/crud.sh
new file mode 100755
index 0000000..3123fa1
--- /dev/null
+++ b/recipes/odoo15/crud.sh
@@ -0,0 +1,196 @@
+#!/bin/bash
+
+function check_database_exists() {
+ db-psql list | grep -q "$longname"
+}
+
+function check_port_used() {
+ # shellcheck disable=SC2034
+ usedport=$(lxc exec "$container" -- bash -c "grep xmlrpc_port /etc/odoo15/$shortname.conf | cut -d' ' -f3")
+ [[ "$usedport" == "$port" ]]
+}
+
+function check_service_running() {
+ lxc exec "$container" -- bash -c "systemctl is-active --quiet ${longname}.service"
+}
+
+function _read() {
+ disable_trace
+ check_database_exists
+ check_container "$container"
+ check_port_used
+ check_service_running
+ enable_trace
+ return 0
+}
+
+function _create() {
+
+ echo "creating templates ... "
+ mkdir -p "$MIAOU_CONFIGDIR/apps/odoo15"
+
+ longport=$((port + 1000))
+ APP_PORT=$port LONG_PORT=$longport APP_NAME=$shortname tera -e -t "$MIAOU_DIR/templates/apps/odoo15/odoo.conf.j2" -o "$MIAOU_CONFIGDIR/apps/odoo15/$shortname.conf" "$MIAOU_CONFIGDIR/miaou.expanded.yaml" >/dev/null
+ APP_NAME=$shortname tera -t "$MIAOU_DIR/templates/apps/odoo15/odoo.service.j2" --env-only -o "$MIAOU_CONFIGDIR/apps/odoo15/$longname.service" >/dev/null
+ echo "creating templates ... OK"
+
+ echo "copying files over container <$container> ... "
+ lxc file push --uid 0 --gid 0 "$MIAOU_CONFIGDIR/apps/odoo15/$shortname.conf" "$container/etc/odoo15/$shortname.conf"
+ lxc file push --uid 0 --gid 0 "$MIAOU_CONFIGDIR/apps/odoo15/$longname.service" "$container/etc/systemd/system/$longname.service"
+ echo "copying files over container <$container> ... OK"
+
+ echo "create data folder for $shortname"
+ cat < ... "
+ db-psql create "$longname"
+ echo "create empty database <$longname> ... OK"
+
+ credential_username=$(load_yaml_from_expanded credential.username)
+ credential_password=$(load_yaml_from_expanded credential.password)
+
+ cat </dev/null) || return 12
+ return 0
+}
+
+function check_target_bgcolor() {
+ (lxc exec "$CONTAINER" -- grep -Pq "^\\\$o-community-color: $BACKGROUND_COLOR" "$ODOO15_DIR/addons/web/static/src/legacy/scss/primary_variables.scss") || return 13
+ return 0
+}
+
+function check_file_odoo-addon-install() {
+ (lxc exec "$CONTAINER" -- test -f /home/odoo/odoo15/odoo-addon-install) || return 23
+ return 0
+}
+
+function check() {
+ PREFIX="recipe:odoo15:check"
+ check_user_odoo || return 21
+ check_target_bgcolor || return 22
+ check_file_odoo-addon-install || return 23
+
+ echo "container <$CONTAINER> approved successfully!"
+ return 0
+}
+
+function install() {
+ PREFIX="recipe:odoo15:install"
+ : $PREFIX
+ launch_container "$CONTAINER"
+ echo "initializing Odoo15 ... "
+
+ lxc exec "$CONTAINER" -- bash < /dev/null
+
+ echo "installing odoo15..."
+ apt update && apt dist-upgrade -y
+
+ echo "required packages"
+ apt install -y postgresql-client build-essential zlib1g-dev libssl-dev libxml2-dev libxslt1-dev libldap2-dev libsasl2-dev libpq-dev libffi-dev
+
+ if [[ ! -d /usr/local/share/python3.9 ]]; then
+ echo "install python-3.9.18"
+ cd /tmp
+ wget https://www.python.org/ftp/python/3.9.18/Python-3.9.18.tgz
+ tar -xf Python-3.9.18.tgz
+ mv Python-3.9.18 /usr/local/share/python3.9
+ cd /usr/local/share/python3.9
+ ./configure --enable-optimizations --enable-shared
+ make -j \$(nproc)
+ make altinstall
+ ldconfig /usr/local/share/python3.9
+ else
+ echo "python-3.9.18 already installed!"
+ fi
+
+ if dpkg -l | grep -s wkhtmltox | grep -qs $WKHTML_VERSION; then
+ echo package=wkhtmltox version=$WKHTML_RELEASE already found!
+ else
+ echo "wkhtmltox version=$WKHTML_RELEASE has to be installed!"
+ wget https://github.com/wkhtmltopdf/packaging/releases/download/$WKHTML_VERSION/wkhtmltox_$WKHTML_RELEASE.deb
+ dpkg -i wkhtmltox_$WKHTML_RELEASE.deb || (apt -fy install && rm wkhtmltox_$WKHTML_RELEASE.deb)
+ fi
+
+ if ! grep -q odoo /etc/passwd; then
+ echo "add user "
+ useradd -ms /bin/bash odoo
+ else
+ echo "user already set!"
+ fi
+
+ echo "install odoo15 in odoo userspace"
+ cat < added!"
+EOF
+}
+
+function compute_bgcolor_target() {
+ target=$(yq '.target' "$EXPANDED_CONF")
+ case "$target" in
+ dev) builtin echo "#17a2b8" ;;
+ beta) builtin echo "#79A70A" ;;
+ prod) builtin echo "#7C7BAD" ;;
+ *) echoerr "unknown target <$target>" && exit 10 ;;
+ esac
+}
+
+### MAIN
+
+. "$MIAOU_BASEDIR/lib/init.sh"
+arg1_required "$@"
+readonly CONTAINER="$1"
+BACKGROUND_COLOR=$(compute_bgcolor_target)
+readonly BACKGROUND_COLOR
+
+check || (
+ install
+ check
+)
diff --git a/recipes/postgresql/install.sh b/recipes/postgresql/install.sh
new file mode 100755
index 0000000..24fc4f7
--- /dev/null
+++ b/recipes/postgresql/install.sh
@@ -0,0 +1,68 @@
+#!/bin/bash
+
+function check() {
+ PREFIX="recipe:postgresql:check"
+ container_running "$CONTAINER" || return 10
+ echo "checking postgresql regarding access to the bridge subnet <$BRIDGE_SUBNET>..."
+
+ lxc exec "$CONTAINER" -- bash </dev/null
+ ss -tlnp | grep postgres | grep -q 0.0.0.0:5432
+ PG_VERSION=\$(pg_lsclusters -h | cut -d' ' -f1)
+ grep -Eq "^host.*all.*all.*$BRIDGE_SUBNET.*md5" /etc/postgresql/\$PG_VERSION/main/pg_hba.conf
+ test -f /etc/default/autopostgresqlbackup
+EOF
+ status="$?"
+ [[ $status -eq 0 ]] && echo "container <$CONTAINER> approved!"
+ return $status
+}
+
+function install() {
+ PREFIX="recipe:postgresql:install"
+ : "$PREFIX"
+
+ echowarn "initializing postgresql regarding access to the bridge subnet <$BRIDGE_SUBNET>..."
+
+ launch_container "$CONTAINER"
+ lxc exec "$CONTAINER" -- bash < files over container <$CONTAINER> ... "
+ lxc file push --uid 0 --gid 0 "$MIAOU_BASEDIR/templates/autopostgresqlbackup/script" "$CONTAINER/usr/sbin/autopostgresqlbackup"
+ lxc file push --uid 0 --gid 0 "$MIAOU_BASEDIR/templates/autopostgresqlbackup/cron.daily" "$CONTAINER/etc/cron.daily/autopostgresqlbackup"
+ lxc file push --uid 0 --gid 0 "$MIAOU_BASEDIR/templates/autopostgresqlbackup/default.conf" "$CONTAINER/etc/default/autopostgresqlbackup"
+ PREFIX="" echo OK
+
+}
+
+# MAIN
+. "$MIAOU_BASEDIR/lib/init.sh"
+
+arg1_required "$@"
+
+CONTAINER="$1"
+BRIDGE_SUBNET=$(lxc network get lxdbr0 ipv4.address)
+readonly CONTAINER BRIDGE_SUBNET
+
+check || (
+ install
+ check
+)
diff --git a/recipes/wordpress/crud.sh b/recipes/wordpress/crud.sh
new file mode 100755
index 0000000..d2e91af
--- /dev/null
+++ b/recipes/wordpress/crud.sh
@@ -0,0 +1,202 @@
+#!/bin/bash
+
+function check_database_exists() {
+ db-maria list | grep -q "$longname"
+}
+
+function check_port_used() {
+ # shellcheck disable=SC2034
+ usedport=$(lxc exec "$container" -- bash -c "grep listen /etc/nginx/sites-enabled/$longname.conf | cut -d' ' -f6")
+ [[ "$usedport" == "$port" ]]
+}
+
+function check_service_running() {
+ lxc exec "$container" -- bash -c "systemctl is-active --quiet nginx.service"
+}
+
+function _read() {
+ disable_trace
+ check_database_exists
+ check_container "$container"
+ check_port_used
+ check_service_running
+ enable_trace
+ return 0
+}
+
+function _create() {
+
+ echo "creating wordpress instance for <$shortname> ... "
+
+ mkdir -p "$MIAOU_CONFIGDIR/apps/wordpress"
+ APP_PORT=$port APP_NAME=$shortname tera -e --env-key env -t "$MIAOU_BASEDIR/templates/apps/wordpress/wp-host.j2" -o "$MIAOU_CONFIGDIR/apps/wordpress/$longname.conf" "$MIAOU_CONFIGDIR/miaou.expanded.yaml"
+ echo "creating templates ... OK"
+
+ echo "copying files over container <$container> ... "
+ lxc file push --uid 0 --gid 0 "$MIAOU_CONFIGDIR/apps/wordpress/$longname.conf" "$container/etc/nginx/sites-available/$longname.conf"
+ echo "copying files over container <$container> ... OK"
+
+ if ! (db-maria list | grep -q "$longname"); then
+ echo "create empty database <$longname> ... "
+ db-maria create "$longname"
+ echo "create empty database <$longname> ... OK"
+ else
+ echo "database already exists!"
+ fi
+
+ echo "initialize wordpress $shortname $longname ..."
+ lxc exec "$container" -- bash < /var/www/wordpress/$shortname/wp-config.php
+ approved successfully!"
+ return 0
+}
+
+function check_mandatory_packages() {
+ lxc exec "$CONTAINER" -- bash </dev/null | grep -q ^ii
+ done
+EOF
+}
+
+function check_wp-tool() {
+ lxc exec "$CONTAINER" -- test -f /usr/local/sbin/wp-tool
+}
+
+function check_wp-backup() {
+ lxc exec "$CONTAINER" -- test -f /usr/local/sbin/wp-backup
+}
+
+function check_wordpress_tgz() {
+ lxc exec "$CONTAINER" -- test -f /var/www/wordpress-latest.tgz
+}
+
+### INSTALL
+
+function install() {
+ PREFIX="recipe:wordpress:install"
+ : $PREFIX
+ launch_container "$CONTAINER"
+ echo "initializing Wordpress ... "
+
+ lxc exec "$CONTAINER" -- bash <\n"
+ printf "\t create [PASSWORD]\n"
+ printf "\t ---------------------------\n"
+ printf "\t backup [FOLDER]\n"
+ printf "\t restore \n"
+ printf "\t ---------------------------\n"
+ printf "\t rename \n"
+}
+
+list() {
+ lxc exec ct1 -- sh -c "echo \"SELECT schema_name FROM information_schema.schemata where schema_name not in ('information_schema','mariadb','mysql','performance_schema')\" | mariadb -u root --skip-column-names -r "
+}
+
+console() {
+ if [[ -z $1 ]]; then
+ lxc exec ct1 -- mariadb -u root
+ else
+ lxc exec ct1 -- sh -c "echo \"$1\" | mariadb -u root"
+ fi
+}
+
+connections() {
+ lxc exec ct1 -- sh -c "echo \"select id, user, host, db, command, time, state, info, progress from information_schema.processlist\" | mariadb -u root "
+}
+
+use() {
+ lxc exec ct1 -- mariadb -u root "$DB_NAME"
+}
+
+create() {
+
+ # shellcheck disable=SC1091
+ source /opt/debian-bash/lib/functions.sh
+
+ # shellcheck disable=SC2034
+ mapfile -t DBs < <(list)
+
+ local NEW_DB="${1:-$DB_NAME}"
+ local NEW_PASSWORD="${2:-$NEW_DB}"
+
+ if ! containsElement DBs "$NEW_DB"; then
+ lxc exec ct1 -- sh -c "echo \"\
+ CREATE DATABASE \\\`$NEW_DB\\\`; \
+ GRANT ALL ON \\\`$NEW_DB\\\`.* TO \\\`$NEW_DB\\\`@'%' IDENTIFIED BY '$NEW_PASSWORD'; \
+ FLUSH PRIVILEGES; \
+ \" | mariadb -u root"
+ else
+ echo "$NEW_DB already exists!"
+ fi
+}
+
+backup() {
+
+ if [[ ! -d "$FOLDER" ]]; then
+ echo "error: Folder required!"
+ file "$FOLDER"
+ exit 2
+ fi
+
+ mkdir -p "$FOLDER"
+ DATE=$(date '+%F')
+ ARCHIVE="$FOLDER"/$DB_NAME-$DATE.mariadb.gz
+
+ if [[ -f $ARCHIVE ]]; then
+ VERSION_CONTROL=numbered mv -b "$ARCHIVE" "$FOLDER"/"$DB_NAME"-"$DATE"-daily.mariadb.gz
+ fi
+
+ echo "backup $DB_NAME into $FOLDER"
+ mariadb-dump -h ct1.lxd -u "$DB_NAME" -p"$DB_NAME" "$DB_NAME" | gzip >"$ARCHIVE"
+ echo "archive file created: $ARCHIVE"
+}
+
+restore() {
+
+ echo "restore $DB_NAME $FILE"
+ if [[ ! -f "$FILE" ]]; then
+ echo "error: Backup file (*.mariadb.gz) required!"
+ file "$FILE"
+ exit 2
+ fi
+
+ PROCESSES=$(lxc exec ct1 -- sh -c "echo \"select id, user, host, db, command, time, state, info, progress from information_schema.processlist\" | mariadb -u root")
+
+ set +e
+ PROCESS_COUNT=$(echo "$PROCESSES" | grep -c "$DB_NAME")
+ if [[ $PROCESS_COUNT -gt 0 ]]; then
+ echo "FAILURE: There are some connections to database, please consider stopping bound services"
+ echo
+ echo "$PROCESSES"
+ exit 2
+ fi
+ set -e
+
+ if [[ "yes" == $(confirm "RESTORATION will drop DATABASE, please acknowledge with care!!!") ]]; then
+ if list | grep -q "$DB_NAME"; then
+ echo "backup <$DB_NAME> for safety reason"
+ backup
+ echo "drop database <$DB_NAME>"
+ lxc exec ct1 -- sh -c "echo \"DROP DATABASE \\\`$DB_NAME\\\`\" | mariadb -u root"
+ fi
+
+ echo "create <$DB_NAME>"
+ create
+ # lxc exec ct1 -- sh -c "CREATE DATABASE \\\`$DB_NAME\\\`\" | mariadb -u root"
+ gunzip -c "$FILE" | grep -av "^CREATE DATABASE" | grep -av "^USE" | mariadb -h ct1.lxd -u "$DB_NAME" -p"$DB_NAME" "$DB_NAME"
+ echo RESTORATION completed successfully
+
+ else
+ exit 1
+ fi
+}
+
+rename() {
+ echo "rename $DB_NAME to $NEW_NAME"
+ local DB_NAME_FOUND=false
+ for database in $(list); do
+ if [[ $database == "$DB_NAME" ]]; then
+ DB_NAME_FOUND=true
+ fi
+ if [[ $database == "$NEW_NAME" ]]; then
+ echoerr "$NEW_NAME already exists! please provide another name instead of <$NEW_NAME> or run list command"
+ exit 20
+ fi
+ done
+ if [[ ! $DB_NAME_FOUND ]]; then
+ echoerr "source <$DB_NAME> does not exist!"
+ exit 20
+ fi
+
+ if [[ "$DB_NAME" == "$NEW_NAME" ]]; then
+ echowarn "no need to rename; no change required <$DB_NAME>"
+ exit 0
+ fi
+
+ echo "create new database <$NEW_NAME>"
+ create "$NEW_NAME"
+
+ for table in $(console "use '$DB_NAME'; show tables"); do
+ if [[ $table != "Tables_in_$DB_NAME" ]]; then
+ echo "renaming table \`$DB_NAME\`.$table to \`$NEW_NAME\`.$table"
+ console "use '$DB_NAME'; rename table \\\`$DB_NAME\\\`.$table to \\\`$NEW_NAME\\\`.$table;"
+ fi
+ done
+
+ echo "every table has been renamed, so remove old database <$DB_NAME>"
+ console "drop user \\\`$DB_NAME\\\`"
+ console "drop database \\\`$DB_NAME\\\`"
+}
+
+# MAIN
+set -Eeuo pipefail
+# shellcheck source=/dev/null
+. "$MIAOU_BASEDIR/lib/functions.sh"
+
+[[ $# -lt 1 ]] && synopsis && exit 1
+ACTION=$1
+
+case $ACTION in
+console)
+ shift
+ TAIL=$*
+ console "$TAIL"
+ ;;
+list)
+ list
+ ;;
+connections)
+ connections
+ ;;
+use)
+ [[ $# -lt 2 ]] && synopsis && exit 1
+ DB_NAME=$2
+ use
+ ;;
+create)
+ [[ $# -lt 2 ]] && synopsis && exit 1
+ DB_NAME=$2
+ DB_PASSWORD=${3:-$DB_NAME}
+ create
+ ;;
+backup)
+ [[ $# -lt 2 ]] && synopsis && exit 1
+ DB_NAME=$2
+ FOLDER=${3:-$DEFAULT_BACKUP_FOLDER}
+ backup
+ ;;
+restore)
+ [[ $# -lt 3 ]] && synopsis && exit 1
+ DB_NAME=$2
+ FILE=$3
+ FOLDER=${4:-$DEFAULT_BACKUP_FOLDER}
+ DB_PASSWORD="$DB_NAME"
+ restore
+ ;;
+rename)
+ [[ $# -lt 3 ]] && synopsis && exit 1
+ DB_NAME=$2
+ NEW_NAME=$3
+ rename
+ ;;
+*)
+ synopsis
+ exit 1
+ ;;
+esac
diff --git a/scripts/db-psql b/scripts/db-psql
new file mode 100755
index 0000000..8b1743e
--- /dev/null
+++ b/scripts/db-psql
@@ -0,0 +1,200 @@
+#!/bin/bash
+
+confirm() {
+ read -p "$1 ([y]es or [N]o): "
+ case $(echo $REPLY | tr '[A-Z]' '[a-z]') in
+ y | yes) echo "yes" ;;
+ *) echo "no" ;;
+ esac
+}
+
+synopsis() {
+ echo "usage: "
+ printf "\t list | console | connections\n"
+ printf "\t ---------------------------\n"
+ printf "\t use \n"
+ printf "\t lookup \n"
+ printf "\t create [PASSWORD]\n"
+ printf "\t ---------------------------\n"
+ printf "\t backup [FOLDER]\n"
+ printf "\t restore [--yes]\n"
+ printf "\t ---------------------------\n"
+ printf "\t rename \n"
+}
+
+list() {
+ lxc exec ct1 -- su - postgres -c "psql -Atc \"SELECT datname FROM pg_database WHERE datistemplate=false AND datname<>'postgres';\""
+}
+
+console() {
+ if [[ -z $1 ]]; then
+ lxc exec ct1 -- su - postgres
+ else
+ lxc exec ct1 -- su - postgres -c "$1"
+ fi
+}
+
+connections() {
+ PROCESSES=$(console "psql -c \"select pid as process_id, usename as username, datname as database_name, client_addr as client_address, application_name, backend_start, state, state_change from pg_stat_activity WHERE datname<>'postgres' ORDER BY datname, usename;\"")
+ printf "$PROCESSES\n"
+}
+
+use() {
+ echo >&2 "about to connect to <${DB_NAME}> ..."
+ if [[ -z $1 ]]; then
+ lxc exec ct1 -- su - postgres -c "psql $DB_NAME"
+ else
+ local sql="psql -A -t $DB_NAME -c \\\"$1;\\\""
+ local command="su - postgres -c \"$sql\""
+ lxc exec ct1 -- sh -c "$command"
+ fi
+}
+
+create() {
+ echo >&2 "about to create to <${DB_NAME}> ..."
+ source /opt/debian-bash/lib/functions.sh
+ local DBs=($(list))
+ if ! $(containsElement DBs $DB_NAME); then
+ local SQL="CREATE USER \\\\\\\"$DB_NAME\\\\\\\" WITH PASSWORD '$DB_PASSWORD'"
+ local command="su - postgres sh -c \"psql -c \\\"$SQL\\\"\" && su - postgres sh -c \"createdb -O $DB_NAME $DB_NAME\" && echo CREATE DB"
+ # echo $command
+ lxc exec ct1 -- sh -c "$command"
+ else
+ echo $DB_NAME already exists!
+ fi
+
+}
+
+lookup() {
+ if [[ ${#TERM} -ge 4 ]]; then
+ echo >&2 "about to lookup term <${TERM}> over all tables of database <$DB_NAME> ..."
+ local command="pg_dump --data-only --inserts $DB_NAME 2>/dev/null | grep --color \"$TERM\""
+ lxc exec ct1 -- su - postgres -c "$command"
+ else
+ echo "term <$TERM> should contain 4 chars minimum!" && exit 2
+ fi
+}
+
+backup() {
+ if [[ ! -d "$FOLDER" ]]; then
+ echo "error: Folder required!"
+ file $FOLDER
+ exit 2
+ fi
+ DATE=$(date '+%F')
+ ARCHIVE="$FOLDER"/$DB_NAME-$DATE.postgres.gz
+
+ if [[ -f $ARCHIVE ]]; then
+ VERSION_CONTROL=numbered mv -b $ARCHIVE "$FOLDER"/$DB_NAME-$DATE-daily.postgres.gz
+ fi
+
+ echo "backup $DB_NAME $FOLDER"
+ PGPASSWORD=$DB_NAME pg_dump -U $DB_NAME $DB_NAME -h ct1.lxd | gzip >"$ARCHIVE"
+ echo "archive file created: $ARCHIVE"
+}
+
+restore() {
+ echo "restore $DB_NAME $FILE"
+ if [[ ! -f "$FILE" ]]; then
+ echo "error: Backup file (*.postgres.gz) required!"
+ file $FILE
+ exit 2
+ fi
+ PROCESSES=$(console "psql -c \"select pid as process_id, usename as username, datname as database_name, client_addr as client_address, application_name, backend_start, state, state_change from pg_stat_activity WHERE datname='$DB_NAME';\"")
+ PROCESS_COUNT=$(echo "$PROCESSES" | wc -l)
+ if [[ $PROCESS_COUNT -gt 3 ]]; then
+ echo "FAILURE: There are some connections to database, please consider stopping bound services"
+ echo
+ printf "$PROCESSES\n"
+ exit 2
+ fi
+
+ if [[ $YES == "true" || "yes" == $(confirm "RESTORATION will drop DATABASE, please acknowledge with care!!!") ]]; then
+ FOLDER="$HOME/RECOVERY_POSTGRES"
+ mkdir -p "$FOLDER"
+ backup
+ echo "backup successful, now drop and restore"
+ lxc exec ct1 -- su - postgres -c "dropdb $DB_NAME && createdb -O $DB_NAME $DB_NAME"
+ gunzip -c "$FILE" | grep -v "^CREATE DATABASE" | PGPASSWORD=$DB_NAME PGOPTIONS='--client-min-messages=warning' psql -X -q -1 -v ON_ERROR_STOP=1 --pset pager=off -U $DB_NAME -h ct1.lxd $DB_NAME 2>&1 >/dev/null
+ else
+ exit 1
+ fi
+}
+
+rename() {
+ echo "rename <$DB_NAME> to <$DB_NEW_NAME>"
+ mapfile -t LIST <<<"$(list)"
+ found=false
+ for db in "${LIST[@]}"; do
+ [[ "$db" == "$DB_NEW_NAME" ]] && echoerr "destination database <$DB_NEW_NAME> already exists! Please provide another name." && exit 11
+ [[ "$db" == "$DB_NAME" ]] && found=true
+ done
+ $found || (echoerr "source database <$DB_NAME> not found!" && exit 12)
+
+ console "psql -c \"ALTER DATABASE \\\"$DB_NAME\\\" RENAME TO \\\"$DB_NEW_NAME\\\" \""
+ console "psql -c \"ALTER USER \\\"$DB_NAME\\\" RENAME TO \\\"$DB_NEW_NAME\\\" \""
+ console "psql -c \"ALTER USER \\\"$DB_NEW_NAME\\\" PASSWORD '$DB_NEW_NAME' \""
+
+}
+
+# MAIN
+. "$MIAOU_BASEDIR/lib/init.sh"
+
+[[ $# -lt 1 ]] && synopsis && exit 1
+ACTION=$1
+
+case $ACTION in
+console)
+ shift
+ TAIL="$@"
+ console "$TAIL"
+ ;;
+list)
+ list
+ ;;
+connections)
+ connections
+ ;;
+use)
+ [[ $# -lt 2 ]] && synopsis && exit 1
+ DB_NAME=$2
+ shift 2
+ TAIL="$@"
+ use "$TAIL"
+ ;;
+create)
+ [[ $# -lt 2 ]] && synopsis && exit 1
+ DB_NAME=$2
+ DB_PASSWORD=${3:-$DB_NAME}
+ create
+ ;;
+lookup)
+ [[ $# -lt 3 ]] && synopsis && exit 1
+ DB_NAME=$2
+ TERM=$3
+ lookup
+ ;;
+backup)
+ [[ $# -lt 2 ]] && synopsis && exit 1
+ DB_NAME=$2
+ FOLDER=${3:-.}
+ backup
+ ;;
+restore)
+ [[ $# -lt 3 ]] && synopsis && exit 1
+ DB_NAME=$2
+ FILE=$3
+ YES=true
+ restore
+ ;;
+rename)
+ [[ $# -lt 3 ]] && synopsis && exit 1
+ DB_NAME=$2
+ DB_NEW_NAME=$3
+ rename
+ ;;
+*)
+ synopsis
+ exit 1
+ ;;
+esac
diff --git a/scripts/lxc-miaou-create b/scripts/lxc-miaou-create
new file mode 100755
index 0000000..3fbc227
--- /dev/null
+++ b/scripts/lxc-miaou-create
@@ -0,0 +1,180 @@
+#!/bin/bash
+
+function check_container_missing() {
+ if container_exists "$CONTAINER"; then
+ echoerr "$CONTAINER already created!"
+ exit 1
+ fi
+}
+
+function usage() {
+ echo 'USAGE with options:'
+ echo -e "\t\tlxc-miaou-create -o sameuser[,nesting,ssh]"
+}
+
+function check() {
+ check_container_missing || return 1
+ return 0
+}
+
+function set_options() {
+ declare -a options=("$@")
+ length=${#options[@]}
+ if [[ "$length" -ne 0 ]]; then
+ if [[ "$length" -ne 2 ]]; then
+ echoerr "unrecognized options: $@" && usage && exit 30
+ else
+ prefix="${options[0]}"
+ option="${options[1]}"
+ if [[ "$prefix" == '-o' ]]; then
+ IFS=',' read -r -a options <<<"$option"
+ for i in ${options[@]}; do
+ case "$i" in
+ sameuser) OPTION_SAMEUSER=true ;;
+ nesting) OPTION_NESTING=true ;;
+ ssh) OPTION_SSH=true ;;
+ *) echoerr "unrecognized options: $@" && usage && exit 32 ;;
+ esac
+ done
+ # echo "OPTION_SAMEUSER=$OPTION_SAMEUSER, OPTION_NESTING=$OPTION_NESTING, OPTION_SSH=$OPTION_SSH"
+ else
+ echoerr "unrecognized options prefix: $prefix" && usage && exit 31
+ fi
+ fi
+ shift
+ fi
+}
+
+function create() {
+ local PREFIX="miaou:create"
+
+ if [[ "$OPTION_SAMEUSER" == true ]]; then
+ miaou_user=$(whoami)
+ fi
+
+ echo -n "creating new container <$CONTAINER> based on image <$CONTAINER_RELEASE>... "
+ bridge_gw=$(lxc network get lxdbr0 ipv4.address | cut -d'/' -f1)
+ user_data="$(
+ cat <
+ Defaults secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/TOOLBOX"
+runcmd:
+ - [ systemctl, mask, systemd-hostnamed.service ]
+ - [ systemctl, disable, e2scrub_reap.service ]
+ - [ systemctl, disable, systemd-resolved.service, --now ]
+ - [ systemctl, reset-failed ]
+ - [ rm, /etc/resolv.conf]
+ - [ rm, /etc/sudoers.d/90-cloud-init-users]
+ - "echo nameserver $bridge_gw > /etc/resolv.conf"
+final_message: "Container from datasource \$datasource is finally up, after \$UPTIME seconds"
+EOF
+ )"
+ lxc init images:debian/$CONTAINER_RELEASE/cloud "$CONTAINER" --config user.user-data="$user_data" -q
+
+ # allow directory `SHARED` to be read-write mounted
+ lxc config set "$CONTAINER" raw.idmap "both $(id -u) 0" -q
+ mkdir -p "$HOME/LXD/SHARED/$CONTAINER"
+
+ lxc config device add "$CONTAINER" SHARED disk source="$HOME/LXD/SHARED/$CONTAINER" path=/mnt/SHARED -q
+ lxc config device add "$CONTAINER" TOOLBOX disk source=/TOOLBOX path=/TOOLBOX -q
+ lxc config device add "$CONTAINER" DEBIAN_BASH disk source=$(realpath /opt/debian-bash) path=/opt/debian-bash -q
+ lxc config set "$CONTAINER" environment.PATH /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/opt/debian-bash/tools:/TOOLBOX -q
+
+ if [[ "$OPTION_NESTING" == true ]]; then
+ lxc config set $CONTAINER security.nesting true -q
+ lxc config device add "$CONTAINER" miaou disk source=/opt/miaou path=/opt/miaou -q
+ fi
+
+ lxc start "$CONTAINER" -q
+
+ # initializing debian-bash
+ lxc exec "$CONTAINER" -- /opt/debian-bash/init.sh
+
+ # default configuration files (btm,)
+ lxc exec "$CONTAINER" -- mkdir -p /root/.config/bottom
+ lxc file push "$MIAOU_BASEDIR/templates/bottom/bottom.toml" "$CONTAINER/root/.config/bottom/bottom.toml" -q
+
+ # purge cloud-init after success
+ lxc exec "$CONTAINER" -- systemd-run -q -p After=cloud-final.service -p Type=oneshot --no-block bash -c '\
+ cloud-init status --wait &&\
+ cp /var/lib/cloud/data/status.json /root/cloud-status.json &&\
+ systemctl stop cloud-{config,final,init-local,init}.service &&\
+ systemctl disable cloud-{config,final,init-local,init}.service &&\
+ systemctl stop cloud-config.target cloud-init.target &&\
+ apt-get purge -y cloud-init &&\
+ rm -rf /var/lib/cloud && \
+ userdel -rf debian \
+ '
+
+ if [[ "$OPTION_SAMEUSER" == true ]]; then
+ if ! lxc exec "$CONTAINER" -- grep "$miaou_user" /etc/passwd; then
+ lxc exec "$CONTAINER" -- useradd -ms /bin/bash -G sudo "$miaou_user"
+ fi
+ if ! lxc exec "$CONTAINER" -- passwd -S "$miaou_user" | cut -d ' ' -f2 | grep -q ^P; then
+ shadow_passwd=$(load_yaml_from_expanded credential.shadow)
+ shadow_remainder=$(lxc exec "$CONTAINER" -- bash -c "grep $miaou_user /etc/shadow | cut -d':' -f3-")
+ lxc exec "$CONTAINER" -- /opt/debian-bash/tools/append_or_replace "^$miaou_user:.*:" "$miaou_user:$shadow_passwd:$shadow_remainder" /etc/shadow >/dev/null
+ fi
+ fi
+
+ if [[ "$OPTION_SSH" == true ]]; then
+ lxc exec "$CONTAINER" -- /opt/debian-bash/tools/idem_apt_install openssh-server
+ fi
+
+ if [[ "$OPTION_SSH" == true && "$OPTION_SAMEUSER" == true ]]; then
+ lxc-miaou-enable-ssh "$CONTAINER"
+ fi
+
+ PREFIX="" echoinfo OK
+
+ echo "hint: \`lxc login $CONTAINER [--env user=]\`"
+ [[ "$OPTION_SAMEUSER" == true ]] && echo "hint: \`lxc sameuser $CONTAINER\`"
+
+ true
+}
+
+## MAIN
+. "$MIAOU_BASEDIR/lib/init.sh"
+OPTION_SAMEUSER=false
+OPTION_NESTING=false
+OPTION_SSH=false
+PREFIX="miaou"
+
+arg1_required "$@" || (usage && exit 1)
+readonly CONTAINER=$1
+readonly CONTAINER_RELEASE="bookworm"
+
+shift
+set_options "$@"
+readonly FULL_OPTIONS="$@"
+
+check
+create
diff --git a/scripts/lxc-miaou-enable-ssh b/scripts/lxc-miaou-enable-ssh
new file mode 100755
index 0000000..ec8db1b
--- /dev/null
+++ b/scripts/lxc-miaou-enable-ssh
@@ -0,0 +1,88 @@
+#!/bin/bash
+
+function check_container_exists() {
+ if ! container_exists "$CONTAINER"; then
+ echoerr "container <$CONTAINER> does not exist!"
+ exit 1
+ fi
+}
+
+function check() {
+ check_container_exists || return 1
+ return 0
+}
+
+function enable_ssh() {
+ echo "lxc: enable ssh in container <$CONTAINER> for user <$SSH_USER>"
+
+ if ! container_running "$CONTAINER"; then
+ echowarn "container <$CONTAINER> seems to be asleep, starting ..."
+ lxc start "$CONTAINER"
+ echowarn DONE
+ fi
+
+ lxc exec "$CONTAINER" -- bash </dev/null; then
+ echo "adding new user <$SSH_USER>"
+ useradd -ms /bin/bash -G sudo "$SSH_USER"
+ else
+ echo "bash: $SSH_USER exists already!"
+ fi
+EOF
+
+ miaou_user=$(whoami)
+ shadow_passwd=$(load_yaml_from_expanded credential.shadow)
+ shadow_remainder=$(lxc exec "$CONTAINER" -- bash -c "grep $SSH_USER /etc/shadow | cut -d':' -f3-")
+ lxc exec "$CONTAINER" -- /opt/debian-bash/tools/append_or_replace "^$SSH_USER:.*:" "$SSH_USER:$shadow_passwd:$shadow_remainder" /etc/shadow >/dev/null
+
+ lxc exec "$CONTAINER" -- /opt/debian-bash/tools/idem_apt_install openssh-server
+ previous_users=($(
+ lxc exec "$CONTAINER" -- bash </dev/null
+ echo 'OK'
+ echo -n "copying sshd_config over container <$CONTAINER> ... "
+ lxc file push --uid 0 --gid 0 "/tmp/sshd_config" "$CONTAINER/etc/ssh/sshd_config" &>/dev/null
+ echo 'OK'
+ lxc exec "$CONTAINER" -- systemctl reload sshd.service
+ fi
+
+ lxc exec "$CONTAINER" -- mkdir -p "/home/$SSH_USER/.ssh"
+ lxc exec "$CONTAINER" -- chown "$SSH_USER:$SSH_USER" "/home/$SSH_USER/.ssh"
+ lxc exec "$CONTAINER" -- chmod 760 "/home/$SSH_USER/.ssh"
+ lxc file push --uid 0 --gid 0 "/home/$miaou_user/.ssh/id_rsa.pub" "$CONTAINER/home/$SSH_USER/.ssh/authorized_keys" &>/dev/null
+ lxc exec "$CONTAINER" -- chown "$SSH_USER:$SSH_USER" "/home/$SSH_USER/.ssh/authorized_keys"
+ lxc exec "$CONTAINER" -- chmod 600 "/home/$SSH_USER/.ssh/authorized_keys"
+
+ echo "create symbolic link for curl from TOOLBOX as required for Codium remote-ssh"
+ lxc exec "$CONTAINER" -- ln -sf /TOOLBOX/curl /usr/bin/
+
+ echo "SUCCESS: container $CONTAINER listening on port 22"
+}
+
+## MAIN
+. "$MIAOU_BASEDIR/lib/init.sh"
+
+arg1_required "$@"
+readonly CONTAINER=$1
+if [[ -z "${2:-}" ]]; then
+ readonly SSH_USER=$(id -un)
+else
+ readonly SSH_USER="$2"
+fi
+
+check
+enable_ssh
diff --git a/scripts/lxc-sort-by-disk b/scripts/lxc-sort-by-disk
new file mode 100755
index 0000000..4c32f18
--- /dev/null
+++ b/scripts/lxc-sort-by-disk
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+lxc list -c nDm -f compact status=running | tail -n+2 | sort -k2 -h -r
diff --git a/scripts/lxc-sort-by-mem b/scripts/lxc-sort-by-mem
new file mode 100755
index 0000000..0837c80
--- /dev/null
+++ b/scripts/lxc-sort-by-mem
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+lxc list -c nmD -f compact status=running | tail -n+2 | sort -k2 -h -r
diff --git a/scripts/lxd-restart-dnsmasq b/scripts/lxd-restart-dnsmasq
new file mode 100755
index 0000000..a1e022b
--- /dev/null
+++ b/scripts/lxd-restart-dnsmasq
@@ -0,0 +1,12 @@
+#!/bin/bash
+
+function restart_dnsmasq() {
+ echo -n "lxd: restart dnsmasq... "
+ lxc network get lxdbr0 raw.dnsmasq >/tmp/dnsmaq.conf
+ lxc network set lxdbr0 raw.dnsmasq - '
+ exit 0
+}
+
+yqm() {
+ #read only
+ yq "$1" "$EXPANDED_CONF"
+}
+
+yqmi() {
+ # for update
+ yq "$1" "$EXPANDED_CONF" -i
+}
+
+yqmt() {
+ # tabular
+ yq "$1" "$EXPANDED_CONF" -o t
+}
+
+compute_fqdn_middlepart() {
+ case "$1" in
+ prod)
+ local fqdn_middlepart="."
+ ;;
+ beta)
+ local fqdn_middlepart=".beta."
+ ;;
+ dev)
+ local fqdn_middlepart=".dev."
+ ;;
+ *)
+ echowarn "unknown target <${target}>, please fix with correct value from {prod, beta, dev} and try again..."
+ exit 1
+ ;;
+ esac
+ builtin echo "$fqdn_middlepart"
+}
+
+# archive_conf(FILE)
+# save patch in archived folder of current file
+function archive_conf() {
+ PREFIX="miaou:conf:archive"
+
+ file="$1"
+ filename=$(basename "$file")
+ mkdir -p "$MIAOU_CONFIGDIR/archived/$filename"
+ previous="$MIAOU_CONFIGDIR/archived/$filename/previous"
+
+ # shellcheck disable=SC2012
+ latest_patch=$(ls -1tr "$MIAOU_CONFIGDIR/archived/$filename/" | tail -n1)
+
+ if [[ -z "$latest_patch" ]]; then
+ echo -n "archiving first file <$file> ..."
+ cp "$file" "$previous"
+ PREFIX="" echoinfo OK
+ elif [[ "$file" -nt "$latest_patch" ]]; then
+ patchname="$MIAOU_CONFIGDIR/archived/$filename/$(date +%F_%T)"
+ if ! diff "$previous" "$file" >"$patchname"; then
+ echo -n "archiving patch <$patchname> ..."
+ cp "$file" "$previous"
+ PREFIX="" echoinfo OK
+ else
+ rm "$patchname"
+ fi
+ fi
+}
+
+function archive_allconf() {
+ mkdir -p "$MIAOU_CONFIGDIR"
+ archive_conf "$CONF"
+ archive_conf "$DEFAULTS"
+}
+
+function check_expand_conf() {
+ PREFIX="miaou:conf:check"
+ if ! "$FORCE" && [ -f "$EXPANDED_CONF" ] && [ "$EXPANDED_CONF" -nt "$CONF" ] && [ "$EXPANDED_CONF" -nt "$DEFAULTS" ]; then
+ echo "already expanded!"
+ return 1
+ fi
+}
+
+function expand_conf() {
+ PREFIX="miaou:conf"
+
+ if [[ -f "$EXPANDED_CONF" ]]; then
+ current_target=$(grep -Es "^target:" /etc/miaou/defaults.yaml | cut -d ' ' -f2)
+ previous_target=$(grep -Es "^target:" "$EXPANDED_CONF" | cut -d ' ' -f2)
+ [[ "$current_target" != "$previous_target" ]] && echoerr "TARGET <$previous_target> mismatched <$current_target>" && exit 101
+ fi
+
+ # initialize expanded conf by merging default
+ # shellcheck disable=SC2016
+ yq eval-all '. as $item ireduce ({}; . * $item )' "$CONF" "$DEFAULTS" >"$EXPANDED_CONF"
+
+ # append unique container unless overridden
+ mapfile -t services_app_only < <(yqmt '.services.[].[] | has("container") | select ( . == false) | [(parent|key)+" " +key]')
+
+ for i in "${services_app_only[@]}"; do
+ read -r -a item <<<"$i"
+ domain=${item[0]}
+ subdomain=${item[1]}
+ app=$(yqm ".services.\"$domain\".\"$subdomain\".app")
+ container=$(get_container_for_domain_subdomain_app "$domain" "$subdomain" "$app")
+ yqmi ".services.\"$domain\".\"$subdomain\".container=\"$container\""
+ done
+
+ # append enabled=true unless overridden
+ mapfile -t services_app_only < <(yqmt '.services.[].[] | has("enabled") | select ( . == false) | [(parent|key)+" " +key] | unique ')
+ # echo "found <${#services_app_only[@]}> enabled services"
+ for i in "${services_app_only[@]}"; do
+ read -r -a item <<<"$i"
+ domain=${item[0]}
+ subdomain=${item[1]}
+ yqmi ".services.\"$domain\".\"$subdomain\".enabled=true"
+ done
+
+ # compute fqdn
+ target=$(yqm '.target')
+ fqdn_middlepart=$(compute_fqdn_middlepart "$target")
+
+ # write fqdn_middlepart
+ yqmi ".expanded.fqdn_middlepart = \"$fqdn_middlepart\""
+
+ # add monitored.containers section
+ yqmi '.expanded.monitored.containers = ([ .services[] | to_entries | .[] | .value | select (.enabled == true ) | .container ] | unique)'
+
+ # add monitored.hosts section
+ yqmi '.expanded.monitored.hosts = [( .services[][] | select (.enabled == true ) | {"domain": ( parent | key ), "subdomain": key, "fqdn": key + (parent | parent | parent | .expanded.fqdn_middlepart) + ( parent | key ), "container":.container, "port":.port, "app":.app })]'
+
+ # add services section
+ if [[ ${#services_app_only[@]} -gt 0 ]]; then
+ yqmi '.expanded.services = [( .services[][] | select (.enabled == true ) | {"domain": ( parent | key ), "subdomain": key, "fqdn": key + (parent | parent | parent | .expanded.fqdn_middlepart) + ( parent | key ), "container":.container, "port":.port, "app":.app, "name": .name // ""})]'
+ else
+ yqmi '.expanded.services = []'
+ fi
+
+ # add firewall section, bridge_subnet + mail_passthrough if any
+ bridge_subnet=$(lxc network get lxdbr0 ipv4.address)
+ yqmi ".firewall.bridge_subnet = \"$bridge_subnet\""
+
+ container_mail_passthrough=$(yqm ".firewall.container_mail_passthrough")
+}
+
+function build_routes() {
+ PREFIX="miaou:routes"
+
+ mapfile -t fqdns < <(yqm '.expanded.services[].fqdn')
+ echo "found <${#fqdns[@]}> fqdn"
+ raw_dnsmasq=''
+ for i in "${fqdns[@]}"; do
+ raw_dnsmasq+="address=/$i/$DMZ_IP\\n"
+ # append domains to conf
+ echo "re-routing any connection from <$i> to internal container <$DMZ_CONTAINER.lxd>"
+ done
+
+ builtin echo -e "$raw_dnsmasq" | lxc network set $BRIDGE raw.dnsmasq -
+}
+
+function build_dmz_reverseproxy() {
+ PREFIX="miaou:build:dmz"
+ echo -n "building configuration for nginx ... "
+ mkdir -p "$MIAOU_CONFIGDIR/nginx"
+ tera -t "$MIAOU_BASEDIR/templates/nginx/_default.j2" "$EXPANDED_CONF" -o "$MIAOU_CONFIGDIR/nginx/_default" &>/dev/null
+
+ tera -t "$MIAOU_BASEDIR/templates/nginx/hosts.j2" "$EXPANDED_CONF" -o "$MIAOU_CONFIGDIR/nginx/hosts" &>/dev/null
+ PREFIX="" echo OK
+
+ echo -n "pushing configuration to <$DMZ_CONTAINER> ... "
+ for f in "$MIAOU_CONFIGDIR"/nginx/*; do
+ lxc file push --uid=0 --gid=0 "$f" "$DMZ_CONTAINER/etc/nginx/sites-available/" &>/dev/null
+ done
+ PREFIX="" echo OK
+
+ cat <"
+ echoerr "please review configuration for fqdn: $fqdn"
+ exit 2
+ fi
+
+ if ! curl_check_unsecure "https://$fqdn"; then
+ echoerr
+ echoerr "DMZ does not seem to dispatch please review DMZ Nginx proxy"
+ exit 3
+ elif [[ "$target" != 'dev' ]] && ! curl_check "https://$fqdn"; then
+ PREFIX="" echo
+ echowarn "T=$target missing valid certificate for fqdn please review DMZ certbot"
+ fi
+
+ done
+ PREFIX="" echo OK
+
+ # templates for monit
+ echo -n "copying templates for monit ..."
+ mkdir -p "$MIAOU_CONFIGDIR/monit"
+ tera -t "$MIAOU_BASEDIR/templates/monit/containers.j2" "$EXPANDED_CONF" -o "$MIAOU_CONFIGDIR/monit/containers" >/dev/null
+ tera -t "$MIAOU_BASEDIR/templates/monit/hosts.j2" "$EXPANDED_CONF" -o "$MIAOU_CONFIGDIR/monit/hosts" >/dev/null
+ PREFIX="" echo OK
+}
+
+# count_service_for_container(container: string)
+# returns how many services run inside container according to expanded conf
+function count_service_for_container() {
+ container_mail_passthrough="$1"
+ count=$(yqm ".expanded.services.[] | select(.container == \"$container_mail_passthrough\") | .fqdn" | wc -l)
+ builtin echo "$count"
+}
+
+function build_nftables() {
+ PREFIX="miaou:nftables:build"
+ mkdir -p "$MIAOU_CONFIGDIR/nftables.rules.d"
+
+ container_mail_passthrough=$(yqm '.firewall.container_mail_passthrough')
+ if [[ "$container_mail_passthrough" != null ]]; then
+ ip_mail_passthrough=$(lxc list "$container_mail_passthrough" -c4 -f csv | grep eth0 | cut -d ' ' -f1)
+ [[ -z "$ip_mail_passthrough" ]] && echoerr "container <$container_mail_passthrough> passthrough unknown ip!" && exit 55
+ echo "passthrough=$container_mail_passthrough/$ip_mail_passthrough"
+
+ count=$(count_service_for_container "$container_mail_passthrough")
+ [[ $count == 0 ]] && echowarn "no service detected => no passthrough, no change!"
+ [[ $count -gt 1 ]] && echoerr "count <$count> services detected on container <$container_mail_passthrough>, please disable some and leave only one service for safety!!!" && exit 56
+
+ ip_mail_passthrough=$ip_mail_passthrough tera -e --env-key env -t "$MIAOU_BASEDIR/templates/nftables/lxd.table.j2" "$EXPANDED_CONF" -o "$MIAOU_CONFIGDIR/nftables.rules.d/lxd.table" &>/dev/null
+ else
+ echo "no container passthrough"
+ tera -t "$MIAOU_BASEDIR/templates/nftables/lxd.table.j2" "$EXPANDED_CONF" -o "$MIAOU_CONFIGDIR/nftables.rules.d/lxd.table" &>/dev/null
+ fi
+
+ if ! diff -q "$MIAOU_CONFIGDIR/nftables.rules.d/lxd.table" /etc/nftables.rules.d/lxd.table; then
+ sudo_required "reloading nftables"
+ echo -n "reloading nftables..."
+ sudo cp "$MIAOU_CONFIGDIR/nftables.rules.d/lxd.table" /etc/nftables.rules.d/lxd.table
+ sudo systemctl reload nftables
+ PREFIX="" echo OK
+ fi
+}
+
+# check whether http server responds 200 OK, required , ie: http://example.com:8001, https://example.com
+function curl_check() {
+ arg1_required "$@"
+ # echo "curl $1"
+ curl -m $MAX_WAIT -sLI4 "$1" | grep -q "^HTTP.* 200"
+}
+
+# check whether https server responds 200 OK, even unsecured certificate (auto-signed in mode DEV)
+function curl_check_unsecure() {
+ arg1_required "$@"
+ curl -m $MAX_WAIT -skLI4 "$1" | grep -q "^HTTP.* 200"
+}
+
+function get_dmz_ip() {
+ if ! container_running "$DMZ_CONTAINER"; then
+ echowarn "Container running dmz <$DMZ_CONTAINER> seems down"
+ echoerr "please \`lxc start $DMZ_CONTAINER\` or initialize first!"
+ exit 1
+ fi
+
+ dmz_ip=$(host "$DMZ_CONTAINER.lxd" | cut -d ' ' -f4)
+ if ! valid_ipv4 "$dmz_ip"; then
+ echowarn "dmz seems up but no valid ip <$dmz_ip> found!"
+ echoerr "please fix this networking issue, then retry..."
+ exit 1
+ else
+ builtin echo "$dmz_ip"
+ fi
+}
+
+function fetch_container_of_type() {
+ local type="$1"
+ readarray -t dmzs < <(yqm ".containers.[].[] | select(.==\"$type\") | parent | key")
+ case ${#dmzs[@]} in
+ 0) : ;;
+ 1) builtin echo "${dmzs[0]}" ;;
+ *) for d in "${dmzs[@]}"; do
+ builtin echo "$d"
+ done ;;
+ esac
+}
+
+function get_container_for_domain_subdomain_app() {
+ local domain="$1"
+ local subdomain="$2"
+ local app="$3"
+ readarray -t containers < <(fetch_container_of_type "$app")
+ case ${#containers[@]} in
+ 0) echoerr "no container of type <$app> found amongst containers for $subdomain.$domain\nHINT : Please, either :\n1. define at least one container for recipe <$app>\n2. remove all services related to recipe <$app>" && exit 1 ;;
+ 1) builtin echo "${containers[0]}" ;;
+ *)
+ for d in "${containers[@]}"; do
+ echowarn "container of type $app found in <$d>"
+ done
+ echoerr "multiple containers (${#containers[@]}) provided same app <$app>, therefore container is mandatory alongside $subdomain.$domain" && exit 2
+ ;;
+ esac
+}
+
+function get_unique_container_dmz() {
+ readarray -t containers < <(fetch_container_of_type "dmz")
+ case ${#containers[@]} in
+ 0) echoerr "no container of type found amongst containers" && exit 1 ;;
+ 1) builtin echo "${containers[0]}" ;;
+
+ *)
+ for d in "${containers[@]}"; do
+ echowarn "container of type dmz found in <$d>"
+ done
+ echoerr "multiple dmz (${#containers[@]}) are not allowed, please select only one " && exit 2
+ ;;
+ esac
+}
+
+function prepare_dmz_container() {
+ "$MIAOU_BASEDIR"/recipes/dmz/install.sh "$DMZ_CONTAINER"
+}
+
+function check_resolv_conf() {
+ local bridge_gw resolver
+ bridge_gw=$(lxc network get lxdbr0 ipv4.address | cut -d'/' -f1)
+ resolver=$(grep nameserver /etc/resolv.conf | head -n1 | cut -d ' ' -f2)
+
+ PREFIX="resolver:check" echo "container resolver is <$resolver>"
+ PREFIX="resolver:check" echo "container bridge is <$bridge_gw>"
+ [[ "$bridge_gw" != "$resolver" ]] && return 21
+ return 0
+}
+
+function prepare_containers() {
+ PREFIX="miaou:prepare"
+ readarray -t containers < <(yqmt ".containers.[] | [ key, .[] ] ")
+ for i in "${containers[@]}"; do
+ read -r -a item <<<"$i"
+ container=${item[0]}
+ for ((j = 1; j < ${#item[@]}; j++)); do
+ service="${item[$j]}"
+ recipe_install="$MIAOU_BASEDIR/recipes/$service/install.sh"
+ if [[ -f "$recipe_install" ]]; then
+ echo "install [$service] onto container <$container>"
+ "$recipe_install" "$container"
+ else
+ echoerr "FAILURE, for container <$container>, install recipe [$service] not found!"
+ echoerr "please review configuration, mismatch recipe name maybe?"
+ exit 50
+ fi
+ done
+ done
+}
+
+function build_services() {
+ PREFIX="miaou:build:services"
+ echo "building services..."
+ readarray -t services < <(yqmt '.expanded.services[] | [ .[] ]')
+ for i in "${services[@]}"; do
+
+ read -r -a item <<<"$i"
+ fqdn=${item[2]}
+ container=${item[3]}
+ port=${item[4]}
+ app=${item[5]}
+ name=${item[6]:-}
+
+ recipe="$MIAOU_BASEDIR/recipes/$app/crud.sh"
+ if [[ -f "$recipe" ]]; then
+ echo "read [$app:$name] onto container <$container>"
+ if ! "$recipe" -r --port "$port" --container "$container" --name "$name" --fqdn "$fqdn"; then
+ echoinfo "CREATE RECIPE"
+ "$recipe" -c --port "$port" --container "$container" --name "$name" --fqdn "$fqdn"
+ echoinfo "CREATE RECIPE: OK"
+ fi
+ else
+ echowarn "for container <$container>, crud recipe [$app] not found!"
+ fi
+ done
+}
+
+### MAIN
+
+. "$MIAOU_BASEDIR/lib/init.sh"
+
+readonly CONF="/etc/miaou/miaou.yaml"
+readonly DEFAULTS="/etc/miaou/defaults.yaml"
+readonly EXPANDED_CONF="$MIAOU_CONFIGDIR/miaou.expanded.yaml"
+readonly BRIDGE="lxdbr0"
+readonly MAX_WAIT=3 # timeout in seconds
+
+# shellcheck disable=SC2034
+declare -a options=("$@")
+
+FORCE=false
+if containsElement options "-f" || containsElement options "--force"; then
+ FORCE=true
+fi
+
+if containsElement options "history"; then
+ echo "TODO: HISTORY"
+ exit 0
+fi
+
+if containsElement options "config"; then
+ editor /etc/miaou/miaou.yaml
+ if diff -q /etc/miaou/miaou.yaml $HOME/.config/miaou/archived/miaou.yaml/previous; then
+ exit 0
+ fi
+fi
+
+if check_expand_conf; then
+ archive_allconf
+ expand_conf
+ check_resolv_conf
+ build_nftables
+ prepare_containers
+
+ DMZ_CONTAINER=$(get_unique_container_dmz)
+ readonly DMZ_CONTAINER
+ build_services
+
+ DMZ_IP=$(get_dmz_ip)
+ readonly DMZ_IP
+ build_dmz_reverseproxy
+
+ build_routes
+ build_monit
+
+fi
+monit_show
diff --git a/scripts/ssl_check b/scripts/ssl_check
new file mode 100755
index 0000000..7f99df9
--- /dev/null
+++ b/scripts/ssl_check
@@ -0,0 +1,80 @@
+#!/bin/bash
+readonly DOMAIN=$1
+readonly PROTOCOL=${2:-https}
+readonly TIMEOUT=10 # max seconds to wait
+
+result=0
+
+function usage {
+ echo 'usage: [ https | 443 | smtps | 587 | pop3 | 993 | imap | 995 | ALL ]'
+ exit -1
+}
+
+function check_ssl {
+ local protocol=$1
+ case $protocol in
+ SMTPS )
+ local extra="-starttls smtp -showcerts"
+ ;;
+ esac
+
+ echo -n "$protocol "
+
+
+ certificate_info=$(echo | timeout $TIMEOUT openssl s_client $extra -connect $DOMAIN:$2 2>/dev/null)
+
+ issuer=$(echo "$certificate_info" | openssl x509 -noout -text 2>/dev/null | grep Issuer: | cut -d: -f2)
+ date=$( echo "$certificate_info" | openssl x509 -noout -enddate 2>/dev/null | cut -d'=' -f2)
+ date_s=$(date -d "${date}" +%s)
+ now_s=$(date -d now +%s)
+ date_diff=$(( (date_s - now_s) / 86400 ))
+
+ if [[ -z $date ]]; then
+ echo -n "does not respond "
+ echo -ne "\033[31;1m"
+ echo FAILURE
+ (( result += 1 ))
+ elif [[ $date_diff -gt 20 ]]; then
+ echo -n "issuer:$issuer "
+ echo -n "will expire in $date_diff days "
+ echo -ne "\033[32;1m"
+ echo ok
+ elif [[ $date_diff -gt 0 ]];then
+ echo -n "issuer:$issuer "
+ echo -n "will expire in $date_diff days "
+ echo -ne "\033[31;1m"
+ echo WARNING
+ (( result += 1 ))
+ else
+ echo -n "issuer:$issuer "
+ echo -n "has already expired $date_diff ago "
+ echo -ne "\033[31;1m"
+ echo FAILURE
+ (( result += 1 ))
+ fi
+ echo -ne "\033[0m"
+}
+
+#MAIN
+[[ -z "$DOMAIN" ]] && usage
+case $PROTOCOL in
+ https | 443 )
+ check_ssl HTTPS 443;;
+ smtps | 587 )
+ check_ssl SMTPS 587;;
+ pop3 | 995 )
+ check_ssl POP3 995;;
+ imap | 993 )
+ check_ssl IMAP 993;;
+ all | ALL )
+ check_ssl HTTPS 443
+ check_ssl SMTPS 587
+ check_ssl POP3 995
+ check_ssl IMAP 993
+ ;;
+ *)
+ usage
+ ;;
+esac
+
+exit "$result"
diff --git a/templates/apps/cagettepei/cagettepei-batch b/templates/apps/cagettepei/cagettepei-batch
new file mode 100644
index 0000000..c45d78b
--- /dev/null
+++ b/templates/apps/cagettepei/cagettepei-batch
@@ -0,0 +1,18 @@
+#!/bin/bash
+
+case $1 in
+minute) ;;
+daily) ;;
+*) echo "expected [minute|daily]" && exit 1 ;;
+esac
+
+SELECTOR=$1
+
+for i in /var/www/cagettepei/*; do
+ if [[ -d $i ]]; then
+ cd "$i/www" || echo "Folder not found: $i/www"
+ echo "cron-$SELECTOR in: $i"
+ neko index.n cron/$SELECTOR
+ echo
+ fi
+done
diff --git a/templates/apps/cagettepei/cagettepei-host.j2 b/templates/apps/cagettepei/cagettepei-host.j2
new file mode 100644
index 0000000..1c907f0
--- /dev/null
+++ b/templates/apps/cagettepei/cagettepei-host.j2
@@ -0,0 +1,8 @@
+Listen {{ env.APP_PORT }}
+
+ DirectoryIndex index.n
+ DocumentRoot /var/www/cagettepei/{{env.APP_NAME}}/www/
+
+ ErrorLog ${APACHE_LOG_DIR}/cagettepei/{{env.APP_NAME}}/debug.log
+ ErrorLogFormat "[%{uc}t] %M"
+
\ No newline at end of file
diff --git a/templates/apps/cagettepei/systemd/cagettepei-batch-day.service b/templates/apps/cagettepei/systemd/cagettepei-batch-day.service
new file mode 100644
index 0000000..6906d0a
--- /dev/null
+++ b/templates/apps/cagettepei/systemd/cagettepei-batch-day.service
@@ -0,0 +1,10 @@
+[Unit]
+Description=Run batch cagettepei every day
+
+[Service]
+User=www-data
+SyslogIdentifier=cagettepei
+ExecStart=/var/www/cagettepei/cagettepei-batch daily
+
+[Install]
+WantedBy=multi-user.target
diff --git a/templates/apps/cagettepei/systemd/cagettepei-batch-day.timer b/templates/apps/cagettepei/systemd/cagettepei-batch-day.timer
new file mode 100644
index 0000000..8af88b6
--- /dev/null
+++ b/templates/apps/cagettepei/systemd/cagettepei-batch-day.timer
@@ -0,0 +1,10 @@
+[Unit]
+Description=Timer for batch cagettepei every day
+Requires=apache2.service
+
+[Timer]
+OnCalendar=daily
+Unit=cagettepei-batch-day.service
+
+[Install]
+WantedBy=timers.target
diff --git a/templates/apps/cagettepei/systemd/cagettepei-batch-minute.service b/templates/apps/cagettepei/systemd/cagettepei-batch-minute.service
new file mode 100644
index 0000000..501536a
--- /dev/null
+++ b/templates/apps/cagettepei/systemd/cagettepei-batch-minute.service
@@ -0,0 +1,10 @@
+[Unit]
+Description=Run batch cagettepei every minute
+
+[Service]
+User=www-data
+SyslogIdentifier=cagettepei
+ExecStart=/var/www/cagettepei/cagettepei-batch minute
+
+[Install]
+WantedBy=multi-user.target
diff --git a/templates/apps/cagettepei/systemd/cagettepei-batch-minute.timer b/templates/apps/cagettepei/systemd/cagettepei-batch-minute.timer
new file mode 100644
index 0000000..c6460c9
--- /dev/null
+++ b/templates/apps/cagettepei/systemd/cagettepei-batch-minute.timer
@@ -0,0 +1,10 @@
+[Unit]
+Description=Timer for batch cagettepei every minute
+Requires=apache2.service
+
+[Timer]
+OnCalendar=minutely
+Unit=cagettepei-batch-minute.service
+
+[Install]
+WantedBy=timers.target
\ No newline at end of file
diff --git a/templates/apps/dolibarr/host.j2 b/templates/apps/dolibarr/host.j2
new file mode 100644
index 0000000..764152c
--- /dev/null
+++ b/templates/apps/dolibarr/host.j2
@@ -0,0 +1,23 @@
+server {
+listen {{ APP_PORT }} default_server;
+
+root /var/www/{{APP_NAME}}/htdocs; # Check this
+error_log /var/log/nginx/{{APP_NAME}}/error.log;
+
+index index.php index.html index.htm;
+charset utf-8;
+
+location / {
+try_files $uri $uri/ /index.php;
+}
+
+location ~ [^/]\.php(/|$) {
+client_max_body_size 50M;
+try_files $uri =404;
+fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
+fastcgi_read_timeout 600;
+include fastcgi_params;
+fastcgi_pass unix:/var/run/php/php{{PHP_VERSION}}-fpm.sock;
+}
+
+}
\ No newline at end of file
diff --git a/templates/apps/odoo12/favicon/favicon-beta.ico b/templates/apps/odoo12/favicon/favicon-beta.ico
new file mode 100644
index 0000000..e76b308
Binary files /dev/null and b/templates/apps/odoo12/favicon/favicon-beta.ico differ
diff --git a/templates/apps/odoo12/favicon/favicon-dev.ico b/templates/apps/odoo12/favicon/favicon-dev.ico
new file mode 100644
index 0000000..8816338
Binary files /dev/null and b/templates/apps/odoo12/favicon/favicon-dev.ico differ
diff --git a/templates/apps/odoo12/favicon/favicon-prod.ico b/templates/apps/odoo12/favicon/favicon-prod.ico
new file mode 100644
index 0000000..4abb409
Binary files /dev/null and b/templates/apps/odoo12/favicon/favicon-prod.ico differ
diff --git a/templates/apps/odoo12/odoo.conf.j2 b/templates/apps/odoo12/odoo.conf.j2
new file mode 100644
index 0000000..d7099a2
--- /dev/null
+++ b/templates/apps/odoo12/odoo.conf.j2
@@ -0,0 +1,17 @@
+[options]
+data_dir = /home/odoo/data-{{ APP_NAME }}
+
+xmlrpc_port = {{ APP_PORT }}
+longpolling_port = {{ LONG_PORT }}
+
+db_host = ct1.lxd
+db_name = odoo12-{{ APP_NAME }}
+db_user = odoo12-{{ APP_NAME }}
+db_password = odoo12-{{ APP_NAME }}
+list_db = {{ target != 'prod'}}
+
+workers = 2
+db_maxconn = 10
+db_filter = .*
+syslog = True
+proxy_mode = True
\ No newline at end of file
diff --git a/templates/apps/odoo12/odoo.service.j2 b/templates/apps/odoo12/odoo.service.j2
new file mode 100644
index 0000000..d73245f
--- /dev/null
+++ b/templates/apps/odoo12/odoo.service.j2
@@ -0,0 +1,14 @@
+[Unit]
+Description=Odoo12 {{ APP_NAME }}
+After=network.target
+
+[Service]
+Type=simple
+SyslogIdentifier=odoo12-{{ APP_NAME }}
+PermissionsStartOnly=true
+User=odoo
+Group=odoo
+ExecStart=/home/odoo/venv/bin/python3 /home/odoo/odoo12/odoo-bin -c /etc/odoo12/{{ APP_NAME }}.conf
+
+[Install]
+WantedBy=multi-user.target
\ No newline at end of file
diff --git a/templates/apps/odoo12/odoo12-addon-install b/templates/apps/odoo12/odoo12-addon-install
new file mode 100644
index 0000000..926a0f5
--- /dev/null
+++ b/templates/apps/odoo12/odoo12-addon-install
@@ -0,0 +1,31 @@
+#!/bin/bash
+
+CLIENT=$1
+ADDON=$2
+
+function usage() {
+ echo 'usage: '
+ exit 1
+}
+
+# VERIFICATION
+
+[[ -z "$CLIENT" || -z "$ADDON" ]] && usage
+[[ ! -d "/home/odoo/data-${CLIENT}" ]] && echo "unknown CLIENT <${CLIENT}>, should exist in folder /home/odoo/data-..." && exit 2
+
+URL="https://pypi.org/project/odoo12-addon-${ADDON}/"
+curl --output /dev/null --silent --head --fail "${URL}"
+[[ $? -ne 0 ]] && echo "unknown ADDON <${ADDON}>, should be downloadable from: ${URL}" && exit 3
+
+[[ -d "/home/odoo/data-${CLIENT}/addons/12.0/${ADDON}" ]] && echo "ADDON <${ADDON}> already exists, consider removing manually!" && exit 4
+
+# ACTION
+
+package=$(curl -Ls ${URL} | rg '' -r '$1')
+wget $package -O /tmp/package.zip
+rm /tmp/ADDON -rf && mkdir /tmp/ADDON
+unzip /tmp/package.zip 'odoo/addons/*' -d /tmp/ADDON/
+chown -R odoo:odoo /tmp/ADDON/
+mv /tmp/ADDON/odoo/addons/* /home/odoo/data-${CLIENT}/addons/12.0/
+
+echo "FORCE RELOADING ADDONS with: ./web?debug#menu_id=48&action=36"
diff --git a/templates/apps/odoo15/odoo-addon-install b/templates/apps/odoo15/odoo-addon-install
new file mode 100644
index 0000000..e2dcd19
--- /dev/null
+++ b/templates/apps/odoo15/odoo-addon-install
@@ -0,0 +1,33 @@
+#!/bin/bash
+
+CLIENT=$1
+ADDON=$2
+
+function usage() {
+ echo 'usage: '
+ exit 100
+}
+
+# VERIFICATION
+
+[[ -z "$CLIENT" || -z "$ADDON" ]] && usage
+[[ ! -d "/home/odoo/data-${CLIENT}" ]] && echo "unknown CLIENT <${CLIENT}>, should exist in folder /home/odoo/data-..." && exit 2
+
+URL="https://pypi.org/project/odoo-addon-${ADDON}/"
+curl --output /dev/null --silent --head --fail "${URL}"
+[[ $? -ne 0 ]] && echo "unknown ADDON <${ADDON}>, should be downloadable from: ${URL}" && exit 3
+
+[[ -d "/home/odoo/data-${CLIENT}/addons/15.0/${ADDON}" ]] && echo "ADDON <${ADDON}> already exists, consider removing manually!" && exit 4
+
+# ACTION
+
+package=$(curl -Ls "$URL" | rg '' -r '$1')
+wget $package -O /tmp/package.zip
+rm /tmp/ADDON -rf && mkdir /tmp/ADDON
+unzip /tmp/package.zip 'odoo/addons/*' -d /tmp/ADDON/
+real_name=$(unzip -l /tmp/package.zip | head -n4 | tail -n1 | cut -d'/' -f3)
+chown -R odoo:odoo /tmp/ADDON/
+mv /tmp/ADDON/odoo/addons/* "/home/odoo/data-$CLIENT/addons/15.0/"
+
+# ADD
+su odoo -c "python3.9 /home/odoo/odoo15/odoo-bin -c /etc/odoo15/$CLIENT.conf -i $real_name -d odoo15-$CLIENT --worker=0 --stop-after-init"
diff --git a/templates/apps/odoo15/odoo.conf.j2 b/templates/apps/odoo15/odoo.conf.j2
new file mode 100644
index 0000000..85e0a8b
--- /dev/null
+++ b/templates/apps/odoo15/odoo.conf.j2
@@ -0,0 +1,17 @@
+[options]
+data_dir = /home/odoo/data-{{ APP_NAME }}
+
+xmlrpc_port = {{ APP_PORT }}
+longpolling_port = {{ LONG_PORT }}
+
+db_host = ct1.lxd
+db_name = odoo15-{{ APP_NAME }}
+db_user = odoo15-{{ APP_NAME }}
+db_password = odoo15-{{ APP_NAME }}
+list_db = {{ target != 'prod'}}
+
+workers = 2
+db_maxconn = 10
+db_filter = .*
+syslog = True
+proxy_mode = True
\ No newline at end of file
diff --git a/templates/apps/odoo15/odoo.service.j2 b/templates/apps/odoo15/odoo.service.j2
new file mode 100644
index 0000000..ffdc620
--- /dev/null
+++ b/templates/apps/odoo15/odoo.service.j2
@@ -0,0 +1,14 @@
+[Unit]
+Description=Odoo15 {{ APP_NAME }}
+After=network.target
+
+[Service]
+Type=simple
+SyslogIdentifier=odoo15-{{ APP_NAME }}
+PermissionsStartOnly=true
+User=odoo
+Group=odoo
+ExecStart=python3.9 /home/odoo/odoo15/odoo-bin -c /etc/odoo15/{{ APP_NAME }}.conf
+
+[Install]
+WantedBy=multi-user.target
\ No newline at end of file
diff --git a/templates/apps/wordpress/wp-backup b/templates/apps/wordpress/wp-backup
new file mode 100644
index 0000000..225e060
--- /dev/null
+++ b/templates/apps/wordpress/wp-backup
@@ -0,0 +1,44 @@
+#!/bin/bash
+
+function detectWordpress() {
+ local result=$(pwd)
+ while [[ ! ("$result" == / || -f "$result/wp-config.php") ]]; do
+ result=$(dirname "$result")
+ done
+
+ if [[ "$result" == / ]]; then
+ echo >&2 "no WORDPRESS detected from current folder <$(pwd)>!"
+ exit 100
+ fi
+
+ echo "$result"
+}
+
+## MAIN
+## ----
+
+set -Eeuo pipefail
+WP_BASE=$(detectWordpress)
+WP_CONFIG="$WP_BASE/wp-config.php"
+DB_HOST=$(grep DB_HOST $WP_CONFIG | cut -d"'" -f4)
+DB_NAME=$(grep DB_NAME $WP_CONFIG | cut -d"'" -f4)
+DB_USER=$(grep DB_USER $WP_CONFIG | cut -d"'" -f4)
+DB_PASSWORD=$(grep DB_PASSWORD $WP_CONFIG | cut -d"'" -f4)
+TODAY=$(date +%F)
+BACKUP_DIR="/mnt/SHARED/wordpress-backup/$DB_NAME-$TODAY"
+
+[[ -d "$BACKUP_DIR" ]] && find "$BACKUP_DIR" -mindepth 1 -delete || mkdir -p "$BACKUP_DIR"
+
+echo -n "backing up database..."
+mariadb-dump -h "$DB_HOST" -u "$DB_NAME" -p"$DB_PASSWORD" "$DB_NAME" | gzip >"$BACKUP_DIR/$DB_NAME".mariadb.gz
+echo OK
+
+echo -n "compressing as tar.gz the wp-content folder ..."
+tar -czvf "$BACKUP_DIR/wp-content.tgz" -C "$WP_BASE" wp-content
+echo OK
+
+echo -n "copying wp-config.php file ..."
+cp "$WP_BASE/wp-config.php" "$BACKUP_DIR"
+echo OK
+
+echo "successful backup in $BACKUP_DIR, db + wp-content + wp-config"
diff --git a/templates/apps/wordpress/wp-host.j2 b/templates/apps/wordpress/wp-host.j2
new file mode 100644
index 0000000..13fff79
--- /dev/null
+++ b/templates/apps/wordpress/wp-host.j2
@@ -0,0 +1,37 @@
+server {
+ listen {{ env.APP_PORT }} default_server;
+
+ access_log /var/log/nginx/{{ env.APP_NAME }}/wp-access.log;
+ error_log /var/log/nginx/{{ env.APP_NAME }}/wp-error.log;
+
+ client_max_body_size 50M;
+ root /var/www/wordpress/{{ env.APP_NAME }};
+ index index.php index.html index.htm;
+ charset UTF-8;
+
+ location / {
+ try_files $uri/ /index.php?$args;
+ }
+
+ location ~ \.php$ {
+ try_files $uri =404;
+ fastcgi_split_path_info ^(.+\.php)(/.+)$;
+ fastcgi_pass unix:/run/php/php-fpm.sock;
+ fastcgi_index index.php;
+ include fastcgi.conf;
+ }
+
+ location ~* \.(js|css|png|jpg|jpeg|svg|gif|ico|eot|otf|ttf|woff|woff2|mp3|wav|ogg)$ {
+ add_header Access-Control-Allow-Origin *;
+ access_log off; log_not_found off; expires 30d;
+ }
+
+ # Mailpoet - tinyMCE quick fix
+ location ~ /wp-content/plugins/wysija-newsletters/js/tinymce/.*\.(htm|html)$ {
+ add_header Access-Control-Allow-Origin *;
+ access_log off; log_not_found off; expires 30d;
+ }
+
+ location = /robots.txt { access_log off; log_not_found off; }
+ location ~ /\. { deny all; access_log off; log_not_found off; }
+}
diff --git a/templates/apps/wordpress/wp-tool b/templates/apps/wordpress/wp-tool
new file mode 100644
index 0000000..721d40b
--- /dev/null
+++ b/templates/apps/wordpress/wp-tool
@@ -0,0 +1,176 @@
+#!/bin/bash
+
+### error_handling
+
+function trap_error() {
+ error_code=$1
+ error_line=$2
+
+ if [[ ${error_code} -lt 100 ]]; then
+ printf "\nEXIT #${error_code} due to error at line ${error_line} : \n-----------------------------------------\n"
+ sed "${error_line}q;d" $0
+ echo
+ fi
+ exit $error_code
+}
+set -e
+trap 'trap_error $? $LINENO' ERR
+
+### ------------------
+
+function detectWordpress() {
+ local result=$(pwd)
+ while [[ ! ("$result" == / || -f "$result/wp-config.php") ]]; do
+ result=$(dirname "$result")
+ done
+
+ if [[ "$result" == / ]]; then
+ echo >&2 "no WORDPRESS detected!"
+ exit 100
+ fi
+
+ echo "$result"
+}
+
+function getConfigComment() {
+ local result=$(grep -e "^#" $WP_CONFIG | grep "$1" | head -n1 | cut -d ',' -f2 | cut -d \' -f2)
+ if [[ -z "$result" ]]; then
+ echo "config comment: $1 not found!"
+ exit 2
+ fi
+ echo "$result"
+}
+function getConfigEntry() {
+ local result=$(grep "$1" $WP_CONFIG | head -n1 | cut -d ',' -f2 | cut -d \' -f2)
+ if [[ -z "$result" ]]; then
+ echo "config entry: $1 not found!"
+ exit 2
+ fi
+ echo "$result"
+}
+
+function sql() {
+ local result=$(echo "$1" | mysql -srN -u $DB_USER -h $DB_HOST $DB_NAME -p$DB_PASS 2>&1)
+ if [[ $result =~ ^ERROR ]]; then
+ echo >&2 "sql failure: $result"
+ exit 3
+ else
+ echo "$result"
+ fi
+}
+
+function sqlFile() {
+ local result=$(cat "$1" | mysql -srN -u $DB_USER -h $DB_HOST $DB_NAME -p$DB_PASS 2>&1)
+ if [[ $result =~ ^ERROR ]]; then
+ echo >&2 "sql failure: $result"
+ exit 3
+ else
+ echo "$result"
+ fi
+}
+function changeHome() {
+ local FROM=$1
+ local TO=$2
+ sql "UPDATE wp_options SET option_value = replace(option_value, '$FROM', '$TO') WHERE option_name = 'home' OR option_name = 'siteurl'"
+ sql "UPDATE wp_posts SET guid = replace(guid, '$FROM','$TO')"
+ sql "UPDATE wp_posts SET post_content = replace(post_content, '$FROM', '$TO')"
+ sql "UPDATE wp_postmeta SET meta_value = replace(meta_value,'$FROM','$TO')"
+}
+
+function lastMigration() {
+ sql "SELECT migration_file FROM migrations ORDER BY last_run DESC LIMIT 1"
+}
+
+function upgradeMigration() {
+ local LAST_MIGRATION=$1
+ local UPGRADE=false
+ if [[ "$LAST_MIGRATION" == '' ]]; then
+ UPGRADE=true
+ fi
+
+ local MIG_BASE="$WP_BASE/wp-content/migrations"
+ local MIGRATIONS=$(ls -p1 $MIG_BASE | grep -v /)
+ local MIG_FILE
+ for mig in $MIGRATIONS; do
+ if [[ "$UPGRADE" == true ]]; then
+ printf "applying %50s ... " $mig
+ printf "%d %d" $(sqlFile $MIG_BASE/$mig)
+ echo " DONE"
+ MIG_FILE=$mig
+ else
+ printf "useless %50s \n" $mig
+ if [[ "$LAST_MIGRATION" == "$mig" ]]; then
+ UPGRADE=true
+ fi
+ fi
+ done
+
+ if [[ $UPGRADE == true && $MIG_FILE != '' ]]; then
+ local done=$(sql "INSERT INTO migrations(migration_file, last_run) VALUES ('$mig', NOW())")
+ echo "all migrations succeeded, wrote: $mig"
+ else
+ echo "already up-to-date"
+ fi
+}
+
+function buildMigrations() {
+ if [[ ! -d "$WP_BASE"/wp-content/migrations ]]; then
+ mkdir -p "$WP_BASE"/wp-content/migrations
+ echo "migrations folder created!"
+ fi
+
+ sql "CREATE TABLE IF NOT EXISTS migrations (id int(11) NOT NULL AUTO_INCREMENT, migration_file varchar(255) COLLATE utf8_unicode_ci NOT NULL, last_run varchar(45) COLLATE utf8_unicode_ci NOT NULL, PRIMARY KEY (id) )"
+
+}
+
+function playEnvironment() {
+
+ buildMigrations
+
+ local PLATFORM=$1
+ local PLATFORM_BASE="$WP_BASE/wp-content/migrations/$PLATFORM"
+ if [[ -d "$PLATFORM_BASE" ]]; then
+ echo play platform $PLATFORM
+
+ local MIGRATIONS=$(ls -p1 $PLATFORM_BASE | grep -v /)
+ for mig in $MIGRATIONS; do
+ printf "applying %50s ... " $mig
+ printf "%d %d" $(sqlFile $PLATFORM_BASE/$mig)
+ echo " DONE"
+ done
+ fi
+}
+
+## MAIN
+## ----
+
+WP_BASE=$(detectWordpress)
+WP_CONFIG="$WP_BASE/wp-config.php"
+echo "WP_BASE = $WP_BASE"
+
+WP_HOME=$(getConfigComment WP_HOME)
+echo "WP_HOME = $WP_HOME"
+
+DB_HOST=$(getConfigEntry DB_HOST)
+DB_NAME=$(getConfigEntry DB_NAME)
+DB_USER=$(getConfigEntry DB_USER)
+DB_PASS=$(getConfigEntry DB_PASSWORD)
+
+CURRENT_HOME=$(sql "SELECT option_value FROM wp_options WHERE option_name = 'home'")
+if [[ "$CURRENT_HOME" != "$WP_HOME" ]]; then
+ echo "HOME detected = $CURRENT_HOME , needs to apply changes"
+ $(changeHome "$CURRENT_HOME" "$WP_HOME")
+fi
+
+if [[ "$WP_HOME" =~ https?:\/\/beta[0-9]*\..*|https?:\/\/.*\.beta[0-9]*\..* ]]; then
+ playEnvironment BETA
+else
+ if [[ "$WP_HOME" =~ https?:\/\/dev[0-9]*\..*|https?:\/\/.*\.dev[0-9]*\..* ]]; then
+ playEnvironment DEV
+ else
+ playEnvironment PROD
+ fi
+fi
+
+CURRENT_MIGRATION=$(lastMigration)
+upgradeMigration "$CURRENT_MIGRATION"
diff --git a/templates/autopostgresqlbackup/cron.daily b/templates/autopostgresqlbackup/cron.daily
new file mode 100755
index 0000000..7e456ab
--- /dev/null
+++ b/templates/autopostgresqlbackup/cron.daily
@@ -0,0 +1,5 @@
+#!/bin/sh
+
+if [ -x /usr/sbin/autopostgresqlbackup ]; then
+ /usr/sbin/autopostgresqlbackup
+fi
diff --git a/templates/autopostgresqlbackup/default.conf b/templates/autopostgresqlbackup/default.conf
new file mode 100644
index 0000000..e407d42
--- /dev/null
+++ b/templates/autopostgresqlbackup/default.conf
@@ -0,0 +1,122 @@
+# ===============================
+# === Debian specific options ===
+#================================
+
+# By default, on Debian systems, only 'postgres' user
+# is allowed to access PostgreSQL databases without password.
+# In order to dump databases we need to run pg_dump/psql
+# commands as 'postgres' with su.
+#
+# The following setting has been added to workraound this issue.
+# (if it is set to empty, 'su' usage will be disabled)
+SU_USERNAME=postgres
+
+#=====================================================================
+# Set the following variables to your system needs
+# (Detailed instructions below variables)
+#=====================================================================
+
+# Username to access the PostgreSQL server e.g. dbuser
+USERNAME=postgres
+
+# Password
+# create a file $HOME/.pgpass containing a line like this
+# hostname:*:*:dbuser:dbpass
+# replace hostname with the value of DBHOST and postgres with
+# the value of USERNAME
+
+# Host name (or IP address) of PostgreSQL server e.g localhost
+DBHOST=localhost
+
+# List of DBNAMES for Daily/Weekly Backup e.g. "DB1 DB2 DB3"
+DBNAMES="all"
+
+# pseudo database name used to dump global objects (users, roles, tablespaces)
+GLOBALS_OBJECTS="postgres_globals"
+
+# Backup directory location e.g /backups
+BACKUPDIR="/mnt/BACKUP/postgresql"
+
+# Mail setup
+# What would you like to be mailed to you?
+# - log : send only log file
+# - files : send log file and sql files as attachments (see docs)
+# - stdout : will simply output the log to the screen if run manually.
+# - quiet : Only send logs if an error occurs to the MAILADDR.
+MAILCONTENT="quiet"
+
+# Set the maximum allowed email size in k. (4000 = approx 5MB email [see docs])
+MAXATTSIZE="4000"
+
+# Email Address to send mail to? (user@domain.com)
+MAILADDR="root"
+
+# ============================================================
+# === ADVANCED OPTIONS ( Read the doc's below for details )===
+#=============================================================
+
+# List of DBBNAMES for Monthly Backups.
+MDBNAMES="$DBNAMES"
+GLOBALS_OBJECTS_INCLUDE="no"
+# List of DBNAMES to EXLUCDE if DBNAMES are set to all (must be in " quotes)
+DBEXCLUDE="postgres template1"
+
+# Include CREATE DATABASE in backup?
+CREATE_DATABASE=yes
+
+# Separate backup directory and file for each DB? (yes or no)
+SEPDIR=yes
+
+# Which day do you want weekly backups? (1 to 7 where 1 is Monday)
+DOWEEKLY=6
+
+# Choose Compression type. (gzip, bzip2 or xz)
+COMP=gzip
+
+# Compress communications between backup server and PostgreSQL server?
+# set compression level from 0 to 9 (0 means no compression)
+COMMCOMP=0
+
+# Additionally keep a copy of the most recent backup in a seperate directory.
+LATEST=no
+
+# OPT string for use with pg_dump ( see man pg_dump )
+OPT=""
+
+# Backup files extension
+EXT="sql"
+
+# Backup files permissions
+PERM=600
+
+# Encyrption settings
+# (inspired by http://blog.altudov.com/2010/09/27/using-openssl-for-asymmetric-encryption-of-backups/)
+#
+# Once the backup done, each SQL dump will be encrypted and the original file
+# will be deleted (if encryption was successful).
+# It is recommended to backup into a staging directory, and then use the
+# POSTBACKUP script to sync the encrypted files to the desired location.
+#
+# Encryption uses private/public keys. You can generate the key pairs like the following:
+# openssl req -x509 -nodes -days 100000 -newkey rsa:2048 -keyout backup.key -out backup.crt -subj '/'
+#
+# Decryption:
+# openssl smime -decrypt -in backup.sql.gz.enc -binary -inform DEM -inkey backup.key -out backup.sql.gz
+
+# Enable encryption
+ENCRYPTION=no
+
+# Encryption public key
+ENCRYPTION_PUBLIC_KEY="/etc/ssl/certs/autopostgresqlbackup.crt"
+
+# Encryption Cipher (see enc manpage)
+ENCRYPTION_CIPHER="aes256"
+
+# Suffix for encyrpted files
+ENCRYPTION_SUFFIX=".enc"
+
+# Command to run before backups (uncomment to use)
+#PREBACKUP="/etc/postgresql-backup-pre"
+
+# Command run after backups (uncomment to use)
+#POSTBACKUP="/etc/postgresql-backup-post"
diff --git a/templates/autopostgresqlbackup/script b/templates/autopostgresqlbackup/script
new file mode 100755
index 0000000..435ba60
--- /dev/null
+++ b/templates/autopostgresqlbackup/script
@@ -0,0 +1,666 @@
+#!/bin/bash
+#
+# PostgreSQL Backup Script Ver 1.0
+# http://autopgsqlbackup.frozenpc.net
+# Copyright (c) 2005 Aaron Axelsen
+# 2005 Friedrich Lobenstock
+# 2013-2019 Emmanuel Bouthenot
+#
+# 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+#=====================================================================
+# Set the following variables to your system needs
+# (Detailed instructions below variables)
+#=====================================================================
+
+# Username to access the PostgreSQL server e.g. dbuser
+USERNAME=postgres
+
+# Password
+# create a file $HOME/.pgpass containing a line like this
+# hostname:*:*:dbuser:dbpass
+# replace hostname with the value of DBHOST and postgres with
+# the value of USERNAME
+
+# Host name (or IP address) of PostgreSQL server e.g localhost
+DBHOST=localhost
+
+# List of DBNAMES for Daily/Weekly Backup e.g. "DB1 DB2 DB3"
+DBNAMES="all"
+
+# pseudo database name used to dump global objects (users, roles, tablespaces)
+GLOBALS_OBJECTS="postgres_globals"
+
+# Backup directory location e.g /backups
+BACKUPDIR="/backups"
+GLOBALS_OBJECTS_INCLUDE="yes"
+
+# Mail setup
+# What would you like to be mailed to you?
+# - log : send only log file
+# - files : send log file and sql files as attachments (see docs)
+# - stdout : will simply output the log to the screen if run manually.
+# - quiet : Only send logs if an error occurs to the MAILADDR.
+MAILCONTENT="stdout"
+
+# Set the maximum allowed email size in k. (4000 = approx 5MB email [see docs])
+MAXATTSIZE="4000"
+
+# Email Address to send mail to? (user@domain.com)
+MAILADDR="user@domain.com"
+
+# ============================================================
+# === ADVANCED OPTIONS ( Read the doc's below for details )===
+#=============================================================
+
+# List of DBBNAMES for Monthly Backups.
+MDBNAMES="template1 $DBNAMES"
+
+# List of DBNAMES to EXLUCDE if DBNAMES are set to all (must be in " quotes)
+DBEXCLUDE=""
+
+# Include CREATE DATABASE in backup?
+CREATE_DATABASE=yes
+
+# Separate backup directory and file for each DB? (yes or no)
+SEPDIR=yes
+
+# Which day do you want weekly backups? (1 to 7 where 1 is Monday)
+DOWEEKLY=6
+
+# Choose Compression type. (gzip, bzip2 or xz)
+COMP=gzip
+
+# Compress communications between backup server and PostgreSQL server?
+# set compression level from 0 to 9 (0 means no compression)
+COMMCOMP=0
+
+# Additionally keep a copy of the most recent backup in a seperate directory.
+LATEST=no
+
+# OPT string for use with pg_dump ( see man pg_dump )
+OPT=""
+
+# Backup files extension
+EXT="sql"
+
+# Backup files permissions
+PERM=600
+
+# Encyrption settings
+# (inspired by http://blog.altudov.com/2010/09/27/using-openssl-for-asymmetric-encryption-of-backups/)
+#
+# Once the backup done, each SQL dump will be encrypted and the original file
+# will be deleted (if encryption was successful).
+# It is recommended to backup into a staging directory, and then use the
+# POSTBACKUP script to sync the encrypted files to the desired location.
+#
+# Encryption uses private/public keys. You can generate the key pairs like the following:
+# openssl req -x509 -nodes -days 100000 -newkey rsa:2048 -keyout backup.key -out backup.crt -subj '/'
+#
+# Decryption:
+# openssl smime -decrypt -in backup.sql.gz.enc -binary -inform DEM -inkey backup.key -out backup.sql.gz
+
+# Enable encryption
+ENCRYPTION=no
+
+# Encryption public key
+ENCRYPTION_PUBLIC_KEY=""
+
+# Encryption Cipher (see enc manpage)
+ENCRYPTION_CIPHER="aes256"
+
+# Suffix for encyrpted files
+ENCRYPTION_SUFFIX=".enc"
+
+# Command to run before backups (uncomment to use)
+#PREBACKUP="/etc/postgresql-backup-pre"
+
+# Command run after backups (uncomment to use)
+#POSTBACKUP="/etc/postgresql-backup-post"
+
+#=====================================================================
+# Debian specific options ===
+#=====================================================================
+
+if [ -f /etc/default/autopostgresqlbackup ]; then
+ . /etc/default/autopostgresqlbackup
+fi
+
+#=====================================================================
+# Options documentation
+#=====================================================================
+# Set USERNAME and PASSWORD of a user that has at least SELECT permission
+# to ALL databases.
+#
+# Set the DBHOST option to the server you wish to backup, leave the
+# default to backup "this server".(to backup multiple servers make
+# copies of this file and set the options for that server)
+#
+# Put in the list of DBNAMES(Databases)to be backed up. If you would like
+# to backup ALL DBs on the server set DBNAMES="all".(if set to "all" then
+# any new DBs will automatically be backed up without needing to modify
+# this backup script when a new DB is created).
+#
+# If the DB you want to backup has a space in the name replace the space
+# with a % e.g. "data base" will become "data%base"
+# NOTE: Spaces in DB names may not work correctly when SEPDIR=no.
+#
+# You can change the backup storage location from /backups to anything
+# you like by using the BACKUPDIR setting..
+#
+# The MAILCONTENT and MAILADDR options and pretty self explanitory, use
+# these to have the backup log mailed to you at any email address or multiple
+# email addresses in a space seperated list.
+# (If you set mail content to "log" you will require access to the "mail" program
+# on your server. If you set this to "files" you will have to have mutt installed
+# on your server. If you set it to "stdout" it will log to the screen if run from
+# the console or to the cron job owner if run through cron. If you set it to "quiet"
+# logs will only be mailed if there are errors reported. )
+#
+# MAXATTSIZE sets the largest allowed email attachments total (all backup files) you
+# want the script to send. This is the size before it is encoded to be sent as an email
+# so if your mail server will allow a maximum mail size of 5MB I would suggest setting
+# MAXATTSIZE to be 25% smaller than that so a setting of 4000 would probably be fine.
+#
+# Finally copy autopostgresqlbackup.sh to anywhere on your server and make sure
+# to set executable permission. You can also copy the script to
+# /etc/cron.daily to have it execute automatically every night or simply
+# place a symlink in /etc/cron.daily to the file if you wish to keep it
+# somwhere else.
+# NOTE:On Debian copy the file with no extention for it to be run
+# by cron e.g just name the file "autopostgresqlbackup"
+#
+# Thats it..
+#
+#
+# === Advanced options doc's ===
+#
+# The list of MDBNAMES is the DB's to be backed up only monthly. You should
+# always include "template1" in this list to backup the default database
+# template used to create new databases.
+# NOTE: If DBNAMES="all" then MDBNAMES has no effect as all DBs will be backed
+# up anyway.
+#
+# If you set DBNAMES="all" you can configure the option DBEXCLUDE. Other
+# wise this option will not be used.
+# This option can be used if you want to backup all dbs, but you want
+# exclude some of them. (eg. a db is to big).
+#
+# Set CREATE_DATABASE to "yes" (the default) if you want your SQL-Dump to create
+# a database with the same name as the original database when restoring.
+# Saying "no" here will allow your to specify the database name you want to
+# restore your dump into, making a copy of the database by using the dump
+# created with autopostgresqlbackup.
+# NOTE: Not used if SEPDIR=no
+#
+# The SEPDIR option allows you to choose to have all DBs backed up to
+# a single file (fast restore of entire server in case of crash) or to
+# seperate directories for each DB (each DB can be restored seperately
+# in case of single DB corruption or loss).
+#
+# To set the day of the week that you would like the weekly backup to happen
+# set the DOWEEKLY setting, this can be a value from 1 to 7 where 1 is Monday,
+# The default is 6 which means that weekly backups are done on a Saturday.
+#
+# COMP is used to choose the copmression used, options are gzip or bzip2.
+# bzip2 will produce slightly smaller files but is more processor intensive so
+# may take longer to complete.
+#
+# COMMCOMP is used to set the compression level (from 0 to 9, 0 means no compression)
+# between the client and the server, so it is useful to save bandwidth when backing up
+# a remote PostgresSQL server over the network.
+#
+# LATEST is to store an additional copy of the latest backup to a standard
+# location so it can be downloaded bt thrid party scripts.
+#
+# Use PREBACKUP and POSTBACKUP to specify Per and Post backup commands
+# or scripts to perform tasks either before or after the backup process.
+#
+#
+#=====================================================================
+# Backup Rotation..
+#=====================================================================
+#
+# Daily Backups are rotated weekly..
+# Weekly Backups are run by default on Saturday Morning when
+# cron.daily scripts are run...Can be changed with DOWEEKLY setting..
+# Weekly Backups are rotated on a 5 week cycle..
+# Monthly Backups are run on the 1st of the month..
+# Monthly Backups are NOT rotated automatically...
+# It may be a good idea to copy Monthly backups offline or to another
+# server..
+#
+#=====================================================================
+# Please Note!!
+#=====================================================================
+#
+# I take no resposibility for any data loss or corruption when using
+# this script..
+# This script will not help in the event of a hard drive crash. If a
+# copy of the backup has not be stored offline or on another PC..
+# You should copy your backups offline regularly for best protection.
+#
+# Happy backing up...
+#
+#=====================================================================
+# Restoring
+#=====================================================================
+# Firstly you will need to uncompress the backup file.
+# eg.
+# gunzip file.gz (or bunzip2 file.bz2)
+#
+# Next you will need to use the postgresql client to restore the DB from the
+# sql file.
+# eg.
+# psql --host dbserver --dbname database < /path/file.sql
+#
+# NOTE: Make sure you use "<" and not ">" in the above command because
+# you are piping the file.sql to psql and not the other way around.
+#
+# Lets hope you never have to use this.. :)
+#
+#=====================================================================
+# Change Log
+#=====================================================================
+#
+# VER 1.0 - (2005-03-25)
+# Initial Release - based on AutoMySQLBackup 2.2
+#
+#=====================================================================
+#=====================================================================
+#=====================================================================
+#
+# Should not need to be modified from here down!!
+#
+#=====================================================================
+#=====================================================================
+#=====================================================================
+PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/postgres/bin:/usr/local/pgsql/bin
+DATE=$(date +%Y-%m-%d_%Hh%Mm) # Datestamp e.g 2002-09-21
+DOW=$(date +%A) # Day of the week e.g. Monday
+DNOW=$(date +%u) # Day number of the week 1 to 7 where 1 represents Monday
+DOM=$(date +%d) # Date of the Month e.g. 27
+M=$(date +%B) # Month e.g January
+W=$(date +%V) # Week Number e.g 37
+VER=1.0 # Version Number
+LOGFILE=$BACKUPDIR/${DBHOST//\//_}-$(date +%N).log # Logfile Name
+LOGERR=$BACKUPDIR/ERRORS_${DBHOST//\//_}-$(date +%N).log # Logfile Name
+BACKUPFILES=""
+
+# Add --compress pg_dump option to $OPT
+if [ "$COMMCOMP" -gt 0 ]; then
+ OPT="$OPT --compress=$COMMCOMP"
+fi
+
+# Create required directories
+if [ ! -e "$BACKUPDIR" ]; then # Check Backup Directory exists.
+ mkdir -p "$BACKUPDIR"
+fi
+
+if [ ! -e "$BACKUPDIR/daily" ]; then # Check Daily Directory exists.
+ mkdir -p "$BACKUPDIR/daily"
+fi
+
+if [ ! -e "$BACKUPDIR/weekly" ]; then # Check Weekly Directory exists.
+ mkdir -p "$BACKUPDIR/weekly"
+fi
+
+if [ ! -e "$BACKUPDIR/monthly" ]; then # Check Monthly Directory exists.
+ mkdir -p "$BACKUPDIR/monthly"
+fi
+
+if [ "$LATEST" = "yes" ]; then
+ if [ ! -e "$BACKUPDIR/latest" ]; then # Check Latest Directory exists.
+ mkdir -p "$BACKUPDIR/latest"
+ fi
+ rm -f "$BACKUPDIR"/latest/*
+fi
+
+# IO redirection for logging.
+touch $LOGFILE
+exec 6>&1 # Link file descriptor #6 with stdout.
+# Saves stdout.
+exec >$LOGFILE # stdout replaced with file $LOGFILE.
+touch $LOGERR
+exec 7>&2 # Link file descriptor #7 with stderr.
+# Saves stderr.
+exec 2>$LOGERR # stderr replaced with file $LOGERR.
+
+# Functions
+
+# Database dump function
+dbdump() {
+ rm -f $2
+ touch $2
+ chmod $PERM $2
+ for db in $1; do
+ if [ -n "$SU_USERNAME" ]; then
+ if [ "$db" = "$GLOBALS_OBJECTS" ]; then
+ su $SU_USERNAME -l -c "pg_dumpall $PGHOST --globals-only" >>$2
+ else
+ su $SU_USERNAME -l -c "pg_dump $PGHOST $OPT $db" >>$2
+ fi
+ else
+ if [ "$db" = "$GLOBALS_OBJECTS" ]; then
+ pg_dumpall --username=$USERNAME $PGHOST --globals-only >>$2
+ else
+ pg_dump --username=$USERNAME $PGHOST $OPT $db >>$2
+ fi
+ fi
+ done
+ return 0
+}
+
+# Encryption function
+encryption() {
+ ENCRYPTED_FILE="$1$ENCRYPTION_SUFFIX"
+ # Encrypt as needed
+ if [ "$ENCRYPTION" = "yes" ]; then
+ echo
+ echo "Encrypting $1"
+ echo " to $ENCRYPTED_FILE"
+ echo " using cypher $ENCRYPTION_CIPHER and public key $ENCRYPTION_PUBLIC_KEY"
+ if openssl smime -encrypt -$ENCRYPTION_CIPHER -binary -outform DEM \
+ -out "$ENCRYPTED_FILE" \
+ -in "$1" "$ENCRYPTION_PUBLIC_KEY"; then
+ echo " and remove $1"
+ chmod $PERM "$ENCRYPTED_FILE"
+ rm -f "$1"
+ fi
+ fi
+ return 0
+}
+
+# Compression (and encrypt) function plus latest copy
+SUFFIX=""
+compression() {
+ if [ "$COMP" = "gzip" ]; then
+ gzip -f "$1"
+ echo
+ echo Backup Information for "$1"
+ gzip -l "$1.gz"
+ SUFFIX=".gz"
+ elif [ "$COMP" = "bzip2" ]; then
+ echo Compression information for "$1.bz2"
+ bzip2 -f -v $1 2>&1
+ SUFFIX=".bz2"
+ elif [ "$COMP" = "xz" ]; then
+ echo Compression information for "$1.xz"
+ xz -9 -v $1 2>&1
+ SUFFIX=".xz"
+ else
+ echo "No compression option set, check advanced settings"
+ fi
+ encryption $1$SUFFIX
+ if [ "$LATEST" = "yes" ]; then
+ cp $1$SUFFIX* "$BACKUPDIR/latest/"
+ fi
+ return 0
+}
+
+# Run command before we begin
+if [ "$PREBACKUP" ]; then
+ echo ======================================================================
+ echo "Prebackup command output."
+ echo
+ $PREBACKUP
+ echo
+ echo ======================================================================
+ echo
+fi
+
+if [ "$SEPDIR" = "yes" ]; then # Check if CREATE DATABSE should be included in Dump
+ if [ "$CREATE_DATABASE" = "no" ]; then
+ OPT="$OPT"
+ else
+ OPT="$OPT --create"
+ fi
+else
+ OPT="$OPT"
+fi
+
+# Hostname for LOG information
+if [ "$DBHOST" = "localhost" ]; then
+ HOST=$(hostname)
+ PGHOST=""
+else
+ HOST=$DBHOST
+ PGHOST="-h $DBHOST"
+fi
+
+# If backing up all DBs on the server
+if [ "$DBNAMES" = "all" ]; then
+ if [ -n "$SU_USERNAME" ]; then
+ DBNAMES="$(su $SU_USERNAME -l -c "LANG=C psql -U $USERNAME $PGHOST -l -A -F: | sed -ne '/:/ { /Name:Owner/d; /template0/d; s/:.*$//; p }'")"
+ else
+ DBNAMES="$(LANG=C psql -U $USERNAME $PGHOST -l -A -F: | sed -ne "/:/ { /Name:Owner/d; /template0/d; s/:.*$//; p }")"
+ fi
+
+ # If DBs are excluded
+ for exclude in $DBEXCLUDE; do
+ DBNAMES=$(echo $DBNAMES | sed "s/\b$exclude\b//g")
+ done
+ DBNAMES="$(echo $DBNAMES | tr '\n' ' ')"
+ MDBNAMES=$DBNAMES
+fi
+
+# Include global objects (users, tablespaces)
+if [ "$GLOBALS_OBJECTS_INCLUDE" = "yes" ]; then
+ DBNAMES="$GLOBALS_OBJECTS $DBNAMES"
+ MDBNAMES="$GLOBALS_OBJECTS $MDBNAMES"
+fi
+
+echo ======================================================================
+echo AutoPostgreSQLBackup VER $VER
+echo http://autopgsqlbackup.frozenpc.net/
+echo
+echo Backup of Database Server - $HOST
+echo ======================================================================
+
+# Test is seperate DB backups are required
+if [ "$SEPDIR" = "yes" ]; then
+ echo Backup Start Time $(date)
+ echo ======================================================================
+ # Monthly Full Backup of all Databases
+ if [ "$DOM" = "01" ]; then
+ for MDB in $MDBNAMES; do
+
+ # Prepare $DB for using
+ MDB="$(echo $MDB | sed 's/%/ /g')"
+
+ if [ ! -e "$BACKUPDIR/monthly/$MDB" ]; then # Check Monthly DB Directory exists.
+ mkdir -p "$BACKUPDIR/monthly/$MDB"
+ fi
+ echo Monthly Backup of $MDB...
+ dbdump "$MDB" "$BACKUPDIR/monthly/$MDB/${MDB}_$DATE.$M.$MDB.$EXT"
+ compression "$BACKUPDIR/monthly/$MDB/${MDB}_$DATE.$M.$MDB.$EXT"
+ BACKUPFILES="$BACKUPFILES $BACKUPDIR/monthly/$MDB/${MDB}_$DATE.$M.$MDB.$EXT$SUFFIX*"
+ echo ----------------------------------------------------------------------
+ done
+ fi
+
+ for DB in $DBNAMES; do
+ # Prepare $DB for using
+ DB="$(echo $DB | sed 's/%/ /g')"
+
+ # Create Seperate directory for each DB
+ if [ ! -e "$BACKUPDIR/daily/$DB" ]; then # Check Daily DB Directory exists.
+ mkdir -p "$BACKUPDIR/daily/$DB"
+ fi
+
+ if [ ! -e "$BACKUPDIR/weekly/$DB" ]; then # Check Weekly DB Directory exists.
+ mkdir -p "$BACKUPDIR/weekly/$DB"
+ fi
+
+ # Weekly Backup
+ if [ "$DNOW" = "$DOWEEKLY" ]; then
+ echo Weekly Backup of Database \( $DB \)
+ echo Rotating 5 weeks Backups...
+ if [ "$W" -le 05 ]; then
+ REMW=$(expr 48 + $W)
+ elif [ "$W" -lt 15 ]; then
+ REMW=0$(expr $W - 5)
+ else
+ REMW=$(expr $W - 5)
+ fi
+ rm -fv "$BACKUPDIR/weekly/$DB/${DB}_week.$REMW".*
+ echo
+ dbdump "$DB" "$BACKUPDIR/weekly/$DB/${DB}_week.$W.$DATE.$EXT"
+ compression "$BACKUPDIR/weekly/$DB/${DB}_week.$W.$DATE.$EXT"
+ BACKUPFILES="$BACKUPFILES $BACKUPDIR/weekly/$DB/${DB}_week.$W.$DATE.$EXT$SUFFIX*"
+ echo ----------------------------------------------------------------------
+
+ # Daily Backup
+ else
+ echo Daily Backup of Database \( $DB \)
+ echo Rotating last weeks Backup...
+ rm -fv "$BACKUPDIR/daily/$DB"/*."$DOW".$EXT*
+ echo
+ dbdump "$DB" "$BACKUPDIR/daily/$DB/${DB}_$DATE.$DOW.$EXT"
+ compression "$BACKUPDIR/daily/$DB/${DB}_$DATE.$DOW.$EXT"
+ BACKUPFILES="$BACKUPFILES $BACKUPDIR/daily/$DB/${DB}_$DATE.$DOW.$EXT$SUFFIX*"
+ echo ----------------------------------------------------------------------
+ fi
+ done
+ echo Backup End $(date)
+ echo ======================================================================
+
+else
+ # One backup file for all DBs
+ echo Backup Start $(date)
+ echo ======================================================================
+ # Monthly Full Backup of all Databases
+ if [ "$DOM" = "01" ]; then
+ echo Monthly full Backup of \( $MDBNAMES \)...
+ dbdump "$MDBNAMES" "$BACKUPDIR/monthly/$DATE.$M.all-databases.$EXT"
+ compression "$BACKUPDIR/monthly/$DATE.$M.all-databases.$EXT"
+ BACKUPFILES="$BACKUPFILES $BACKUPDIR/monthly/$DATE.$M.all-databases.$EXT$SUFFIX*"
+ echo ----------------------------------------------------------------------
+ fi
+
+ # Weekly Backup
+ if [ "$DNOW" = "$DOWEEKLY" ]; then
+ echo Weekly Backup of Databases \( $DBNAMES \)
+ echo
+ echo Rotating 5 weeks Backups...
+ if [ "$W" -le 05 ]; then
+ REMW=$(expr 48 + $W)
+ elif [ "$W" -lt 15 ]; then
+ REMW=0$(expr $W - 5)
+ else
+ REMW=$(expr $W - 5)
+ fi
+ rm -fv "$BACKUPDIR/weekly/week.$REMW".*
+ echo
+ dbdump "$DBNAMES" "$BACKUPDIR/weekly/week.$W.$DATE.$EXT"
+ compression "$BACKUPDIR/weekly/week.$W.$DATE.$EXT"
+ BACKUPFILES="$BACKUPFILES $BACKUPDIR/weekly/week.$W.$DATE.$EXT$SUFFIX*"
+ echo ----------------------------------------------------------------------
+ # Daily Backup
+ else
+ echo Daily Backup of Databases \( $DBNAMES \)
+ echo
+ echo Rotating last weeks Backup...
+ rm -fv "$BACKUPDIR"/daily/*."$DOW".$EXT*
+ echo
+ dbdump "$DBNAMES" "$BACKUPDIR/daily/$DATE.$DOW.$EXT"
+ compression "$BACKUPDIR/daily/$DATE.$DOW.$EXT"
+ BACKUPFILES="$BACKUPFILES $BACKUPDIR/daily/$DATE.$DOW.$EXT$SUFFIX*"
+ echo ----------------------------------------------------------------------
+ fi
+ echo Backup End Time $(date)
+ echo ======================================================================
+fi
+echo Total disk space used for backup storage..
+echo Size - Location
+echo $(du -hs "$BACKUPDIR")
+echo
+
+# Run command when we're done
+if [ "$POSTBACKUP" ]; then
+ echo ======================================================================
+ echo "Postbackup command output."
+ echo
+ $POSTBACKUP
+ echo
+ echo ======================================================================
+fi
+
+#Clean up IO redirection
+exec 1>&6 6>&- # Restore stdout and close file descriptor #6.
+exec 2>&7 7>&- # Restore stdout and close file descriptor #7.
+
+if [ "$MAILCONTENT" = "files" ]; then
+ if [ -s "$LOGERR" ]; then
+ # Include error log if is larger than zero.
+ BACKUPFILES="$BACKUPFILES $LOGERR"
+ ERRORNOTE="WARNING: Error Reported - "
+ fi
+ #Get backup size
+ ATTSIZE=$(du -c $BACKUPFILES | grep "[[:digit:][:space:]]total$" | sed s/\s*total//)
+ if [ $MAXATTSIZE -ge $ATTSIZE ]; then
+ if which biabam >/dev/null 2>&1; then
+ BACKUPFILES=$(echo $BACKUPFILES | sed -r -e 's#\s+#,#g')
+ biabam -s "PostgreSQL Backup Log and SQL Files for $HOST - $DATE" $BACKUPFILES $MAILADDR <$LOGFILE
+ elif which heirloom-mailx >/dev/null 2>&1; then
+ BACKUPFILES=$(echo $BACKUPFILES | sed -e 's# # -a #g')
+ heirloom-mailx -s "PostgreSQL Backup Log and SQL Files for $HOST - $DATE" $BACKUPFILES $MAILADDR <$LOGFILE
+ elif which neomutt >/dev/null 2>&1; then
+ BACKUPFILES=$(echo $BACKUPFILES | sed -e 's# # -a #g')
+ neomutt -s "PostgreSQL Backup Log and SQL Files for $HOST - $DATE" -a $BACKUPFILES -- $MAILADDR <$LOGFILE
+ elif which mutt >/dev/null 2>&1; then
+ BACKUPFILES=$(echo $BACKUPFILES | sed -e 's# # -a #g')
+ mutt -s "PostgreSQL Backup Log and SQL Files for $HOST - $DATE" -a $BACKUPFILES -- $MAILADDR <$LOGFILE
+ else
+ cat "$LOGFILE" | mail -s "WARNING! - Enable to send PostgreSQL Backup dumps, no suitable mail client found on $HOST - $DATE" $MAILADDR
+ fi
+ else
+ cat "$LOGFILE" | mail -s "WARNING! - PostgreSQL Backup exceeds set maximum attachment size on $HOST - $DATE" $MAILADDR
+ fi
+elif [ "$MAILCONTENT" = "log" ]; then
+ cat "$LOGFILE" | mail -s "PostgreSQL Backup Log for $HOST - $DATE" $MAILADDR
+ if [ -s "$LOGERR" ]; then
+ cat "$LOGERR" | mail -s "ERRORS REPORTED: PostgreSQL Backup error Log for $HOST - $DATE" $MAILADDR
+ fi
+elif [ "$MAILCONTENT" = "quiet" ]; then
+ if [ -s "$LOGERR" ]; then
+ cat "$LOGERR" | mail -s "ERRORS REPORTED: PostgreSQL Backup error Log for $HOST - $DATE" $MAILADDR
+ cat "$LOGFILE" | mail -s "PostgreSQL Backup Log for $HOST - $DATE" $MAILADDR
+ fi
+else
+ if [ -s "$LOGERR" ]; then
+ cat "$LOGFILE"
+ echo
+ echo "###### WARNING ######"
+ echo "Errors reported during AutoPostgreSQLBackup execution.. Backup failed"
+ echo "Error log below.."
+ cat "$LOGERR"
+ else
+ cat "$LOGFILE"
+ fi
+fi
+
+if [ -s "$LOGERR" ]; then
+ STATUS=1
+else
+ STATUS=0
+fi
+
+# Clean up Logfile
+rm -f "$LOGFILE"
+rm -f "$LOGERR"
+
+exit $STATUS
diff --git a/templates/bottom/bottom.toml b/templates/bottom/bottom.toml
new file mode 100644
index 0000000..27438f6
--- /dev/null
+++ b/templates/bottom/bottom.toml
@@ -0,0 +1,162 @@
+[flags]
+# Whether to hide the average cpu entry.
+#hide_avg_cpu = false
+# Whether to use dot markers rather than braille.
+#dot_marker = false
+# The update rate of the application.
+#rate = 1000
+# Whether to put the CPU legend to the left.
+#left_legend = false
+# Whether to set CPU% on a process to be based on the total CPU or just current usage.
+#current_usage = false
+# Whether to group processes with the same name together by default.
+#group_processes = false
+# Whether to make process searching case sensitive by default.
+#case_sensitive = false
+# Whether to make process searching look for matching the entire word by default.
+#whole_word = false
+# Whether to make process searching use regex by default.
+#regex = false
+# Defaults to Celsius. Temperature is one of:
+#temperature_type = "k"
+#temperature_type = "f"
+#temperature_type = "c"
+#temperature_type = "kelvin"
+#temperature_type = "fahrenheit"
+#temperature_type = "celsius"
+# The default time interval (in milliseconds).
+#default_time_value = 60000
+# The time delta on each zoom in/out action (in milliseconds).
+#time_delta = 15000
+# Hides the time scale.
+#hide_time = false
+# Override layout default widget
+#default_widget_type = "proc"
+#default_widget_count = 1
+# Use basic mode
+#basic = false
+# Use the old network legend style
+#use_old_network_legend = false
+# Remove space in tables
+#hide_table_gap = false
+# Show the battery widgets
+#battery = false
+# Disable mouse clicks
+#disable_click = false
+# Built-in themes. Valid values are "default", "default-light", "gruvbox", "gruvbox-light", "nord", "nord-light"
+#color = "default"
+# Show memory values in the processes widget as values by default
+#mem_as_value = false
+# Show tree mode by default in the processes widget.
+#tree = false
+# Shows an indicator in table widgets tracking where in the list you are.
+#show_table_scroll_position = false
+# Show processes as their commands by default in the process widget.
+#process_command = false
+# Displays the network widget with binary prefixes.
+#network_use_binary_prefix = false
+# Displays the network widget using bytes.
+network_use_bytes = true
+# Displays the network widget with a log scale.
+#network_use_log = false
+# Hides advanced options to stop a process on Unix-like systems.
+#disable_advanced_kill = false
+
+# These are all the components that support custom theming. Note that colour support
+# will depend on terminal support.
+
+#[colors] # Uncomment if you want to use custom colors
+# Represents the colour of table headers (processes, CPU, disks, temperature).
+#table_header_color="LightBlue"
+# Represents the colour of the label each widget has.
+#widget_title_color="Gray"
+# Represents the average CPU color.
+#avg_cpu_color="Red"
+# Represents the colour the core will use in the CPU legend and graph.
+#cpu_core_colors=["LightMagenta", "LightYellow", "LightCyan", "LightGreen", "LightBlue", "LightRed", "Cyan", "Green", "Blue", "Red"]
+# Represents the colour RAM will use in the memory legend and graph.
+#ram_color="LightMagenta"
+# Represents the colour SWAP will use in the memory legend and graph.
+#swap_color="LightYellow"
+# Represents the colour rx will use in the network legend and graph.
+#rx_color="LightCyan"
+# Represents the colour tx will use in the network legend and graph.
+#tx_color="LightGreen"
+# Represents the colour of the border of unselected widgets.
+#border_color="Gray"
+# Represents the colour of the border of selected widgets.
+#highlighted_border_color="LightBlue"
+# Represents the colour of most text.
+#text_color="Gray"
+# Represents the colour of text that is selected.
+#selected_text_color="Black"
+# Represents the background colour of text that is selected.
+#selected_bg_color="LightBlue"
+# Represents the colour of the lines and text of the graph.
+#graph_color="Gray"
+# Represents the colours of the battery based on charge
+#high_battery_color="green"
+#medium_battery_color="yellow"
+#low_battery_color="red"
+
+# Layout - layouts follow a pattern like this:
+# [[row]] represents a row in the application.
+# [[row.child]] represents either a widget or a column.
+# [[row.child.child]] represents a widget.
+#
+# All widgets must have the type value set to one of ["cpu", "mem", "proc", "net", "temp", "disk", "empty"].
+# All layout components have a ratio value - if this is not set, then it defaults to 1.
+# The default widget layout:
+[[row]]
+ratio=30
+[[row.child]]
+type="cpu"
+
+[[row]]
+ratio=40
+[[row.child]]
+ratio=4
+type="mem"
+[[row.child]]
+ratio=3
+[[row.child.child]]
+type="disk"
+
+[[row]]
+ratio=30
+[[row.child]]
+type="net"
+[[row.child]]
+type="proc"
+default=true
+
+
+# Filters - you can hide specific temperature sensors, network interfaces, and disks using filters. This is admittedly
+# a bit hard to use as of now, and there is a planned in-app interface for managing this in the future:
+[disk_filter]
+is_list_ignored = true
+list = ["/dev/loop\\d+"]
+regex = true
+case_sensitive = false
+whole_word = false
+
+#[mount_filter]
+#is_list_ignored = true
+#list = ["/mnt/.*", "/boot"]
+#regex = true
+#case_sensitive = false
+#whole_word = false
+
+#[temp_filter]
+#is_list_ignored = true
+#list = ["cpu", "wifi"]
+#regex = false
+#case_sensitive = false
+#whole_word = false
+
+#[net_filter]
+#is_list_ignored = true
+#list = ["virbr0.*"]
+#regex = true
+#case_sensitive = false
+#whole_word = false
diff --git a/templates/dev-container-ssh/sshd_config.j2 b/templates/dev-container-ssh/sshd_config.j2
new file mode 100644
index 0000000..b28ffcb
--- /dev/null
+++ b/templates/dev-container-ssh/sshd_config.j2
@@ -0,0 +1,14 @@
+Port 22
+AddressFamily inet
+
+AllowUsers {{ env.USERS }}
+
+AcceptEnv LANG LC_*
+Subsystem sftp internal-sftp
+UsePAM yes
+PasswordAuthentication no
+PermitRootLogin no
+PrintLastLog no
+PrintMotd no
+ChallengeResponseAuthentication no
+X11Forwarding no
\ No newline at end of file
diff --git a/templates/etc/defaults.yaml.j2 b/templates/etc/defaults.yaml.j2
new file mode 100644
index 0000000..cebe29e
--- /dev/null
+++ b/templates/etc/defaults.yaml.j2
@@ -0,0 +1,10 @@
+---
+containers:
+ dmz: [dmz]
+ ct1: [mariadb, postgresql]
+credential:
+ username: {{env.current_user}}
+ shadow: {{env.shadow_passwd}}
+ email: TO BE DEFINED # example user@domain.tld
+ password: TO BE DEFINED
+
diff --git a/templates/etc/miaou.yaml.j2 b/templates/etc/miaou.yaml.j2
new file mode 100644
index 0000000..0adbdf2
--- /dev/null
+++ b/templates/etc/miaou.yaml.j2
@@ -0,0 +1,3 @@
+---
+services: []
+containers: []
diff --git a/templates/etc/miaou.yaml.sample b/templates/etc/miaou.yaml.sample
new file mode 100644
index 0000000..8caa68f
--- /dev/null
+++ b/templates/etc/miaou.yaml.sample
@@ -0,0 +1,26 @@
+---
+containers:
+ ct2: [ dokuwiki, dolibarr13 ]
+services:
+ artcode.re:
+ compta:
+ app: dolibarr13
+ port: 9001
+ name: doli-artcode
+ enabled: false
+ pnrun.re:
+ wiki:
+ app: dokuwiki
+ port: 80
+ name: doku-pnrun
+ enabled: false
+ original:
+ app: dokuwiki
+ port: 8001
+ name: doku-first
+ enabled: false
+ comptoirduvrac.re:
+ gestion:
+ app: odoo12
+ port: 6003
+ name: cdv
diff --git a/templates/hardened/firewall.table b/templates/hardened/firewall.table
new file mode 100644
index 0000000..b48bdae
--- /dev/null
+++ b/templates/hardened/firewall.table
@@ -0,0 +1,23 @@
+table inet firewall {
+
+ chain input {
+ type filter hook input priority 0; policy drop;
+
+ # established/related connections
+ ct state established,related accept
+
+ # loopback + lxdbr0 interface
+ iifname lo accept
+ iifname lxdbr0 accept
+
+ # icmp
+ icmp type echo-request accept
+
+ # allow mDNS
+ udp dport mdns accept
+
+ # allow SSH + GITEA + NGINX
+ tcp dport {22, 2222, 80, 443} accept
+ }
+
+}
diff --git a/templates/hardened/hardened.yaml.sample b/templates/hardened/hardened.yaml.sample
new file mode 100644
index 0000000..a39480f
--- /dev/null
+++ b/templates/hardened/hardened.yaml.sample
@@ -0,0 +1,14 @@
+---
+
+authorized:
+ pubkey: TO_BE_DEFINED
+
+alert:
+ to: TO_BE_DEFINED # example: mine@domain.tld
+ from: TO_BE_DEFINED # example: no-reply@domain.tld
+ smtp:
+ server: TO_BE_DEFINED # example: mail.domain.tld
+ username: TO_BE_DEFINED # example: postmaster@domain.tld
+ password: TO_BE_DEFINED
+
+timezone: # optional, example: UTC, Indian/Reunion, ...
diff --git a/templates/hardened/mailer/aliases.j2 b/templates/hardened/mailer/aliases.j2
new file mode 100644
index 0000000..e6efadb
--- /dev/null
+++ b/templates/hardened/mailer/aliases.j2
@@ -0,0 +1,2 @@
+{{ env.current_user }}: root
+root: {{ alert.to }}
\ No newline at end of file
diff --git a/templates/hardened/mailer/mail.rc.j2 b/templates/hardened/mailer/mail.rc.j2
new file mode 100644
index 0000000..22d26e3
--- /dev/null
+++ b/templates/hardened/mailer/mail.rc.j2
@@ -0,0 +1,6 @@
+set ask askcc append dot save crt
+#ignore Received Message-Id Resent-Message-Id Status Mail-From Return-Path Via Delivered-To
+set mta=/usr/bin/msmtp
+
+alias {{ env.current_user }} root
+alias root {{ alert.to }}
\ No newline at end of file
diff --git a/templates/hardened/mailer/msmtprc.j2 b/templates/hardened/mailer/msmtprc.j2
new file mode 100644
index 0000000..7c0dfa1
--- /dev/null
+++ b/templates/hardened/mailer/msmtprc.j2
@@ -0,0 +1,26 @@
+# Set default values for all following accounts.
+defaults
+
+# Use the mail submission port 587 instead of the SMTP port 25.
+port 587
+
+# Always use TLS.
+tls on
+
+# Set a list of trusted CAs for TLS. The default is to use system settings, but
+# you can select your own file.
+tls_trust_file /etc/ssl/certs/ca-certificates.crt
+
+# The SMTP server of your ISP
+account alert
+host {{ alert.smtp.server }}
+from {{ env.fqdn }} <{{ alert.from }}>
+auth on
+user {{ alert.smtp.username }}
+password {{ alert.smtp.password }}
+
+# Set default account to isp
+account default: alert
+
+# Map local users to mail addresses
+aliases /etc/aliases
diff --git a/templates/hardened/motd/10-header b/templates/hardened/motd/10-header
new file mode 100644
index 0000000..75c5275
--- /dev/null
+++ b/templates/hardened/motd/10-header
@@ -0,0 +1,31 @@
+#!/bin/bash
+
+hostname=$(hostname -s)
+number=$(echo $hostname | grep -oP '[0-9]*$')
+hostname=${hostname%"$number"}
+rows=9
+
+case $hostname in
+'prod')
+ #print in RED
+ echo -ne "\033[31;1m"
+ ;;
+'beta')
+ rows=7
+ #print in ORANGE
+ echo -ne "\033[33;1m"
+ ;;
+'dev')
+ rows=7
+ #print in GREEN
+ echo -ne "\033[33;1m"
+ ;;
+*)
+ #print in GREEN
+ echo -ne "\033[32;1m"
+ ;;
+esac
+
+fullname="$hostname $number"
+figlet -f big "$fullname" | head -n$rows
+echo -ne "\033[0m"
diff --git a/templates/hardened/motd/40-machineinfo b/templates/hardened/motd/40-machineinfo
new file mode 100644
index 0000000..6b95e6f
--- /dev/null
+++ b/templates/hardened/motd/40-machineinfo
@@ -0,0 +1,15 @@
+#!/bin/bash
+
+FQDN=$(hostname --fqdn)
+IP_ADDRESS=$(hostname -I | cut -d ' ' -f1)
+DISTRO=$(lsb_release -d | cut -f2)
+KERNEL=$(uname -srm)
+UPTIME=$(uptime | awk -F'( |,|:)+' '{if ($7=="min") m=$6; else {if ($7~/^day/) {d=$6;h=$8;m=$9} else {h=$6;m=$7}}} {print d+0,"days"}')
+LOAD=$(cat /proc/loadavg)
+
+echo "FQDN : $FQDN"
+echo "UPTIME: $UPTIME"
+echo "IPADDR: $IP_ADDRESS"
+echo "DISTRO: $DISTRO"
+echo "KERNEL: $KERNEL"
+echo "LOAD : $LOAD"
diff --git a/templates/hardened/motd/80-users b/templates/hardened/motd/80-users
new file mode 100644
index 0000000..4c13c02
--- /dev/null
+++ b/templates/hardened/motd/80-users
@@ -0,0 +1,15 @@
+#!/bin/bash
+
+RED='\033[0;31m'
+NC='\033[0m' # No Color
+
+USERS=$(
+ w -uh
+)
+
+if [ -n "$USERS" ]; then
+ echo '-----------------------------------------------'
+ echo -e "${RED}Beware,${NC} there is another connected user${RED}"
+ echo "$USERS"
+ echo -e "${NC}-----------------------------------------------"
+fi
diff --git a/templates/hardened/nftables.conf b/templates/hardened/nftables.conf
new file mode 100644
index 0000000..17941ae
--- /dev/null
+++ b/templates/hardened/nftables.conf
@@ -0,0 +1,5 @@
+#!/usr/sbin/nft -f
+
+flush ruleset
+
+include "/etc/nftables.rules.d/*"
diff --git a/templates/hardened/pam/alert_ssh_password.sh b/templates/hardened/pam/alert_ssh_password.sh
new file mode 100644
index 0000000..6d4d105
--- /dev/null
+++ b/templates/hardened/pam/alert_ssh_password.sh
@@ -0,0 +1,17 @@
+#!/bin/bash
+
+[[ "$PAM_TYPE" != "open_session" ]] && exit 0
+
+if journalctl --since "1 minute ago" -u ssh | tac | grep Accepted -m1 | grep password; then
+ {
+ echo "User: $PAM_USER"
+ echo "Remote Host: $PAM_RHOST"
+ echo "Service: $PAM_SERVICE"
+ echo "TTY: $PAM_TTY"
+ echo "Date: $(date)"
+ echo "Server: $(uname -a)"
+ echo
+ echo "Somebody has successfully logged in your machine, please be aware and acknowledge this event."
+ } | mail -s "$PAM_SERVICE login on $(hostname -f) for account $PAM_USER" root
+fi
+exit 0
diff --git a/templates/hardened/sshd_config.j2 b/templates/hardened/sshd_config.j2
new file mode 100644
index 0000000..e808ddd
--- /dev/null
+++ b/templates/hardened/sshd_config.j2
@@ -0,0 +1,16 @@
+Port 2222
+AllowUsers {{env.current_user}}
+
+AcceptEnv LANG LC_*
+Subsystem sftp /usr/lib/openssh/sftp-server
+ClientAliveInterval 120
+UsePAM yes
+MaxAuthTries 3
+
+PasswordAuthentication no
+PermitRootLogin no
+PermitEmptyPasswords no
+PrintLastLog no
+PrintMotd no
+ChallengeResponseAuthentication no
+X11Forwarding no
\ No newline at end of file
diff --git a/templates/hardened/sudoers.j2 b/templates/hardened/sudoers.j2
new file mode 100644
index 0000000..df402e4
--- /dev/null
+++ b/templates/hardened/sudoers.j2
@@ -0,0 +1,6 @@
+Defaults env_reset
+Defaults mail_badpass
+Defaults secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/TOOLBOX"
+
+User_Alias ROOT = root, {{env.current_user}}
+ROOT ALL=(ALL:ALL) NOPASSWD: ALL
\ No newline at end of file
diff --git a/templates/hardened/systemd/on_startup.service b/templates/hardened/systemd/on_startup.service
new file mode 100644
index 0000000..8404b1a
--- /dev/null
+++ b/templates/hardened/systemd/on_startup.service
@@ -0,0 +1,12 @@
+[Unit]
+Description=Startup Script
+After=network-online.target
+Wants=network-online.target
+
+[Service]
+ExecStartPre=/bin/sleep 10
+ExecStart=/bin/bash -c "last -wad | mail -s 'server has been rebooted' root"
+RemainAfterExit=yes
+
+[Install]
+WantedBy=multi-user.target
\ No newline at end of file
diff --git a/templates/monit/containers.j2 b/templates/monit/containers.j2
new file mode 100644
index 0000000..bf1c942
--- /dev/null
+++ b/templates/monit/containers.j2
@@ -0,0 +1,7 @@
+{% for container in expanded.monitored.containers -%}
+check program {{ container }}.running
+ with path "/root/lxc-is-running {{ container }}"
+ depends on bridge
+ if status != 0 then alert
+
+{% endfor -%}
\ No newline at end of file
diff --git a/templates/monit/hosts.j2 b/templates/monit/hosts.j2
new file mode 100644
index 0000000..49d40e2
--- /dev/null
+++ b/templates/monit/hosts.j2
@@ -0,0 +1,6 @@
+{% for host in expanded.monitored.hosts -%}
+check host {{ host.container }}.{{ host.port }} with address {{ host.container }}.lxd
+ depends on {{ host.container }}.running
+ if failed port {{ host.port }} protocol http for 2 cycles then alert
+
+{% endfor -%}
\ No newline at end of file
diff --git a/templates/network-manager/50-miaou-resolver b/templates/network-manager/50-miaou-resolver
new file mode 100755
index 0000000..c38d0cd
--- /dev/null
+++ b/templates/network-manager/50-miaou-resolver
@@ -0,0 +1,27 @@
+#!/bin/bash
+
+if [[ "$2" == "up" ]]; then
+
+ ACTIVE_CONNECTION=$(nmcli -g NAME connection show --active | head -n1)
+ ACTIVE_DEVICE=$(nmcli -g DEVICE connection show --active | head -n1)
+ BRIDGE=$(ip addr show lxdbr0 | grep "inet\b" | awk '{print $2}' | cut -d/ -f1)
+ GATEWAY=$(ip route | head -n1 | grep default | cut -d' ' -f3)
+ logger -t NetworkManager:Dispatcher -p info "on $ACTIVE_DEVICE:$ACTIVE_CONNECTION up , change resolver to $BRIDGE,$GATEWAY"
+ nmcli device modify "$ACTIVE_DEVICE" ipv4.dns "$BRIDGE,$GATEWAY"
+
+ if ! grep nameserver /etc/resolv.conf | head -n1 | grep -q "$BRIDGE"; then
+ # sometimes, nmcli generates wrong order for namespace in resolv.conf, therefore forcing connection settings must be applied!
+ logger -t NetworkManager:Dispatcher -p info "on $ACTIVE_DEVICE:$ACTIVE_CONNECTION nameservers wrong order detected, therefore forcing connection settings must be applied"
+ nmcli connection modify "$ACTIVE_CONNECTION" ipv4.ignore-auto-dns yes
+ nmcli connection modify "$ACTIVE_CONNECTION" ipv4.dns "$BRIDGE,$GATEWAY"
+ logger -t NetworkManager:Dispatcher -p info "on $ACTIVE_DEVICE:$ACTIVE_CONNECTION nameservers wrong order detected, connection reloaded now!"
+ nmcli connection up "$ACTIVE_CONNECTION"
+ else
+ logger -t NetworkManager:Dispatcher -p info "on $ACTIVE_DEVICE:$ACTIVE_CONNECTION nameservers look fine"
+ fi
+else
+ if [[ "$2" == "connectivity-change" ]]; then
+ ACTIVE_DEVICE=$(nmcli -g DEVICE connection show --active | head -n1)
+ logger -t NetworkManager:Dispatcher -p info "on $ACTIVE_DEVICE connectivity-change detected"
+ fi
+fi
diff --git a/templates/nftables/lxd.table.j2 b/templates/nftables/lxd.table.j2
new file mode 100644
index 0000000..3f48203
--- /dev/null
+++ b/templates/nftables/lxd.table.j2
@@ -0,0 +1,35 @@
+table inet lxd {
+chain pstrt.lxdbr0 {
+type nat hook postrouting priority srcnat; policy accept;
+
+{%- if target != 'prod' %}
+# BLOCK SMTP PORTS
+tcp dport { 25, 465, 587 } ip saddr {{ firewall.bridge_subnet }} {%- if firewall.container_mail_passthrough %} ip saddr
+!= {{ env.ip_mail_passthrough }} {% endif %} log prefix "Drop SMTP away from container: " drop
+{% endif -%}
+
+ip saddr {{ firewall.bridge_subnet }} ip daddr != {{ firewall.bridge_subnet }} masquerade
+}
+
+chain fwd.lxdbr0 {
+type filter hook forward priority filter; policy accept;
+ip version 4 oifname "lxdbr0" accept
+ip version 4 iifname "lxdbr0" accept
+}
+
+chain in.lxdbr0 {
+type filter hook input priority filter; policy accept;
+iifname "lxdbr0" tcp dport 53 accept
+iifname "lxdbr0" udp dport 53 accept
+iifname "lxdbr0" icmp type { destination-unreachable, time-exceeded, parameter-problem } accept
+iifname "lxdbr0" udp dport 67 accept
+}
+
+chain out.lxdbr0 {
+type filter hook output priority filter; policy accept;
+oifname "lxdbr0" tcp sport 53 accept
+oifname "lxdbr0" udp sport 53 accept
+oifname "lxdbr0" icmp type { destination-unreachable, time-exceeded, parameter-problem } accept
+oifname "lxdbr0" udp sport 67 accept
+}
+}
\ No newline at end of file
diff --git a/templates/nftables/nat.table.j2 b/templates/nftables/nat.table.j2
new file mode 100644
index 0000000..ed1c713
--- /dev/null
+++ b/templates/nftables/nat.table.j2
@@ -0,0 +1,6 @@
+table ip nat {
+ chain prerouting {
+ type nat hook prerouting priority dstnat; policy accept;
+ iif "{{ nftables.wan_interface }}" tcp dport { 80, 443 } dnat to {{ nftables.dmz_ip }}
+ }
+}
diff --git a/templates/nginx/_default.j2 b/templates/nginx/_default.j2
new file mode 100644
index 0000000..ec43ab3
--- /dev/null
+++ b/templates/nginx/_default.j2
@@ -0,0 +1,16 @@
+# DEFAULT SERVER
+
+# redirect any http request to https
+server {
+ listen 80;
+ server_name _;
+ return 301 https://$host$request_uri;
+}
+
+# respond dummy nginx page
+server {
+ listen 443 default_server ssl;
+ include snippets/snakeoil.conf;
+ root /var/www/html;
+ index index.html index.htm index.nginx-debian.html;
+}
diff --git a/templates/nginx/hosts.j2 b/templates/nginx/hosts.j2
new file mode 100644
index 0000000..f3eff54
--- /dev/null
+++ b/templates/nginx/hosts.j2
@@ -0,0 +1,37 @@
+{% for service in expanded.services %}
+server {
+ listen 443 http2 ssl;
+ server_name {{ service.fqdn }};
+
+ {%- if target == 'dev' %}
+ include snippets/snakeoil.conf;
+ {%- else %}
+ ssl_certificate /etc/letsencrypt/live/{{ service.domain }}/fullchain.pem;
+ ssl_certificate_key /etc/letsencrypt/live/{{ service.domain }}/privkey.pem;
+ {%- endif %}
+
+ location / {
+ proxy_pass http://{{ service.container }}:{{ service.port }};
+ {%- if service.app == 'odoo15' %}
+ client_max_body_size 5M;
+ {%- endif %}
+
+ proxy_http_version 1.1;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header Host $http_host;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+
+ {%- if target != 'prod' %}
+ include snippets/banner_{{ target }}.conf;
+ {%- endif %}
+ }
+
+ {%- if service.app == 'odoo15' or service.app == 'odoo12' %}
+ location /longpolling {
+ proxy_pass http://{{ service.container }}:{{ service.port + 1000 }};
+ }
+ {%- endif %}
+}
+{% endfor %}
diff --git a/templates/nginx/snippets/banner_beta.conf b/templates/nginx/snippets/banner_beta.conf
new file mode 100644
index 0000000..3dc09c4
--- /dev/null
+++ b/templates/nginx/snippets/banner_beta.conf
@@ -0,0 +1,67 @@
+proxy_set_header Accept-Encoding "";
+
+subs_filter '