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 '' ' +
+
BETA
+
+ + + +'; diff --git a/templates/nginx/snippets/banner_dev.conf b/templates/nginx/snippets/banner_dev.conf new file mode 100644 index 0000000..4837fab --- /dev/null +++ b/templates/nginx/snippets/banner_dev.conf @@ -0,0 +1,65 @@ +proxy_set_header Accept-Encoding ""; + +subs_filter '' ' +
+
DEV
+
+ + + +';