diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..26ee4e8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +.coverage +.pybuild/ +debian/debhelper-build-stamp +debian/files +debian/python3-opennel-pymanager.debhelper.log +debian/python3-opennel-pymanager.postinst.debhelper +debian/python3-opennel-pymanager.prerm.debhelper +debian/python3-opennel-pymanager.substvars +debian/python3-opennel-pymanager/ +docs/build/ +htmlcov/ +pymanager.egg-info/ +pymanager/__pycache__/ + diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..0c2a91a --- /dev/null +++ b/LICENSE.txt @@ -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. + + Khanat + Copyright (C) 2016 Khaganat + + 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/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..e69de29 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c4012da --- /dev/null +++ b/Makefile @@ -0,0 +1,64 @@ +PYTHON=`which python3` +PYTHONCOVERAGE=`which python3-coverage` +DESTDIR=/ +BUILDIR=$(CURDIR)/debian/pymanager +PROJECT=pymanager +VERSION=1.0.0 +OMIT_COVERGAGE=--omit=/usr/lib/python3/*,tests/* + +all: + @echo "make sdist - Create source package" + @echo "make bdist - Create package" + @echo "make test - Test" + @echo "make coverage - coverage" + @echo "make htmldoc - generate html doc (out : doc/build)" + @echo "make install - Install on local system" + @echo "make builddeb - Generate a deb package" + @echo "make clean - Get rid of scratch and byte files" + +sdist: + $(PYTHON) setup.py sdist $(COMPILE) + +bdist: + $(PYTHON) setup.py bdist $(COMPILE) + +test: + $(PYTHON) setup.py test + +coverage: + $(PYTHONCOVERAGE) erase + $(PYTHONCOVERAGE) run -a ${OMIT_COVERGAGE} pymanager/certificate.py --version + $(PYTHONCOVERAGE) run -a ${OMIT_COVERGAGE} tests/test_certificate.py + $(PYTHONCOVERAGE) run -a ${OMIT_COVERGAGE} pymanager/manager.py --version + $(PYTHONCOVERAGE) run -a ${OMIT_COVERGAGE} tests/test_manager.py + $(PYTHONCOVERAGE) run -a ${OMIT_COVERGAGE} pymanager/client.py --version + $(PYTHONCOVERAGE) run -a ${OMIT_COVERGAGE} tests/test_client.py + $(PYTHONCOVERAGE) run -a ${OMIT_COVERGAGE} pymanager/password.py --version + $(PYTHONCOVERAGE) run -a ${OMIT_COVERGAGE} tests/test_password.py + $(PYTHONCOVERAGE) report # ${OMIT_COVERGAGE} + $(PYTHONCOVERAGE) html + +htmldoc: + sphinx-build -b html docs/source docs/build + +install: + $(PYTHON) setup.py install --root $(DESTDIR) $(COMPILE) + +builddeb: + # build the source package in the parent directory + # then rename it to project_version.orig.tar.gz + $(PYTHON) setup.py sdist $(COMPILE) --dist-dir=../ + rename -v -f 's/$(PROJECT)-(.*)\.tar\.gz/$(PROJECT)_$$1\.orig\.tar\.gz/' ../* + # build the package + dpkg-buildpackage -i -I -rfakeroot + +clean: + $(PYTHON) setup.py clean + $(MAKE) -f $(CURDIR)/debian/rules clean + rm -rf build/ MANIFEST + rm -rf docs/build/ + rm -rf dist/ + rm -rf .coverage + rm -rf htmlcov + rm -rf pymanager/__pycache__ + find . -name '*.pyc' -delete diff --git a/README.md b/README.md index e69de29..e8de064 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,50 @@ +OpenNel pyManager project +========================= + +This projects does aim to make tools to manipulate the MMORPG Khanat. + +Khanat is open source and released under the terms of the GNU Affero General Public License 3.0 (GNU/AGPLv3) for the source code and the Creative Commons Attributions-ShareAlike 3.0 (CC-BY-SA) for the art assets. Which means you can create your own game using Khanat OpenNel, for more information on doing so check out Creating Your Own Game Using Khanat OpenNel. + + +https://khaganat.net/wikhan/fr:collabo_pymanager + + +Prepare our environment +======================= + +# Install + + apt-get install python3 python3-setuptools python3-virtualenv python3-stdeb python3-all python3-coverage python3-pep8 python3-sphinx python3-pip graphviz python3-bcrypt + apt-get install autogen autoconf automake fakeroot + +# Check + + cd opennel-pymanager + make + +# Test + + cd opennel-pymanager + make test + +# Coverage + + cd opennel-pymanager + make coverage + # result at + ls htmlcov/ + +# Generate Html Documentation : + + cd opennel-pymanager + make htmldoc + # result at + ls docs/build/ + +# Debian package : + + cd opennel-pymanager + make builddeb + # result at + ls ../python3-opennel-pymanager_*_all.deb + diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..6acf153 --- /dev/null +++ b/README.rst @@ -0,0 +1,9 @@ +OpenNel pyManager project +========================= + +This projects does aim to make tools to manipulate the MMORPG Khanat. + +Khanat is open source and released under the terms of the GNU Affero General Public License 3.0 (GNU/AGPLv3) for the source code and the Creative Commons Attributions-ShareAlike 3.0 (CC-BY-SA) for the art assets. Which means you can create your own game using Khanat OpenNel, for more information on doing so check out Creating Your Own Game Using Khanat OpenNel. + + +https://khaganat.net/wikhan/fr:collabo_pymanager diff --git a/conf/khaganat.cfg b/conf/khaganat.cfg new file mode 100644 index 0000000..e9193f9 --- /dev/null +++ b/conf/khaganat.cfg @@ -0,0 +1,246 @@ +# +# Configuration process khaganat +# +# Copyright (C) 2017 AleaJactaEst +# +# 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 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +############################## +############################## +# Global parameter +############################## +############################## +[manage] +# Define port listen (default 8000) +port = 8000 + +# key +keyfile = /home/gameserver/ca/appli/private/serverkey.pem + +# certificate +certfile = /home/gameserver/ca/appli/certs/servercert.pem + +# certification to check signature +ca_cert = /home/gameserver/ca/appli/certs/cachaincert.pem + +# address listen (default all port) +address = + +############################## +############################## +# List all program we manage # +############################## +############################## + +############################## +# Admin Executor Service +############################## +[aes] +# command to launch the program +command = ryzom_admin_service -A/home/gameserver/khanat/server -C/home/gameserver/khanat/server -L/home/gameserver/log/khanat --nobreak --fulladminname=admin_executor_service --shortadminname=AES +# Path : where this program is launched +path = /home/gameserver/khanat/server/ +# size buffer log for each program launched (number line stdout) +logsize = 1000 +# buffer size (define value bufsize on subprocess.Popen, this buffer is use before read by manager) +bufsize = 100 + +############################## +# bms_master : backup_service +############################## +[bms_master] +# command to launch the program +command = ryzom_backup_service -A/home/gameserver/khanat/server -C/home/gameserver/khanat/server -L/home/gameserver/khanat/server/log --nobreak --writepid -P49990 +# Path : where this program is launched +path = /home/gameserver/khanat/server/ +# size buffer log for each program launched (number line stdout) +logsize = 1000 + +#[bms_pd_master] +# # command to launch the program +# command = ryzom_backup_service -A/home/gameserver/khanat/server -C/home/gameserver/khanat/server -L/home/gameserver/khanat/server/log --nobreak --writepid -P49992 +# # Path : where this program is launched +# path = /home/gameserver/khanat/server/ +# # size buffer log for each program launched (number line stdout) +# logsize = 1000 + +############################## +# egs : entities_game_service +############################## +[egs] +# command to launch the program +command = ryzom_entities_game_service -A/home/gameserver/khanat/server -C/home/gameserver/khanat/server -L/home/gameserver/khanat/server/log --nobreak --writepid +# Path : where this program is launched +path = /home/gameserver/khanat/server/ +# size buffer log for each program launched (number line stdout) +logsize = 1000 + +############################## +# gpms : gpm_service +############################## +[gpms] +# command to launch the program +command = ryzom_gpm_service -A/home/gameserver/khanat/server -C/home/gameserver/khanat/server -L/home/gameserver/khanat/server/log --nobreak --writepid +# Path : where this program is launched +path = /home/gameserver/khanat/server/gpms +# size buffer log for each program launched (number line stdout) +logsize = 1000 + +############################## +# ios : input_output_service +############################## +[ios] +# command to launch the program +command = ryzom_ios_service -A/home/gameserver/khanat/server -C/home/gameserver/khanat/server -L/home/gameserver/khanat/server/log --nobreak --writepid +# Path : where this program is launched +path = /home/gameserver/khanat/server/ +# size buffer log for each program launched (number line stdout) +logsize = 1000 + +############################## +# rns : naming_service +############################## +[rns] +# command to launch the program +command = ryzom_naming_service -A/home/gameserver/khanat/server -C/home/gameserver/khanat/server -L/home/gameserver/khanat/server/log --nobreak --writepid +# Path : where this program is launched +path = /home/gameserver/khanat/server/ +# size buffer log for each program launched (number line stdout) +logsize = 1000 + +############################## +# rws : welcome_service +############################## +[rws] +# command to launch the program +command = ryzom_welcome_service -A/home/gameserver/khanat/server -C/home/gameserver/khanat/server -L/home/gameserver/khanat/server/log --nobreak --writepid +# Path : where this program is launched +path = /home/gameserver/khanat/server/ +# size buffer log for each program launched (number line stdout) +logsize = 1000 + +############################## +# ts : tick_service +############################## +[ts] +# command to launch the program +command = ryzom_tick_service -A/home/gameserver/khanat/server -C/home/gameserver/khanat/server -L/home/gameserver/khanat/server/log --nobreak --writepid +# Path : where this program is launched +path = /home/gameserver/khanat/server/ +# size buffer log for each program launched (number line stdout) +logsize = 1000 + +############################## +# ms : mirror_service +############################## +[ms] +# command to launch the program +command = ryzom_mirror_service -A/home/gameserver/khanat/server -C/home/gameserver/khanat/server -L/home/gameserver/khanat/server/log --nobreak --writepid +# Path : where this program is launched +path = /home/gameserver/khanat/server/ +# size buffer log for each program launched (number line stdout) +logsize = 1000 + +############################## +# ais_newbyland : ai_service +############################## +[ais_newbyland] +# command to launch the program +command = ryzom_ai_service -A/home/gameserver/khanat/server -C/home/gameserver/khanat/server -L/home/gameserver/khanat/server/log --nobreak --writepid -mCommon:Newbieland:Post +# Path : where this program is launched +path = /home/gameserver/khanat/server/ +# size buffer log for each program launched (number line stdout) +logsize = 1000 + +############################## +# mfs : mail_forum_service +############################## +[mfs] +# command to launch the program +command = ryzom_mail_forum_service -A/home/gameserver/khanat/server -C/home/gameserver/khanat/server -L/home/gameserver/khanat/server/log --nobreak --writepid +# Path : where this program is launched +path = /home/gameserver/khanat/server/ +# size buffer log for each program launched (number line stdout) +logsize = 1000 + +############################## +# su : shard_unifier_service +############################## +[su] +# command to launch the program +command = ryzom_shard_unifier_service -A/home/gameserver/khanat/server -C/home/gameserver/khanat/server -L/home/gameserver/khanat/server/log --nobreak --writepid +# Path : where this program is launched +path = /home/gameserver/khanat/server/ +# size buffer log for each program launched (number line stdout) +logsize = 1000 + +############################## +# fes : frontend_service +############################## +[fes] +# command to launch the program +command = ryzom_frontend_service -A/home/gameserver/khanat/server -C/home/gameserver/khanat/server -L/home/gameserver/khanat/server/log --nobreak --writepid +# Path : where this program is launched +path = /home/gameserver/khanat/server/ +# size buffer log for each program launched (number line stdout) +logsize = 1000 + +############################## +# sbs : session_browser_server +############################## +[sbs] +# command to launch the program +command = ryzom_session_browser_service -A/home/gameserver/khanat/server -C/home/gameserver/khanat/server -L/home/gameserver/khanat/server/log --nobreak --writepid +# Path : where this program is launched +path = /home/gameserver/khanat/server/ +# size buffer log for each program launched (number line stdout) +logsize = 1000 + +############################## +# lgs : logger_service +############################## +[lgs] +# command to launch the program +command = ryzom_logger_service -A/home/gameserver/khanat/server -C/home/gameserver/khanat/server -L/home/gameserver/khanat/server/log --nobreak --writepid +# Path : where this program is launched +path = /home/gameserver/khanat/server/ +# size buffer log for each program launched (number line stdout) +logsize = 1000 + +# [mos] +# # command to launch the program +# command = ryzom_monitor_service -A/home/gameserver/khanat/server -C/home/gameserver/khanat/server -L/home/gameserver/khanat/server/log --nobreak --writepid +# # Path : where this program is launched +# path = /home/gameserver/khanat/server/ +# # size buffer log for each program launched (number line stdout) +# logsize = 1000 + +# [pdss] +# # command to launch the program +# command = ryzom_pd_support_service -A/home/gameserver/khanat/server -C/home/gameserver/khanat/server -L/home/gameserver/khanat/server/log --nobreak --writepid +# # Path : where this program is launched +# path = /home/gameserver/khanat/server/ +# # size buffer log for each program launched (number line stdout) +# logsize = 1000 + +############################## +# ras : admin_service +############################## +[ras] +# command to launch the program +command = ryzom_admin_service --fulladminname=admin_service --shortadminname=AS -A/home/gameserver/khanat/server -C/home/gameserver/khanat/server -L/home/gameserver/khanat/server/log --nobreak --writepid +# Path : where this program is launched +path = /home/gameserver/khanat/server/ +# size buffer log for each program launched (number line stdout) +logsize = 1000 diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 0000000..38317fe --- /dev/null +++ b/debian/changelog @@ -0,0 +1,5 @@ +opennel-pymanager (1.0.0) UNRELEASED; urgency=medium + + * Initial release. (Closes: #XXXXXX) + + -- aleajactaest Wed, 29 Nov 2017 21:57:08 +0000 diff --git a/debian/compat b/debian/compat new file mode 100644 index 0000000..ec63514 --- /dev/null +++ b/debian/compat @@ -0,0 +1 @@ +9 diff --git a/debian/control b/debian/control new file mode 100644 index 0000000..40473ec --- /dev/null +++ b/debian/control @@ -0,0 +1,15 @@ +Source: opennel-pymanager +Section: shells +Priority: optional +Maintainer: AleaJactaEst +Build-Depends: debhelper (>=9.20150101), python3 (>= 3.4) +XS-Python-Version: >=3.4 +Standards-Version: 3.9.6.1 + +Package: python3-opennel-pymanager +Architecture: all +Homepage: https://khaganat.net/ +XB-Python-Version: >=3.4 +Depends: python3 (>= 3.4) +Description: OpenNel pyManager project + This projects does aim to make tools to manipulate the MMORPG Khanat. diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 0000000..68b65be --- /dev/null +++ b/debian/copyright @@ -0,0 +1,29 @@ +This package was debianized by AleaJactaEst + on Wed, 29 Nov 2017 21:38:10 +0000 + +It was downloaded from https://git.khaganat.net/khaganat/mmorpg_khanat/opennel-pymanager + +Upstream Author: + AleaJactaEst + + +Files: * +Copyright: + 2008-2017, AleaJactaEst +License: AGPL + + +License: AGPL + 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 . + diff --git a/debian/opennel-pymanager.postinst.debhelper b/debian/opennel-pymanager.postinst.debhelper new file mode 100644 index 0000000..61aeb62 --- /dev/null +++ b/debian/opennel-pymanager.postinst.debhelper @@ -0,0 +1,7 @@ + +# Automatically added by dhpython: +if which py3compile >/dev/null 2>&1; then + py3compile -p opennel-pymanager +fi + +# End automatically added section diff --git a/debian/opennel-pymanager.prerm.debhelper b/debian/opennel-pymanager.prerm.debhelper new file mode 100644 index 0000000..c70b571 --- /dev/null +++ b/debian/opennel-pymanager.prerm.debhelper @@ -0,0 +1,10 @@ + +# Automatically added by dhpython: +if which py3clean >/dev/null 2>&1; then + py3clean -p opennel-pymanager +else + dpkg -L opennel-pymanager | perl -ne 's,/([^/]*)\.py$,/__pycache__/\1.*, or next; unlink $_ or die $! foreach glob($_)' + find /usr/lib/python3/dist-packages/ -type d -name __pycache__ -empty -print0 | xargs --null --no-run-if-empty rmdir +fi + +# End automatically added section diff --git a/debian/opennel-pymanager.substvars b/debian/opennel-pymanager.substvars new file mode 100644 index 0000000..8f21610 --- /dev/null +++ b/debian/opennel-pymanager.substvars @@ -0,0 +1,2 @@ +python3:Depends=python3, python3:any (>= 3.3.2-2~) +misc:Depends= diff --git a/debian/rules b/debian/rules new file mode 100755 index 0000000..0daa215 --- /dev/null +++ b/debian/rules @@ -0,0 +1,12 @@ +#!/usr/bin/make -f +# See debhelper(7) (uncomment to enable) +# output every command that modifies files on the build system. +#DH_VERBOSE = 1 +DPKG_EXPORT_BUILDFLAGS = 1 + + +# main packaging script based on dh7 syntax +%: + dh $@ --with python3 --buildsystem=pybuild + +override_dh_auto_test: diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..0e2e337 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,177 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = build + +# User-friendly check for sphinx-build +ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) +$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) +endif + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source + +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " xml to make Docutils-native XML files" + @echo " pseudoxml to make pseudoxml-XML files for display purposes" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +clean: + rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/opennel-pymanager.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/opennel-pymanager.qhc" + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/opennel-pymanager" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/opennel-pymanager" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +latexpdfja: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through platex and dvipdfmx..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." + +xml: + $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml + @echo + @echo "Build finished. The XML files are in $(BUILDDIR)/xml." + +pseudoxml: + $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml + @echo + @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 0000000..969b8c0 --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,266 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# opennel-pymanager documentation build configuration file, created by +# sphinx-quickstart on Thu Nov 30 21:55:34 2017. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys +import os + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +sys.path.insert(0, os.path.abspath('../../.')) +print("******", os.path.abspath('../../.')) + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.doctest', + 'sphinx.ext.mathjax', + 'sphinx.ext.viewcode', + 'sphinx.ext.graphviz', +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = 'opennel-pymanager' +copyright = '2017, AleaJactaEst' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '1.0.0' +# The full version, including alpha/beta/rc tags. +release = '1.0.0' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = [] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +#keep_warnings = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'default' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +#html_extra_path = [] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'opennel-pymanagerdoc' + + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { +# The paper size ('letterpaper' or 'a4paper'). +#'papersize': 'letterpaper', + +# The font size ('10pt', '11pt' or '12pt'). +#'pointsize': '10pt', + +# Additional stuff for the LaTeX preamble. +#'preamble': '', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + ('index', 'opennel-pymanager.tex', 'opennel-pymanager Documentation', + 'AleaJactaEst', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('index', 'opennel-pymanager', 'opennel-pymanager Documentation', + ['AleaJactaEst'], 1) +] + +# If true, show URL addresses after external links. +#man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ('index', 'opennel-pymanager', 'opennel-pymanager Documentation', + 'AleaJactaEst', 'opennel-pymanager', 'One line description of project.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +#texinfo_appendices = [] + +# If false, no module index is generated. +#texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +#texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +#texinfo_no_detailmenu = False diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 0000000..a5a47ff --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,43 @@ +.. opennel-pymanager documentation master file, created by + sphinx-quickstart on Thu Nov 30 21:55:34 2017. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to opennel-pymanager's documentation! +============================================= + +Contents: + +.. toctree:: + :maxdepth: 2 + +Certificate +----------- + +.. automodule:: pymanager.certificate + :members: + :undoc-members: + :show-inheritance: + +PyManager +--------- + +.. automodule:: pymanager.manager + :members: + :undoc-members: + :show-inheritance: + +PyClient +--------- +.. automodule:: pymanager.client + :members: + :undoc-members: + :show-inheritance: + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + diff --git a/pymanager/__init__.py b/pymanager/__init__.py new file mode 100644 index 0000000..474073b --- /dev/null +++ b/pymanager/__init__.py @@ -0,0 +1,20 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# +# intialize module and define version +# Copyright (C) 2017 AleaJactaEst +# +# 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 . + +__version__ = '1.0.0' diff --git a/pymanager/certificate.py b/pymanager/certificate.py new file mode 100755 index 0000000..79e4451 --- /dev/null +++ b/pymanager/certificate.py @@ -0,0 +1,557 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# +# create certificate (use for test) +# Copyright (C) 2017 AleaJactaEst +# +# 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 . + +""" + +Generate all certificates : + +.. graphviz:: + + digraph Certificate { + "Root Certificate" -> "Application Certificate"; + "Application Certificate" -> "Server Certificate"; + "Application Certificate" -> " Client Certificate"; + } + +Detail: + * Root certificate : our global certificate + * Application certification : our application certificate (use to sign child certificat) + * Server certificate : certificate for server side + * Client certificate : certificate for client side + +""" + +import argparse +import logging +import logging.config +import os +import stat +import subprocess +import sys + + +class Certificate: + """ class to generate all certificate + + :param str openssl: localization openssl program (absolut path) + :param str workdir_cert_root: root directory + :param str workdir_cert_appli: application directory + :param str passroot: root password + :param str passappli: application password + :param str country_name: your contry name (2 characters) + :param str state_or_province_name: The name of a user's state or province. + :param str locality_name: Represents the name of a locality, such as a town or city. + :param str organization_name: The name of the company or organization. + :param str common_name: The name that represents an object. Used to perform searches. + :param int size_root: root key size + :param int size_appli: application key size + :param int size_child: child key size (server & client) + """ + + def __init__(self, + openssl, + workdir_cert_root, + workdir_cert_appli, + passroot, + passappli, + country_name, + state_or_province_name, + locality_name, + organization_name, + common_name, + size_root, + size_appli, + size_child): + """ Constructor + + :param str openssl: localization openssl program (absolut path) + :param str workdir_cert_root: root directory + :param str workdir_cert_appli: application directory + :param str passroot: root password + :param str passappli: application password + :param str country_name: your contry name (2 characters) + :param str state_or_province_name: The name of a user's state or province. + :param str locality_name: Represents the name of a locality, such as a town or city. + :param str organization_name: The name of the company or organization. + :param str common_name: The name that represents an object. Used to perform searches. + :param int size_root: root key size + :param int size_appli: application key size + :param int size_child: child key size (server & client) + """ + self.workdir_cert_root = os.path.abspath(workdir_cert_root) + self.workdir_cert_appli = os.path.abspath(workdir_cert_appli) + self.openssl = openssl + self.passroot = passroot + self.passappli = passappli + self.country_name = country_name + self.state_or_province_name = state_or_province_name + self.locality_name = locality_name + self.organization_name = organization_name + self.common_name = common_name + self.configroot = os.path.join(self.workdir_cert_root, 'openssl.cnf') + self.configappli = os.path.join(self.workdir_cert_appli, 'openssl.cnf') + self.size_root = size_root + self.size_appli = size_appli + self.size_child = size_child + + def directory_create(self, dirpath): + """ Create directory if not exist + + :param str dirpath: absolut path + :raises FileNotFoundError: if dirpath is bad path + :raises TypeError: if dirpath is None + """ + if not os.path.exists(dirpath): + os.makedirs(dirpath) + + def send_command_openssl(self, args): + """ Execute Openssl command + + :param str args: argument send to openssl + :raises error: if openssl return error + """ + command = '%s %s' % (self.openssl, args) + logging.debug("command:%s" % command) + code = subprocess.call(command.split(), stdout=sys.stdout, + stderr=sys.stderr) + if code != 0: + logging.error("Command '%s' return code:%d" % (command, code)) + raise RuntimeError + + def write_config_openssl(self, config, dirpath, certfile, keyfile): + """ Create openssl configuration + + :param str config: configuration file (output) + :param str dirpath: confiuration path + :param str certfile: certificate filename + :param str keyfile: key filename + """ + with open(config, 'w') as f: + f.write('[ ca ]\n' + 'default_ca = Cert_default\n' + '\n[ Cert_default ]\n' + 'dir = %s\n' + 'certs = $dir/certs\n' + 'crl_dir = $dir/crl\n' + 'database = $dir/index.txt\n' + 'new_certs_dir = $dir/newcerts\n' + 'certificate = $dir/certs/%s\n' + 'serial = $dir/serial\n' + 'crlnumber = $dir/crlnumber\n' + 'crl = $dir/crl/%s\n' + 'private_key = $dir/private/%s\n' + 'RANDFILE = $dir/private/.rand\n' + 'name_opt = ca_default\n' + 'cert_opt = ca_default\n' + 'default_days = 390\n' + 'default_crl_days = 30\n' + 'default_md = sha256\n' + 'preserve = no\n' + 'policy = policy_match\n' + 'crl_extensions = crl_ext\n' + 'unique_subject = no\n' + '\n[ policy_match ]\n' + 'countryName = match\n' + 'stateOrProvinceName = match\n' + 'organizationName = match\n' + 'organizationalUnitName = optional\n' + 'commonName = supplied\n' + 'emailAddress = optional\n' + '\n[ req ]\n' + 'default_bits = 2048\n' + 'distinguished_name = req_distinguished_name\n' + 'x509_extensions = v3_ca\n' + 'string_mask = utf8only\n' + 'unique_subject = no\n' + '\n[ server_cert ]\n' + 'basicConstraints=CA:false\n' + 'nsComment = "OpenSSL Generated Certificate"\n' + 'subjectKeyIdentifier=hash\n' + 'authorityKeyIdentifier=keyid,issuer:always\n' + 'keyUsage = critical, digitalSignature, keyEncipherment\n' + 'nsCertType = server\n' + '\n[ client_cert ]\n' + 'basicConstraints=CA:false\n' + 'nsComment = "OpenSSL Generated Certificate"\n' + 'subjectKeyIdentifier=hash\n' + 'authorityKeyIdentifier=keyid:always,issuer\n' + 'keyUsage = critical, nonRepudiation, ' + 'digitalSignature, keyEncipherment\n' + 'nsCertType = client\n' + '\n[ v3_ca ]\n' + 'basicConstraints=critical, CA:true\n' + 'nsComment = "OpenSSL Generated Certificate"\n' + 'subjectKeyIdentifier=hash\n' + 'authorityKeyIdentifier=keyid:always,issuer\n' + 'keyUsage = critical, digitalSignature, ' + 'cRLSign, keyCertSign\n' + '\n[ v3_application_ca ]\n' + 'basicConstraints=critical, CA:true, pathlen:0\n' + 'nsComment = "OpenSSL Generated Certificate"\n' + 'subjectKeyIdentifier=hash\n' + 'authorityKeyIdentifier=keyid:always,issuer\n' + 'keyUsage = critical, digitalSignature, ' + 'cRLSign, keyCertSign\n' + '\n[ req_distinguished_name ]\n' + 'countryName = Country Name (2 letter code)\n' + 'countryName_default = FR\n' + 'countryName_min = 2\n' + 'countryName_max = 2\n' + 'stateOrProvinceName = State or Province Name (full name)\n' + 'stateOrProvinceName_default = France\n' + 'localityName = Locality Name (eg, city)\n' + '0.organizationName = Organization Name (eg, company)\n' + '0.organizationName_default = Khanat\n' + 'organizationalUnitName = Organizational ' + 'Unit Name (eg, section)\n' + 'commonName = Common Name (e.g. server FQDN or YOUR name)\n' + 'commonName_max = 64\n' + 'emailAddress = Email Address\n' + 'emailAddress_max = 64\n' + '\n[ crl_ext ]\n' + 'authorityKeyIdentifier=keyid:always\n' + % (dirpath, certfile, 'cacrl.pem', keyfile)) + + def create_root_certificate(self): + """ Create Root certificate """ + logging.info("Create Root Certificate") + # Create directory + certfilename = 'cacert.pem' + keyfilename = 'cakey.pem' + self.directory_create(self.workdir_cert_root) + self.directory_create(os.path.join(self.workdir_cert_root, 'certs')) + private = os.path.join(self.workdir_cert_root, 'private') + self.directory_create(private) + os.chmod(private, stat.S_IEXEC | stat.S_IWUSR | stat.S_IRUSR) + self.directory_create(os.path.join(self.workdir_cert_root, 'crl')) + self.directory_create(os.path.join(self.workdir_cert_root, 'newcerts')) + # Create files use in CA + index = os.path.join(self.workdir_cert_root, 'index.txt') + with open(index, 'w') as f: + f.write('') + serial = os.path.join(self.workdir_cert_root, 'serial') + with open(serial, 'w') as f: + f.write('10') + # Create configuration + self.write_config_openssl(self.configroot, self.workdir_cert_root, + certfilename, keyfilename) + # Create private key for our CA + keyfile = os.path.join(self.workdir_cert_root, 'private', keyfilename) + self.send_command_openssl('genrsa -aes256 -out %s -passout pass:%s %d' % + (keyfile, self.passroot, self.size_root)) + os.chmod(keyfile, stat.S_IEXEC | stat.S_IWUSR | stat.S_IRUSR) + # Create certificate for our CA + certfile = os.path.join(self.workdir_cert_root, 'certs', certfilename) + self.send_command_openssl('req ' + '-config %s ' + '-key %s ' + '-passin pass:%s ' + '-new ' + '-x509 ' + '-days 390 ' + '-sha256 ' + '-extensions v3_ca ' + '-out %s ' + '-subj /C=%s/ST=%s/L=%s/O=%s/CN=%s' % + (self.configroot, + keyfile, + self.passroot, + certfile, + self.country_name, + self.state_or_province_name, + self.locality_name, + self.organization_name, + self.common_name)) + os.chmod(certfile, stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH) + + # Check certificate + self.send_command_openssl('x509 -noout -text -in ' + certfile) + + def create_application_certificate(self): + """ create application certificate """ + logging.info("Create Application Certificate") + certfilename = 'applicert.pem' + csrfilename = 'applicsr.pem' + keyfilename = 'applikey.pem' + # Create directory + self.directory_create(self.workdir_cert_appli) + self.directory_create(os.path.join(self.workdir_cert_appli, 'certs')) + private = os.path.join(self.workdir_cert_appli, 'private') + self.directory_create(private) + os.chmod(private, stat.S_IEXEC | stat.S_IWUSR | stat.S_IRUSR) + self.directory_create(os.path.join(self.workdir_cert_appli, 'crl')) + self.directory_create(os.path.join(self.workdir_cert_appli, 'newcerts')) + self.directory_create(os.path.join(self.workdir_cert_appli, 'csr')) + # Create files use in CA + index = os.path.join(self.workdir_cert_appli, 'index.txt') + with open(index, 'w') as f: + f.write('') + serial = os.path.join(self.workdir_cert_appli, 'serial') + with open(serial, 'w') as f: + f.write('10') + serial = os.path.join(self.workdir_cert_appli, 'crlnumber') + with open(serial, 'w') as f: + f.write('10') + # Create configuration + self.write_config_openssl(self.configappli, self.workdir_cert_appli, + certfilename, keyfilename) + # Create private key for our Application + keyfile = os.path.join(self.workdir_cert_appli, 'private', keyfilename) + self.send_command_openssl('genrsa -aes256 -out %s -passout pass:%s %d' % + (keyfile, + self.passappli, + self.size_appli)) + os.chmod(keyfile, stat.S_IEXEC | stat.S_IWUSR | stat.S_IRUSR) + # Create certificate for our CA + csrfile = os.path.join(self.workdir_cert_appli, 'csr', csrfilename) + self.send_command_openssl('req ' + '-config %s ' + '-new ' + '-sha256 ' + '-passin pass:%s ' + '-key %s ' + '-out %s ' + '-subj /C=%s/ST=%s/L=%s/O=%s/CN=%s' % + (self.configappli, + self.passappli, + keyfile, + csrfile, + self.country_name, + self.state_or_province_name, + self.locality_name, + self.organization_name, + self.common_name)) + os.chmod(csrfile, stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH) + certfile = os.path.join(self.workdir_cert_appli, 'certs', certfilename) + # Sign certificate + self.send_command_openssl('ca ' + '-config %s ' + '-extensions v3_application_ca ' + '-days 390 ' + '-notext ' + '-md sha256 ' + '-passin pass:%s ' + '-in %s ' + '-batch ' + '-out %s ' % (self.configroot, + self.passroot, + csrfile, + certfile)) + os.chmod(csrfile, stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH) + self.send_command_openssl('x509 -noout -text -in ' + certfile) + certcafilename = os.path.join(self.workdir_cert_root, 'certs', + 'cacert.pem') + self.send_command_openssl('verify -CAfile %s %s' % (certcafilename, + certfile)) + # concat applicert & cacert + cachainfile = os.path.join(self.workdir_cert_appli, 'certs', + 'cachaincert.pem') + with open(cachainfile, 'w') as outfp: + with open(certfile, 'r') as infp: + outfp.write(infp.read()) + with open(certcafilename, 'r') as infp: + outfp.write(infp.read()) + + def create_child_certificate(self, childname, extension): + """ create child certificate + + :param str childname: prefix key/certificate + :param str extension: Extension section (override value in config file) + """ + keyfilename = childname + "key.pem" + csrfilename = childname + "csr.pem" + certfilename = childname + "cert.pem" + keyfile = os.path.join(self.workdir_cert_appli, 'private', + keyfilename) + self.send_command_openssl('genrsa -out %s %d' % (keyfile, + self.size_child)) + csrfile = os.path.join(self.workdir_cert_appli, 'csr', csrfilename) + self.send_command_openssl('req ' + '-config %s ' + '-new ' + '-sha256 ' + '-key %s ' + '-out %s ' + '-subj /C=%s/ST=%s/L=%s/O=%s/CN=%s' % + (self.configappli, + keyfile, + csrfile, + self.country_name, + self.state_or_province_name, + self.locality_name, + self.organization_name, + self.common_name)) + certfile = os.path.join(self.workdir_cert_appli, 'certs', certfilename) + # Sign certificate + self.send_command_openssl('ca ' + '-config %s ' + '-extensions %s ' + '-days 390 ' + '-notext ' + '-md sha256 ' + '-passin pass:%s ' + '-in %s ' + '-batch ' + '-out %s ' % (self.configappli, + extension, + self.passappli, + csrfile, + certfile)) + self.send_command_openssl('x509 -noout -text -in ' + certfile) + certcafilename = os.path.join(self.workdir_cert_appli, 'certs', + 'cachaincert.pem') + self.send_command_openssl('verify -CAfile %s %s' % (certcafilename, + certfile)) + + +def root(filelog, + loglevel, + show_log_console, + workdir_cert_root, + workdir_cert_appli, + openssl, + size_root, + size_appli, size_child, + passroot, + passappli, + country_name, + state_or_province_name, + locality_name, + organization_name, + common_name): + """ Generate all certificate + + :param str filelog: file log + :param loglevel: level message + :type loglevel: DEBUG, INFO, WARNING or ERROR + :param str show_log_console: show message in console (stdout) + :param str workdir_cert_root: root directory + :param str workdir_cert_appli: application directory + :param str openssl: localization openssl program (absolut path) + :param str size_root: root key size + :param str size_appli: application key size + :param str size_child: child key size (server & client) + :param str passroot: root password + :param str passappli: application password + :param str country_name: your contry name (2 characters) + :param str state_or_province_name: The name of a user's state or province. + :param str locality_name: Represents the name of a locality, such as a town or city. + :param str organization_name: The name of the company or organization. + :param str common_name: The name that represents an object. Used to perform searches. + """ + # Manage log + logging.getLogger('logging') + numeric_level = getattr(logging, loglevel.upper(), None) + if not isinstance(numeric_level, int): + raise ValueError('Invalid log level: ' + loglevel) + handlers = [] + if show_log_console: + handlers.append(logging.StreamHandler()) + if filelog: + handlers.append(logging.FileHandler(filelog.name)) + logging.basicConfig(handlers=handlers, + level=numeric_level, + format='%(asctime)s %(levelname)s [pid:%(process)d] \ +[%(funcName)s:%(lineno)d] %(message)s') + + certicate = Certificate(openssl, + workdir_cert_root, + workdir_cert_appli, + passroot, passappli, + country_name, + state_or_province_name, + locality_name, + organization_name, + common_name, + size_root, + size_appli, + size_child) + logging.info("Generate CA certificate") + certicate.create_root_certificate() + logging.info("Generate Application certificate") + certicate.create_application_certificate() + logging.info("Generate Server certificate") + certicate.create_child_certificate('server', 'server_cert') + logging.info("Generate Client certificate") + certicate.create_child_certificate('client', 'client_cert') + logging.info("Certifcate generated") + + +def main(arguments=sys.argv[1:]): + """ Main function + + :param list args: root password + """ + parser = argparse.ArgumentParser(description='Create certificate ' + '(root, application, server & client)') + parser.add_argument('--version', action='version', version='%(prog)s 1.0') + parser.add_argument('--openssl', default='openssl', help='binary openssl') + parser.add_argument('-c', '--workdir-cert-root', default='ca', + help='workdir Root certificate') + parser.add_argument('-a', '--workdir-cert-appli', default='ca/appli', + help='workdir Application certificate') + parser.add_argument('--show-log-console', action='store_true', + help='show message in console', default=False) + parser.add_argument('--filelog', type=argparse.FileType('wt'), + default=None, help='log file') + parser.add_argument('--log', + default='INFO', + help='log level [DEBUG, INFO, WARNING, ERROR]') + parser.add_argument('--size_root', type=int, default=4096, + help='Define size key for CA certificate') + parser.add_argument('--size_appli', type=int, default=4096, + help='Define size key for Application certificate') + parser.add_argument('--size_child', type=int, default=4096, + help='Define size key for Child certificate') + parser.add_argument('--passroot', default='OpenNelCA9439', + help='define password for Root certificate') + parser.add_argument('--passappli', default='OpenNelAPPLI1097', + help='define password for Application certificate') + parser.add_argument('--country_name', default='FR', + help='countryName for certicate') + parser.add_argument('--state_or_province_name', default='France', + help='stateOrProvinceName for certicate') + parser.add_argument('--locality_name', default='Paris', + help='localityName for certicate') + parser.add_argument('--organization_name', default='khanat', + help='organizationName for certicate') + parser.add_argument('--common_name', default='khanat', + help='commonName for certicate') + print("--") + args = parser.parse_args(arguments) + print("--") + root(filelog=args.filelog, + loglevel=args.log, + show_log_console=args.show_log_console, + workdir_cert_root=args.workdir_cert_root, + workdir_cert_appli=args.workdir_cert_appli, + openssl=args.openssl, + size_root=args.size_root, + size_appli=args.size_appli, + size_child=args.size_child, + passroot=args.passroot, + passappli=args.passappli, + country_name=args.country_name, + state_or_province_name=args.state_or_province_name, + locality_name=args.locality_name, + organization_name=args.organization_name, + common_name=args.common_name) + +if __name__ == '__main__': + main() diff --git a/pymanager/client.py b/pymanager/client.py new file mode 100755 index 0000000..c190fa2 --- /dev/null +++ b/pymanager/client.py @@ -0,0 +1,400 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# +# script to send command to manager khaganat process +# +# Copyright (C) 2017 AleaJactaEst +# +# 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 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" + +Configuration File +------------------ + +This script need configuration file (see below for model):: + + [config:client] + # Define port listen (default 8000) + port = 8000 + + # Example to generate all key : see pycertificate + # key + keyfile = /home/gameserver/ca/appli/private/clientkey.pem + + # certificate + certfile = /home/gameserver/ca/appli/certs/clientcert.pem + + # certification to check signature + ca_cert = /home/gameserver/ca/appli/certs/cachaincert.pem + address = 127.0.0.1 + + +Manipulate manager khaganat +--------------------------- + +We can end some command to manager. + + **Global** + + * SHUTDOWN : Stop manager & Stop all programs + * STARTALL : Start all programs + * STATUSALL : Get status of all programs + * STOPALL : Stop all programs + * LIST : List all programs available + + **For each program** + + * START : Start program + * STOP : Stop program + * STATUS : Get status + * STDOUT : Get log + firstline : option to define first line we need send + * STDIN : Send action (command) in stdin + action : option to define which action you need send to stdin + +Example :: + + pyclient --command="START" --program="aes" -c /home/gameserver/cfg/khaganat.cfg --log="info" --show-log-console + pyclient --command="STATUS" --program="aes" -c /home/gameserver/cfg/khaganat.cfg --log="debug" --show-log-console + pyclient --command="STDIN" --program="aes" --action="help all" -c /home/gameserver/cfg/khaganat.cfg --log="debug" --show-log-console + pyclient --command="STDOUT" --program="aes" --firstline=0 -c /home/gameserver/cfg/khaganat.cfg --log="debug" --show-log-console + pyclient --command="STOP" --program="aes" -c /home/gameserver/cfg/khaganat.cfg --log="debug" --show-log-console + pyclient --command="LIST" -c /home/gameserver/cfg/khaganat.cfg --log="debug" --show-log-console + pyclient --command="SHUTDOWN" -c /home/gameserver/cfg/khaganat.cfg --log="debug" --show-log-console + pyclient --command="STARTALL" -c /home/gameserver/cfg/khaganat.cfg --log="debug" --show-log-console + pyclient --command="STATUSALL" -c /home/gameserver/cfg/khaganat.cfg --log="debug" --show-log-console + + +You can use curl (to replace this script). + +Example :: + + curl -k --tlsv1.2 --cacert /home/gameserver/ca/appli/certs/cachaincert.pem --cert /home/gameserver/ca/appli/certs/clientcert.pem --key /home/gameserver/ca/appli/private/clientkey.pem -XGET "https://127.0.0.1:8000/STATUSALL" + curl -k --tlsv1.2 --cacert /home/gameserver/ca/appli/certs/cachaincert.pem --cert /home/gameserver/ca/appli/certs/clientcert.pem --key /home/gameserver/ca/appli/private/clientkey.pem --header "content-type: application/json" -d '{"name": "MyProgram"}' -XGET "https://127.0.0.1:8000/STATUS + +""" + +import sys +import argparse +import logging +import logging.config +import http.client +import json +import socket +import ssl +import configparser +import base64 +import getpass + +__VERSION__ = '1.0' + + +def cmp_to_key(): + """ Compare two key (numbre or string) """ + class K(object): + def __init__(self, obj, *args): + self.obj = obj + + def __lt__(self, other): + try: + return int(self.obj) < int(other.obj) + except ValueError: + return self.obj < other.obj + + def __gt__(self, other): + try: + return int(self.obj) > int(other.obj) + except ValueError: + return self.obj > other.obj + + def __eq__(self, other): + try: + return int(self.obj) == int(other.obj) + except ValueError: + return self.obj == other.obj + + def __le__(self, other): + try: + return int(self.obj) <= int(other.obj) + except ValueError: + return self.obj <= other.obj + + def __ge__(self, other): + try: + return int(self.obj) >= int(other.obj) + except ValueError: + return self.obj >= other.obj + + def __ne__(self, other): + try: + return int(self.obj) != int(other.obj) + except ValueError: + return self.obj != other.obj + + return K + + +class HTTPSConnectionCertificate(http.client.HTTPConnection): + """ Class HTTP connection with check certificate (if certicate is defined) """ + def __init__(self, key_file, cert_file, ca_cert, host='localhost', port=8000, timeout=10): + """ + Constructor + """ + logging.debug("constructor") + http.client.HTTPConnection.__init__(self, host, port, timeout) + self.key_file = key_file + self.cert_file = cert_file + self.ca_cert = ca_cert + self.host = host + self.port = port + + def connect(self): + """ + connect in https (and check certificate if defined) + """ + logging.debug("connect launched") + sock = socket.create_connection((self.host, self.port), self.timeout) + # If there's no CA File, don't force Server Certificate Check + if self.ca_cert: + logging.debug("key_file: " + str(self.key_file)) + logging.debug("cert_file: " + str(self.cert_file)) + logging.debug("ca_cert: " + str(self.ca_cert)) + self.sock = ssl.wrap_socket(sock, + self.key_file, + self.cert_file, + ca_certs=self.ca_cert, + cert_reqs=ssl.CERT_REQUIRED, + ssl_version=ssl.PROTOCOL_TLSv1_2 + ) + else: + self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file, cert_reqs=ssl.CERT_NONE) + + +class Client(): + """ + Class to manipulate pymanager + """ + def __init__(self, username, password_comand_line, password): + self.username = None + self.port = 8000 + self.address = 'localhost' + self.keyfile = None + self.certfile = None + self.ca_cert = None + self.timeout = 10 + if username: + self.username = username + if not password_comand_line: + if password is not None: + raise Exception("password in command line (but option disable to read this password).") + else: + self.password = getpass.getpass('Enter password:') + elif password is None: + raise Exception("Missing password.") + else: + self.password = password + + def load_config(self, filecfg): + if filecfg is None: + raise ValueError + config = configparser.ConfigParser() + config.read_file(filecfg) + logging.debug("Sections :%s" % config.sections()) + self._load_config(config) + + def _load_config(self, config): + """ + Read configuration object + param: config: configuration object + """ + logging.debug("Sections :%s" % config.sections()) + for name in config.sections(): + if name == 'config:client': + logging.debug("read config '%s'" % name) + try: + self.port = int(config[name]['port']) + except KeyError: + pass + except (TypeError, ValueError): + raise Exception("bad parameter for 'port' (in section %s)" % name) + try: + self.address = config[name]['address'] + except KeyError: + pass + try: + self.keyfile = config[name]['keyfile'] + except KeyError: + pass + try: + self.certfile = config[name]['certfile'] + except KeyError: + pass + try: + self.ca_cert = config[name]['ca_cert'] + except KeyError: + pass + try: + self.timeout = config[name]['timeout'] + except KeyError: + pass + + def send_json(self, + jsonin={}, + command='GET', + path='/', + raw_data=False, + remove_color=False): + """ + send command with https & json format + :param str jsonin: json input (send in data on http request) + ;param str command: http command (GET, POST, ...) + :param str path: path http + :param bool raw_data: show result without analyze + :param bool remove_color: at end string send color to disable background & color + """ + conn = HTTPSConnectionCertificate(host=self.address, + port=self.port, + key_file=self.keyfile, + cert_file=self.certfile, + ca_cert=self.ca_cert, + timeout=self.timeout) + conn.putrequest(command, path) + out = json.dumps(jsonin) + if self.username: + accountpassword = "%s:%s" % (self.username, self.password) + conn.putheader('Authorization', b"Basic " + base64.b64encode(bytes(accountpassword, 'UTF-8'))) + conn.putheader('Content-type', 'application/json') + conn.putheader('Content-length', len(out)) + conn.endheaders() + conn.send(bytes(out, "utf-8")) + response = conn.getresponse() + if raw_data: + print(response.read()) + else: + if remove_color: + endText = '\x1b[0m' + else: + endText = '' + if response.status != 200: + logging.error("Error detected (html code:%d)" % response.status) + print(response.read()) + return + ret = response.read().decode() + try: + msgjson = json.loads(ret) + except json.JSONDecodeError: + logging.error("Impossible to decode Json output") + print(ret) + return + for key in sorted(msgjson, key=cmp_to_key()): + print("%s: %s %s" % (key, msgjson[key], endText)) + + +def root(command, + program, + stdin, + firstline, + fileLog, + logLevel, + username, + password_comand_line, + password, + show_log_console, + raw_data=False, + remove_color=False, + filecfg=None): + # Manage log + logging.getLogger('logging') + numeric_level = getattr(logging, logLevel.upper(), None) + if not isinstance(numeric_level, int): + raise ValueError('Invalid log level: %s' % logLevel) + handlers = [] + if show_log_console: + handlers.append(logging.StreamHandler()) + if fileLog: + handlers.append(logging.FileHandler(fileLog.name)) + logging.basicConfig(handlers=handlers, level=numeric_level, + format='%(asctime)s %(levelname)s [pid:%(process)d] [%(funcName)s:%(lineno)d] %(message)s') + + client = Client(username, password_comand_line, password) + client.load_config(filecfg) + # Send command + if command == 'START' or command == 'STOP': + client.send_json({'name': program}, 'POST', "/" + command) + elif command == 'STATUS': + client.send_json({'name': program}, 'GET', "/" + command) + elif command == 'STDIN': + client.send_json({'name': program, 'action': stdin}, 'POST', "/" + command) + elif command == 'STDOUT': + client.send_json({'name': program, 'first-line': firstline}, 'GET', "/" + command) + elif command == 'LIST': + client.send_json({}, 'GET', "/" + command) + elif command == 'SHUTDOWN' or command == 'STARTALL' or command == 'STOPALL': + client.send_json({}, 'POST', "/" + command) + elif command == 'STATUSALL': + client.send_json({}, 'GET', "/" + command) + else: + logging.error("command unknown (%s)" % command) + + +def main(args=sys.argv[1:]): + """ Main function + + :param list args: all arguments ('--help, '--version', ...) + """ + parser = argparse.ArgumentParser(description='Manipulate khaganat process') + parser.add_argument('--version', action='version', version='%(prog)s ' + __VERSION__) + parser.add_argument('--show-log-console', action='store_true', + help='show message in console', default=False) + parser.add_argument('--filelog', type=argparse.FileType('wt'), + default=None, help='log file') + parser.add_argument('--log', + default='INFO', help='log level [DEBUG, INFO, WARNING, ERROR') + parser.add_argument('--command', help='command send to khganat', default='/STATUS') + parser.add_argument('--program', help='program khaganat id', default='aes') + parser.add_argument('--stdin', help='action send to stdin', default='') + parser.add_argument('--firstline', type=int, + help='define fistline read for log command', default=0) + parser.add_argument('--raw-data', action='store_true', + help='show raw message', default=False) + parser.add_argument('--keep-color', action='store_true', + help='some message have color define, by default we reset the color ' + '(this option keep current color state)', + default=False) + parser.add_argument('-c', '--conf', type=argparse.FileType('r'), + default='khaganat.cfg', help='configuration file') + parser.add_argument('-b', '--password-comand-line', action='store_true', + help='Use the password from the command line rather than prompting for it.', + default=False) + parser.add_argument('username', type=str, nargs='?', default=None) + parser.add_argument('password', type=str, nargs='?') + + param = parser.parse_args(args) + root(stdin=param.stdin, + firstline=param.firstline, + command=param.command, + program=param.program, + fileLog=param.filelog, + logLevel=param.log, + show_log_console=param.show_log_console, + raw_data=param.raw_data, + remove_color=not param.keep_color, + filecfg=param.conf, + username=param.username, + password_comand_line=param.password_comand_line, + password=param.password) + +if __name__ == '__main__': + main() diff --git a/pymanager/manager.py b/pymanager/manager.py new file mode 100755 index 0000000..ae225a9 --- /dev/null +++ b/pymanager/manager.py @@ -0,0 +1,1029 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# +# script to start/stop/status/send command/read log for khaganat process +# +# Copyright (C) 2017 AleaJactaEst +# +# 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 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" + +Configuration File +------------------ + +This script need configuration file (see below for model):: + + [config:server] + # Define port listen (default 8000) + port = 8000 + + # Example to generate all key : see pycertificate + # key + keyfile = /home/gameserver/ca/appli/private/serverkey.pem + + # certificate + certfile = /home/gameserver/ca/appli/certs/servercert.pem + + # certification to check signature + ca_cert = /home/gameserver/ca/appli/certs/cachaincert.pem + + # address listen (default all port) + address = + + + # Admin Executor Service + [command:aes] + # command to launch the program + command = ryzom_admin_service -A/home/gameserver/khanat/server -C/home/gameserver/khanat/server -L/home/gameserver/log/khanat --nobreak --fulladminname=admin_executor_service --shortadminname=AES + # Path : where this program is launched + path = /home/gameserver/khanat/server/ + # size buffer log for each program launched (number line stdout) + logsize = 1000 + # buffer size (define value bufsize on subprocess.Popen, this buffer is use before read by manager) + bufsize = 100 + + + # bms_master : backup_service + [command:bms_master] + # command to launch the program + command = ryzom_backup_service -A/home/gameserver/khanat/server -C/home/gameserver/khanat/server -L/home/gameserver/khanat/server/log --nobreak --writepid -P49990 + # Path : where this program is launched + path = /home/gameserver/khanat/server/ + # we keep [logsize] last number line stdout + logsize = 1000 + # buffer size (define value bufsize on subprocess.Popen) + bufsize = 100 + + +Manager +------- + + Manage all process khaganat + Launch this prorgam in background and use clientManager to manipulate process + +Design + +.. graphviz:: + + digraph Manager { + "Manager" -> "ManageCommand (command 1)"; + "ManageCommand (command 1)" -> "read_output (thread1)"; + "Manager" -> "ManageCommand (command 2)"; + "ManageCommand (command 2)" -> "read_output (thread2)"; + "Manager" -> "ServerHttp"; + "ServerHttp" -> "khaganatHTTPServer"; + "khaganatHTTPServer" -> "ManageHttpRequest"; + "ManageHttpRequest" -> "ManageCommand (command 1)" [style=dashed]; + "ManageHttpRequest" -> "ManageCommand (command 2)" [style=dashed]; + } + + +http(s) command : +----------------- + ++------------------+-------------+---------------------------------------------+---------------------------------------------+ +| **Html command** | **Path** | **Argument** {json format} | **Comment** | ++------------------+-------------+---------------------------------------------+---------------------------------------------+ +| **POST** | /SHUTDOWN | | Stop all process and stop pymanager | ++------------------+-------------+---------------------------------------------+---------------------------------------------+ +| **POST** | /STARTALL | | Start all processes | ++------------------+-------------+---------------------------------------------+---------------------------------------------+ +| **GET** | /STATUSALL | | Get status all processes | ++------------------+-------------+---------------------------------------------+---------------------------------------------+ +| **POST** | /STOPALL | | Stop all processes | ++------------------+-------------+---------------------------------------------+---------------------------------------------+ +| **POST** | /START | {'name': program} | Start for one program | ++------------------+-------------+---------------------------------------------+---------------------------------------------+ +| **POST** | /STDIN | {'name': program, 'action': action} | Send action for one program (send to input) | ++------------------+-------------+---------------------------------------------+---------------------------------------------+ +| **GET** | /STATUS | {'name': program} | Get status for one program | ++------------------+-------------+---------------------------------------------+---------------------------------------------+ +| **POST** | /STOP | {'name': program} | Stop for one program | ++------------------+-------------+---------------------------------------------+---------------------------------------------+ +| **GET** | /STDOUT | {'name': program, 'first-line': firstline } | Get log for one program | ++------------------+-------------+---------------------------------------------+---------------------------------------------+ + + +Example :: + + nohup pymanager --log info --filelog /home/gameserver/log/manager.log -c /home/gameserver/cfg/khaganat.cfg 2>/dev/null 1>/dev/null 0 %s" % (command, name, result)) + + outjson = {'state': result} + self._set_headers() + self.wfile.write(bytes(json.dumps(outjson), "utf-8")) + + def check_authentication(self): + if not self.server.authentification: + return True + try: + auth_header = self.headers['Authorization'].split() + if auth_header[0] != 'Basic': + logging.error("Authentification with Bad method (%s)" % auth_header[0]) + return False + decode = base64.b64decode(auth_header[1]).decode('UTF-8') + account, password = decode.split(':', maxsplit=1) + if account not in self.server.users: + logging.error("Authentification with unknown user (%s)" % account) + return False + hashed_password = self.server.users[account] + if bcrypt.checkpw(password, hashed_password): + return True + else: + logging.error("Authentification with wrong password for user (%s)" % account) + return False + except (ValueError, IndexError, AttributeError) as e: + logging.error("Error detected %s" % e) + return False + return True + + def do_GET(self): + """ + Manage request READ + we can execute LOG, STATUS, LIST & STATUSALL + """ + logging.debug('get recieved : %s' % self.path) + if not self.check_authentication(): + self.send_error(403, 'Wrong authentication') + logging.error("Wrong authentication") + elif self.path == '/STDOUT': + self._command_log() + elif self.path == '/STATUS': + self._send_command("STATUS") + elif self.path == '/LIST': + self._send_list() + elif self.path == '/STATUSALL': + self._send_command_all("STATUS") + else: + self.send_error(400, 'Path unknown') + logging.error("Path unknwon '%s'" % self.path) + + def do_POST(self): + """ Manage request POST (CREATE) + currently, we execute START, STOP, ACTION, SHUTDOWN, STARTALL & STOPALL + """ + logging.debug('post recieved : %s' % self.path) + if not self.check_authentication(): + self.send_error(400, 'Bad authentication') + logging.error("Bad authentication") + elif self.path == '/START': + self._send_command("START") + elif self.path == '/STOP': + self._send_command("STOP") + elif self.path == '/STDIN': + self._send_action() + elif self.path == '/SHUTDOWN': + self._send_shutdown() + elif self.path == '/STARTALL': + self._send_command_all("START") + elif self.path == '/STOPALL': + self._send_command_all("STOP") + else: + self.send_error(400, 'Path unknown') + logging.error("Path unknwon '%s'" % self.path) + + def do_HEAD(self): + """ request HEAD received """ + logging.debug('head recieved : %s' % self.path) + self.send_error(404, 'File Not Found: %s' % self.path) + + def do_PUT(self): + """ request PUT (UPDATE/REPLACE) received """ + logging.debug('put recieved!') + self.send_error(404, 'File Not Found: %s' % self.path) + + def do_PATCH(self): + """ request PATCH (UPDATE/MODIFY) received """ + logging.debug('patch recieved!') + self.send_error(404, 'File Not Found: %s' % self.path) + + def do_DELETE(self): + """ request DELETE received """ + logging.debug('delete recieved!') + self.send_error(404, 'File Not Found: %s' % self.path) + + +class khaganatHTTPServer(http.server.HTTPServer): + """ + Class khaganatHTTPServer + Redefine HTTPServer (adding queue input & queue output, use by ManageHttpRequest) + """ + def __init__(self, + listQueueIn, + listQueueOut, + listEvent, + server_address, + RequestHandlerClass, + authentification, + users, + bind_and_activate=True): + http.server.HTTPServer.__init__(self, server_address, RequestHandlerClass, bind_and_activate) + self.listQueueIn = listQueueIn + self.listQueueOut = listQueueOut + self.listEvent = listEvent + self.authentification = authentification + self.users = users + + +class ServerHttp(multiprocessing.Process): + """ + Initialize server HTTPS + * define Dictionnary queueIn & queueOut (with key as section's name in configuration) + """ + def __init__(self, keyfile, certfile, ca_cert, address='', + port=8000, authentification=True, users={}): + multiprocessing.Process.__init__(self) + self.listQueueIn = {} + self.listQueueOut = {} + self.listEvent = {} + self.port = port + self.key_file = keyfile + self.cert_file = certfile + self.ca_cert = ca_cert + self.address = address + self.authentification = authentification + self.users = users + + def run(self): + server_address = (self.address, self.port) + httpd = khaganatHTTPServer(self.listQueueIn, + self.listQueueOut, + self.listEvent, + server_address, + ManageHttpRequest, + self.authentification, + self.users) + if self.ca_cert: + httpd.socket = ssl.wrap_socket(httpd.socket, + keyfile=self.key_file, + certfile=self.cert_file, + ca_certs=self.ca_cert, + cert_reqs=ssl.CERT_REQUIRED, + ssl_version=ssl.PROTOCOL_TLSv1_2, + server_side=True) + else: + httpd.socket = ssl.wrap_socket(httpd.socket, + keyfile=self.key_file, + certfile=self.cert_file, + server_side=True) + logging.info('https listen') + httpd.serve_forever() + + def append(self, name, queueIn, queueOut, event): + self.listQueueIn.setdefault(name, queueIn) + self.listQueueOut.setdefault(name, queueOut) + self.listEvent.setdefault(name, event) + + +class ManageCommand(): + """ + Manage Command (only one) + * start/stop/status/get log/send an action [stdin] for command (receive order with queueIn) + * read output [in other thread] + * communicate with ManageHttpRequest (with queueOut) + """ + def __init__(self, name, command, path, logsize, bufsize, queueIn, queueOut, event, maxWaitEnd=10, waitDelay=1): + self.process = None + self.queueIn = queueIn + self.queueOut = queueOut + self.name = name + self.command = command + self.path = path + self.log = [] + self.poslastlog = 0 + self.maxlog = logsize + self.event = event + self.bufsize = bufsize + self.threadRead = None + self.running = False + self.state = multiprocessing.Queue() + self.pipeIn, self.pipeOut = multiprocessing.Pipe() + self.eventRunning = threading.Event() + self.maxWaitEnd = maxWaitEnd + self.waitDelay = waitDelay + + def read_output(self): + """ Thread to read output (stdout) """ + fl = fcntl.fcntl(self.process.stdout, fcntl.F_GETFL) + fcntl.fcntl(self.process.stdout, fcntl.F_SETFL, fl | os.O_NONBLOCK) + logging.debug("Start reader %s" % self.name) + while self.eventRunning.is_set(): + code = self.process.poll() + if code is not None: + logging.error("process %s down" % self.name) + self.eventRunning.clear() + continue + try: + line = self.process.stdout.readline() + except AttributeError: + logging.error("process %s down (not detected)" % self.name) + self.eventRunning.clear() + continue + if not line: + time.sleep(self.waitDelay) + continue + now = time.strftime('%Y/%m/%d %H:%M:%S %Z') + logging.debug("line %s " % line) + self.poslastlog += 1 + while len(self.log) >= self.maxlog: + self.log.pop(0) + msg = line.decode().strip() + self.log.append(now + ' ' + msg) + logging.debug("recu: '%s'" % (msg)) + logging.debug("End reader: '%s'" % self.name) + + def handler(self, signum, frame): + """ Managed signal (not used) """ + if self.process: + # logging.debug("Send signal %d to '%s'" %(signum, self.name)) + self.process.send_signal(signum) + else: + logging.error("Impossible to send signal %d to '%s'" % (signum, self.name)) + raise IOError("signal received") + + def start(self): + """ Start program """ + logging.debug("start %s" % (self.name)) + if self.process: + logging.debug("%s already exist" % self.name) + code = self.process.poll() + if code is None: + logging.debug("%s already exist" % self.name) + return "already-started" + else: + logging.debug("%s crashed" % self.name) + code = self.process.wait() + logging.error("%s crashed (return code:%d) - restart program" % (self.name, code)) + try: + self.process = subprocess.Popen(self.command.split(), + cwd=self.path, + shell=False, + bufsize=self.bufsize, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + close_fds=True) + except FileNotFoundError as e: + logging.error("Impossible to start %s (%s)" % (self.name, e)) + return "crashed" + except PermissionError as e: + logging.error("Impossible to start %s (%s)" % (self.name, e)) + return "crashed" + self.eventRunning.set() + if self.threadRead: + self.eventRunning.clear() + self.threadRead.join() + self.threadRead = None + self.running = True + self.threadRead = threading.Thread(target=self.read_output) + self.threadRead.start() + return "started" + + def status(self): + """ Get status of program """ + logging.debug("status %s" % (self.name)) + if self.process: + logging.debug("status %s - check" % (self.name)) + code = self.process.poll() + if code is None: + logging.debug("%s status" % (self.name)) + return "started" + else: + logging.error("%s crashed (return code:%d)" % (self.name, code)) + self.process = None + return "stopped" + else: + logging.debug("%s status" % (self.name)) + return "stopped" + + def list_thread(self): + """ List number thrad (not used) """ + logging.debug('list thread') + # main_thread = threading.currentThread() + for t in threading.enumerate(): + logging.debug('thread %s', t.getName()) + logging.debug("id %d" % t.ident) + + def stop(self): + """ Stop program """ + logging.debug("stop %s" % (self.name)) + if not self.process: + return "stopped" + else: + code = self.process.poll() + loop = self.maxWaitEnd + while (code is None) and (loop > 0): + logging.debug("stop process %s", self.name) + self.process.send_signal(15) + time.sleep(1) + code = self.process.poll() + loop -= 1 + + loop = self.maxWaitEnd + while (code is None) and (loop > 0): + logging.debug("terminate process %s", self.name) + self.process.terminate() + time.sleep(1) + code = self.process.poll() + loop -= 1 + + loop = self.maxWaitEnd + while (code is None) and (loop > 0): + logging.debug("kill process %s", self.name) + self.process.send_signal(9) + time.sleep(1) + code = self.process.poll() + loop -= 1 + + code = self.process.wait() + self.process = None + if self.threadRead: + self.eventRunning.clear() + self.threadRead.join() + self.threadRead = None + logging.info("%s stopped (return code:%d)" % (self.name, code)) + return "stopped" + + def getlog(self, firstline): + """ Get log """ + logging.debug("read log %d " % firstline) + outjson = {} + pos = self.poslastlog - len(self.log) + 1 + firstlinefound = None + for line in self.log: + if pos >= firstline: + outjson.setdefault(pos, line) + if not firstlinefound: + firstlinefound = pos + pos += 1 + outjson.setdefault('first-line', firstlinefound) + outjson.setdefault('last-line', pos - 1) + return json.dumps(outjson) + + def action(self, action): + """ Send action to program (send input to stdin) """ + logging.debug("STDIN '%s'" % action) + if self.process: + code = self.process.poll() + if code is None: + if action: + self.process.stdin.write(bytes(action+'\n', 'UTF-8')) + self.process.stdin.flush() + return "ok" + return "ko" + + def run(self): + """ loop, run child (wait command) """ + loop = True + while loop: + logging.debug('wait %s' % self.name) + self.event.wait() + logging.debug('received event %s' % self.name) + try: + msg = self.queueIn.get(timeout=4) + except queue.Empty: + self.event.clear() + logging.debug("pas de message recu pour %s" % self.name) + return + logging.debug("command : '%s'" % msg) + command = msg.split()[0] + if command == "SHUTDOWN": + loop = False + continue + elif command == "START": + self.queueOut.put(self.start()) + elif command == "STATUS": + self.queueOut.put(self.status()) + elif command == "STOP": + self.queueOut.put(self.stop()) + elif command == "STDIN": + data = msg.split(maxsplit=1)[1] + self.queueOut.put(self.action(data)) + elif command == "STDOUT": + try: + firstline = int(msg.split(maxsplit=1)[1]) + except ValueError: + firstline = 0 + self.queueOut.put(self.getlog(firstline)) + else: + self.queueOut.put("error : command unknown") + self.event.clear() + self.stop() + self.event.clear() + logging.debug('end') + + +class Manager(): + """ + Manage all services + (read configuration, launch ManageCommand & launch ServerHttp & wait the end) + * https service + * all child to manage (it start ManageCommand by command define in configuration) + + """ + def __init__(self, launch_program): + self.threadCommand = [] + self.command = [] + self.launch_program = launch_program + self.param = {} + self.users = {} + self.passwordfile = None + + def load_config(self, filecfg): + if filecfg is None: + raise ValueError + config = configparser.ConfigParser() + config.read_file(filecfg) + self._load_config(config) + + def load_password(self): + if self.passwordfile: + with open(self.passwordfile, 'rt') as fp: + for line in fp: + line = line.strip() + if not line: + continue + username, password = line.split(':', maxsplit=1) + self.users.setdefault(username, password) + + def _load_config(self, config): + """ + Read configuration object + param: config: configuration object + """ + logging.debug("Sections :%s" % config.sections()) + for name in config.sections(): + if name == 'config:client': + continue + if name == 'config:user': + continue + elif name == 'config:server': + logging.debug("read config '%s'" % name) + try: + self.port = int(config[name]['port']) + except (TypeError, KeyError, ValueError): + self.port = 8000 + try: + self.address = config[name]['address'] + except (TypeError, KeyError): + self.address = '' + try: + self.keyfile = config[name]['keyfile'] + except (TypeError, KeyError): + self.keyfile = 'crt/key.pem' + try: + self.certfile = config[name]['certfile'] + except (TypeError, KeyError): + self.certfile = 'crt/cert.pem' + try: + self.ca_cert = config[name]['ca_cert'] + except (TypeError, KeyError): + self.ca_cert = 'crt/ca_cert.crt' + try: + tmp = config[name]['authentification'] + if tmp.upper().strip() == 'YES': + self.authentification = True + else: + self.authentification = False + except (TypeError, KeyError): + self.authentification = False + try: + self.passwordfile = config[name]['passwordfile'] + except (TypeError, KeyError): + self.passwordfile = None + else: + head, value = name.split(':', maxsplit=1) + if head == 'command' and 'command' in config[name]: + logging.debug("read command '%s'" % name) + if 'path' in config[name]: + path = config[name]['path'] + else: + path = None + if 'logsize' in config[name]: + try: + logsize = int(config[name]['logsize']) + except (TypeError, KeyError, ValueError): + logging.error("Impossible to read param logsize (command:%s)", name) + raise ValueError + else: + logsize = 100 + if 'bufsize' in config[name]: + try: + bufsize = int(config[name]['bufsize']) + except (TypeError, KeyError, ValueError): + logging.error("Impossible to read param bufsize (command:%s)", name) + raise ValueError + else: + bufsize = 100 + self.param.setdefault(name, {'command': config[name]['command'], + 'path': path, + 'logsize': logsize, + 'bufsize': bufsize}) + + def initialize_http(self): + """ + Initialize object serverHttp + """ + self.serverHttp = ServerHttp(self.keyfile, + self.certfile, + self.ca_cert, + self.address, + self.port, + authentification=self.authentification, + users=self.users) + + def runCommand(self, name, command, path, logsize, bufsize, queueIn, queueOut, event): + """ + Thread to manage khaganat program + """ + logging.debug("Initialize '%s'" % name) + manageCommand = ManageCommand(name=name, + command=command, + path=path, + logsize=logsize, + bufsize=bufsize, + queueIn=queueIn, + queueOut=queueOut, + event=event) + manageCommand.run() + + def launch_server_http(self): + """ Launch server https """ + self.serverHttp.daemon = True + self.serverHttp .start() + + def launch_command(self): + """ Launch child to manage each program """ + for name in self.param: + logging.debug("Initialize '%s'" % name) + queueIn = multiprocessing.Queue() + queueOut = multiprocessing.Queue() + event = multiprocessing.Event() + self.serverHttp.append(name, queueIn, queueOut, event) + threadCommand = multiprocessing.Process(target=self.runCommand, + args=(name, + self.param[name]['command'], + self.param[name]['path'], + self.param[name]['logsize'], + self.param[name]['bufsize'], + queueIn, + queueOut, + event)) + threadCommand.start() + if self.launch_program: + event.set() + queueIn.put("START") + try: + item = queueOut.get(timeout=4) + except queue.Empty: + item = "" + logging.debug("pas de message recu pour %s" % name) + return + logging.info("%s => %s" % (name, item)) + self.threadCommand.append(threadCommand) + + def receive_signal(self, signum, frame): + """ Managed signal """ + for child in self.threadCommand: + child.terminate() + if self.serverHttp: + self.serverHttp.terminate() + + def wait_children_commands(self): + for child in self.threadCommand: + child.join() + + def wait_child_server_http(self): + self.serverHttp.terminate() + self.serverHttp.join() + + def run(self): + """ launch all """ + self.launch_command() + self.launch_server_http() + logging.info('started') + self.wait_children_commands() + logging.info('execute shutdown') + signal.alarm(0) + logging.info('wait thread http') + time.sleep(1) + self.wait_child_server_http() + logging.info('shutdown completed') + + +def root(filecfg, fileLog, logLevel, launch_program, show_log_console): + """ + Main function + :param str filecfg: configuration file + :param str fileLog: log file + :param bool launch_program: do you launch program when you start manager (auto start) + :param bool show_log_console: do you need show log on console + """ + # Manage log + logging.getLogger('logging') + numeric_level = getattr(logging, logLevel.upper(), None) + if not isinstance(numeric_level, int): + raise ValueError('Invalid log level: %s' % logLevel) + handlers = [] + if show_log_console: + handlers.append(logging.StreamHandler()) + if fileLog: + handlers.append(logging.FileHandler(fileLog.name)) + logging.basicConfig(handlers=handlers, level=numeric_level, + format='%(asctime)s %(levelname)s [pid:%(process)d] [%(funcName)s:%(lineno)d] %(message)s') + if filecfg is None: + logging.error("Missing configuration file") + raise ValueError + manager = Manager(launch_program) + manager.load_config(filecfg) + manager.load_password() + manager.initialize_http() + manager.run() + + +def main(args=sys.argv[1:]): + """ Main function + + :param list args: all arguments ('--help, '--version', ...) + """ + parser = argparse.ArgumentParser(description='Manage khaganat process') + parser.add_argument('--version', action='version', version='%(prog)s ' + __VERSION__) + parser.add_argument('-c', '--conf', type=argparse.FileType('r'), + default='khaganat.cfg', help='configuration file') + parser.add_argument('--show-log-console', action='store_true', + help='show message in console', default=False) + parser.add_argument('--filelog', type=argparse.FileType('wt'), + default=None, help='log file') + parser.add_argument('--log', + default='INFO', help='log level [DEBUG, INFO, WARNING, ERROR') + parser.add_argument('--launch-program', action='store_true', + help='launch program when start manager', default=False) + param = parser.parse_args(args) + root(filecfg=param.conf, + fileLog=param.filelog, + logLevel=param.log, + launch_program=param.launch_program, + show_log_console=param.show_log_console) + +if __name__ == '__main__': + main() diff --git a/pymanager/password.py b/pymanager/password.py new file mode 100755 index 0000000..fad084d --- /dev/null +++ b/pymanager/password.py @@ -0,0 +1,136 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# +# scriptto manipulate password file +# +# Copyright (C) 2017 AleaJactaEst +# +# 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 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +import bcrypt +import argparse +import sys +import os +import getpass + +__VERSION__ = '1.0' + + +class PasswordFile: + def __init__(self, passwordfile, createfile, password_comand_line, username, password): + self.passwordfile = passwordfile + self.users = {} + + if not password_comand_line: + if password is not None: + raise Exception("password in command line (but option disable to read this password).") + else: + self.password = None + elif password is None: + raise Exception("Missing password.") + else: + self.password = password + self.username = username + + if not os.path.exists(self.passwordfile): + if createfile: + with open(self.passwordfile, 'wt', encoding='utf-8') as fp: + fp.write('\n') + else: + raise Exception("%s does not exist." % self.passwordfile) + else: + self.__load__() + + def __load__(self): + with open(self.passwordfile, 'rt') as fp: + for line in fp: + line = line.strip() + if not line: + continue + username, password = line.split(':', maxsplit=1) + self.users.setdefault(username, password) + + def save(self): + with open(self.passwordfile, 'wt', encoding='utf-8') as fp: + for user in sorted(self.users): + fp.write('%s:%s\n' % (user, self.users[user])) + + def update(self): + if self.password is None: + self.password = getpass.getpass('New password:') + check = getpass.getpass('Re-type new password:') + if self.password != check: + raise Exception("password verification error") + + hashed_password = bcrypt.hashpw(self.password.encode('utf-8'), bcrypt.gensalt()).decode('utf8') + self.users[self.username] = hashed_password + print("Adding password for user %s" % self.username) + + def verify(self): + if self.password is None: + self.password = getpass.getpass('Enter password:') + if bcrypt.checkpw(self.password.encode('utf-8'), self.users[self.username].encode('utf-8')): + print("Password for user %s correct." % self.username) + else: + raise Exception("password verification failed.") + + def delete(self): + if self.username in self.users: + del self.users[self.username] + + +def root(passwordfile, create, password_comand_line, username, password, delete, verify): + pf = PasswordFile(passwordfile, create, password_comand_line, username, password) + if delete: + pf.delete() + pf.save() + elif verify: + pf.verify() + else: + pf.update() + pf.save() + + +def main(args=sys.argv[1:]): + """ Main function + + :param list args: root password + """ + parser = argparse.ArgumentParser(description='password manager') + parser.add_argument('--version', action='version', version='%(prog)s ' + __VERSION__) + + parser.add_argument('-c', '--create', action='store_true', + help='create a new file', default=False) + parser.add_argument('-D', '--delete', action='store_true', + help='delete username', default=False) + parser.add_argument('-v', '--verify', action='store_true', + help='Verify password for the specified user.', default=False) + parser.add_argument('-b', '--password-comand-line', action='store_true', + help='Use the password from the command line rather than prompting for it.', + default=False) + parser.add_argument('passwordfile', type=str, help='file contains all password') + parser.add_argument('username', type=str) + parser.add_argument('password', type=str, nargs='?') + param = parser.parse_args(args) + root(param.passwordfile, + param.create, + param.password_comand_line, + param.username, + param.password, + param.delete, param.verify) + + +if __name__ == '__main__': + main() diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..2a83be7 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,10 @@ +[global] +verbose=1 + +# option for pep8 (check code style python) +[pep8] +max-line-length = 300 + +# option for pycodestyle (check code style python) - replace pep8 +[pycodestyle] +max-line-length = 300 diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..e4e1d91 --- /dev/null +++ b/setup.py @@ -0,0 +1,140 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# +# A setuptools for opennel-pymanager +# Copyright (C) 2017 AleaJactaEst +# +# 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 . + +""" + A setuptools for opennel-pymanager + + python3 setup.py test + python3 setup.py build_sphinx + python3 setup.py --command-packages=stdeb.command bdist_deb +""" + +from pymanager import __version__ +from setuptools import setup, find_packages +# To use a consistent encoding +from codecs import open +from os import path + +#import unittest +#def my_test_suite(): +# test_loader = unittest.TestLoader() +# test_suite = test_loader.discover('tests', pattern='test_*.py') +# return test_suite + +here = path.abspath(path.dirname(__file__)) + +# Get the long description from the README file +with open(path.join(here, 'README.rst'), encoding='utf-8') as f: + long_description = f.read() + +setup( + name='pymanager', + + # Versions should comply with PEP440. For a discussion on single-sourcing + # the version across setup.py and the project code, see + # https://packaging.python.org/en/latest/single_source_version.html + version=__version__, + + description='Tools to manage OpenNel', + long_description=long_description, + + # The project's main homepage. + url='//git.khaganat.net/khaganat/mmorpg_khanat/opennel-pymanager', + + # Author details + author='Aleajactaest', + author_email='jean.sorgemoel.liber@free.fr', + + # Choose your license + license='AGPLv3', + + # See https://pypi.python.org/pypi?%3Aaction=list_classifiers + classifiers=[ + # How mature is this project? Common values are + # 3 - Alpha + # 4 - Beta + # 5 - Production/Stable + 'Development Status :: 3 - Alpha', + + # Indicate who your project is intended for + 'Intended Audience :: Developers', + 'Topic :: Games/Entertainment :: Role-Playing', + + # Pick your license as you wish (should match "license" above) + 'License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)', + + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + ], + + # What does your project relate to? + keywords='opennel manager', + + # You can just specify the packages manually here if your project is + # simple. Or you can use find_packages(). + packages=find_packages(exclude=['contrib', 'docs', 'tests']), + + # Alternatively, if you want to distribute just a my_module.py, uncomment + # this: + # py_modules=["my_module"], + + # List run-time dependencies here. These will be installed by pip when + # your project is installed. For an analysis of "install_requires" vs pip's + # requirements files see: + # https://packaging.python.org/en/latest/requirements.html + #install_requires=['peppercorn'], + + # List additional groups of dependencies here (e.g. development + # dependencies). You can install these using the following syntax, + # for example: + # $ pip install -e .[dev,test] +# extras_require={ +# 'dev': ['check-manifest'], +# 'test': ['coverage'], +# }, + #test_suite='setup.my_test_suite', + test_suite="tests", + + # If there are data files included in your packages that need to be + # installed, specify them here. If using Python 2.6 or less, then these + # have to be included in MANIFEST.in as well. + #package_data={ + # 'sample': ['package_data.dat'], + #}, + + # Although 'package_data' is the preferred approach, in some case you may + # need to place data files outside of your packages. See: + # http://docs.python.org/3.4/distutils/setupscript.html#installing-additional-files # noqa + # In this case, 'data_file' will be installed into '/my_data' + #data_files=[('my_data', ['data/data_file'])], + + # To provide executable scripts, use entry points in preference to the + # "scripts" keyword. Entry points provide cross-platform support and allow + # pip to create the appropriate form of executable for the target platform. + entry_points={ + 'console_scripts': [ + 'opennel_certificate=pymanager.certificate:main', + 'opennel_manager=pymanager.manager:main', + 'opennel_client=pymanager.client:main', + 'opennel_password=pymanager.password:main', + ], + }, +) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/simulate_program.py b/tests/simulate_program.py new file mode 100755 index 0000000..6e467d2 --- /dev/null +++ b/tests/simulate_program.py @@ -0,0 +1,82 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# +# Script use to simulate opennel program (input/output terminal) +# Copyright (C) 2017 AleaJactaEst +# +# 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 . + +import argparse +import sys +import time +import signal + + +class ManageSignal: + + def __init__(self): + self.kill_now = False + + def activate(self): + signal.signal(signal.SIGINT, self.exit_gracefully) + signal.signal(signal.SIGTERM, self.exit_gracefully) + + def exit_gracefully(self,signum, frame): + self.kill_now = True + + +class SimulateProgram(): + def __init__(self): + self.line = 0 + + def print_output(self, message): + self.line += 1 + print(self.line, message) + + def main(self, noloop, timeout, refuse_kill): + manageSignal = ManageSignal() + if refuse_kill: + manageSignal.activate() + loop = not noloop + self.print_output("Initializing") + self.print_output("Starting") + self.print_output("Started") + while loop is True: + try: + msg = input() + self.print_output(msg) + except (KeyboardInterrupt, EOFError): + loop = refuse_kill + time.sleep(timeout) + self.print_output("End") + +def main(args=sys.argv[1:]): + """ Main function + + :param list args: root password + """ + parser = argparse.ArgumentParser(description='Simulate program') + + parser.add_argument('--no-loop', action='store_true', + help='disable loop', default=False) + parser.add_argument('--timeout', type=int, + default=10, help='timeout') + parser.add_argument('--disable-kill', action='store_true', + help='disable loop', default=False) + args = parser.parse_args() + simulate = SimulateProgram() + simulate.main(args.no_loop, args.timeout, args.disable_kill) + +if __name__ == '__main__': + main() diff --git a/tests/test.cfg b/tests/test.cfg new file mode 100644 index 0000000..edd799e --- /dev/null +++ b/tests/test.cfg @@ -0,0 +1,72 @@ +# +# Configuration management program khaganat +# +# Copyright (C) 2017 AleaJactaEst +# +# 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 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# +# Global parameter : use bu ymanager +# +[config:server] +# Define port listen (default 8000) +port = 8000 + +# key +keyfile = /home/gameserver/ca/appli/private/serverkey.pem + +# certificate +certfile = /home/gameserver/ca/appli/certs/servercert.pem + +# certification to check signature +ca_cert = /home/gameserver/ca/appli/certs/cachaincert.pem + +# address listen (default all port) +address = + +# activate authentification (yes or no) +authentification = no +passwordfile = /home/gameserver/passwordfile + +[config:client] +port = 8000 +keyfile = /home/gameserver/ca/appli/private/clientkey.pem +certfile = /home/gameserver/ca/appli/certs/clientcert.pem +ca_cert = /home/gameserver/ca/appli/certs/cachaincert.pem +address = 127.0.0.1 + + +############################## +# List all program we manage # +############################## + +# Admin Executor Service +[coucou] +# command to launch the program +command = /home/gameserver/coucou.sh + +# Admin Executor Service +[coucou2] +# command to launch the program +command = /home/gameserver/coucou.sh +# size buffer log for each program launched (number line stdout) +logsize = 1000 +# buffer size (define value bufsize on subprocess.Popen, this buffer is use before read by manager) +bufsize = 100 + +# Admin Executor Service +[sleep] +# command to launch the program +command = sleep 10 + diff --git a/tests/test_certificate.py b/tests/test_certificate.py new file mode 100644 index 0000000..6556d55 --- /dev/null +++ b/tests/test_certificate.py @@ -0,0 +1,188 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# +# create certificate (use for test) +# Copyright (C) 2017 AleaJactaEst +# +# 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 . + +import unittest +import tempfile +try: + import pymanager.certificate as cert +except ImportError: + import sys + import os + sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + import pymanager.certificate as cert + + +class TestCertificate(unittest.TestCase): + def setUp(self): + self.openssl = '/usr/bin/openssl' + self.size_root = 4096 + self.size_appli = 4096 + self.size_child = 2048 + self.passroot = 'BadPasswordRoot' + self.passappli = 'BadPasswordApplication' + self.country_name = 'FR' + self.state_or_province_name = 'France' + self.locality_name = 'Paris' + self.organization_name = 'khanat' + self.common_name = 'khanat' + + def testInitialize(self): + logfile = tempfile.NamedTemporaryFile(suffix=".log") + loglevel = 'DEBUG' + show_log_console = True + workdir_cert_root = tempfile.mkdtemp(prefix='pymanager-certificate-root-') + workdir_cert_appli = tempfile.mkdtemp(prefix='pymanager-certificate-application-') + #ele = pymanager.certificate.Certificate() + + cert.root(logfile, + loglevel, + show_log_console, + workdir_cert_root, + workdir_cert_appli, + self.openssl, + self.size_root, + self.size_appli, + self.size_child, + self.passroot, + self.passappli, + self.country_name, + self.state_or_province_name, + self.locality_name, + self.organization_name, + self.common_name) + self.assertTrue(True) + + def testMain(self): + workdir_cert_root = tempfile.mkdtemp(prefix='pymanager-certificate-root-') + workdir_cert_appli = tempfile.mkdtemp(prefix='pymanager-certificate-application-') + args=['--workdir-cert-root', workdir_cert_root, '--workdir-cert-appli', workdir_cert_appli] + cert.main(args) + self.assertTrue(True) + + def testErrorLogLevel(self): + logfile = tempfile.NamedTemporaryFile(suffix=".log") + loglevel = 'BADVALUE' + show_log_console = False + workdir_cert_root = tempfile.mkdtemp(prefix='pymanager-certificate-root-') + workdir_cert_appli = tempfile.mkdtemp(prefix='pymanager-certificate-application-') + + with self.assertRaises(ValueError): + cert.root(logfile, + loglevel, + show_log_console, + workdir_cert_root, + workdir_cert_appli, + self.openssl, + self.size_root, + self.size_appli, + self.size_child, + self.passroot, + self.passappli, + self.country_name, + self.state_or_province_name, + self.locality_name, + self.organization_name, + self.common_name) + + def testErrorCreateNoneDirectory(self): + workdir_cert_root = tempfile.mkdtemp(prefix='pymanager-certificate-root-') + workdir_cert_appli = tempfile.mkdtemp(prefix='pymanager-certificate-application-') + + certificate = cert.Certificate(self.openssl, + workdir_cert_root, + workdir_cert_appli, + self.passroot, + self.passappli, + self.country_name, + self.state_or_province_name, + self.locality_name, + self.organization_name, + self.common_name, + self.size_root, + self.size_appli, + self.size_child + ) + with self.assertRaises(TypeError): + certificate.directory_create(None) + + def testErrorCreateBadDirectory(self): + workdir_cert_root = tempfile.mkdtemp(prefix='pymanager-certificate-root-') + workdir_cert_appli = tempfile.mkdtemp(prefix='pymanager-certificate-application-') + + certificate = cert.Certificate(self.openssl, + workdir_cert_root, + workdir_cert_appli, + self.passroot, + self.passappli, + self.country_name, + self.state_or_province_name, + self.locality_name, + self.organization_name, + self.common_name, + self.size_root, + self.size_appli, + self.size_child + ) + with self.assertRaises(FileNotFoundError): + certificate.directory_create("") + + def testErrorSendCommandOpenssl(self): + workdir_cert_root = tempfile.mkdtemp(prefix='pymanager-certificate-root-') + workdir_cert_appli = tempfile.mkdtemp(prefix='pymanager-certificate-application-') + + certificate = cert.Certificate("false", + workdir_cert_root, + workdir_cert_appli, + self.passroot, + self.passappli, + self.country_name, + self.state_or_province_name, + self.locality_name, + self.organization_name, + self.common_name, + self.size_root, + self.size_appli, + self.size_child + ) + with self.assertRaises(RuntimeError): + certificate.send_command_openssl("") + + def testError2SendCommandOpenssl(self): + workdir_cert_root = tempfile.mkdtemp(prefix='pymanager-certificate-root-') + workdir_cert_appli = tempfile.mkdtemp(prefix='pymanager-certificate-application-') + + certificate = cert.Certificate(workdir_cert_root, + workdir_cert_root, + workdir_cert_appli, + self.passroot, + self.passappli, + self.country_name, + self.state_or_province_name, + self.locality_name, + self.organization_name, + self.common_name, + self.size_root, + self.size_appli, + self.size_child + ) + with self.assertRaises(PermissionError): + certificate.send_command_openssl("") + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_client.py b/tests/test_client.py new file mode 100644 index 0000000..80177d0 --- /dev/null +++ b/tests/test_client.py @@ -0,0 +1,219 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# +# create certificate (use for test) +# Copyright (C) 2017 AleaJactaEst +# +# 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 . + +import unittest +import tempfile +import os +import configparser +from unittest.mock import patch + +try: + import pymanager.client as Client +except ImportError: + import sys + sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + import pymanager.client as Client + +try: + import pymanager.certificate as cert +except ImportError: + import sys + import os + sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + import pymanager.certificate as cert + +class TestManager(unittest.TestCase): + def setUp(self): + pass + + def test_cmp_to_key_correct(self): + list = [1, 1] + for key in sorted(list, key=Client.cmp_to_key()): + pass + + def test_cmp_to_key_bad_value(self): + list = ['A', '1'] + for key in sorted(list, key=Client.cmp_to_key()): + pass + + def test_cmp_to_key_all_int(self): + mref = Client.cmp_to_key() + m1 = mref(1) + m2 = mref(2) + if m1 == m2: + pass + if m1 >= m2: + pass + if m1 <= m2: + pass + if m1 < m2: + pass + if m1 > m2: + pass + if m1 != m2: + pass + + def test_cmp_to_key_all_string(self): + mref = Client.cmp_to_key() + m1 = mref('a') + m2 = mref('a') + if m1 == m2: + pass + if m1 >= m2: + pass + if m1 <= m2: + pass + if m1 < m2: + pass + if m1 > m2: + pass + if m1 != m2: + pass + + def test_load_config_file(self): + cfgfile = tempfile.NamedTemporaryFile(suffix="config.cfg", mode='w+t') + cfgfile.write('#\n[config:server]\nauthentification = No\n') + cfgfile.flush() + client = Client.Client(None, None, None) + client.load_config(cfgfile) + + def test_load_config_file_none(self): + client = Client.Client(None, None, None) + with self.assertRaises(ValueError): + client.load_config(None) + + def test_load_config(self): + config = configparser.ConfigParser() + config.add_section('config:server') + config.set('config:server', 'port', '8000') + config.set('config:server', 'keyfile', '/home/gameserver/ca/appli/private/serverkey.pem') + config.set('config:server', 'certfile', '/home/gameserver/ca/appli/certs/servercert.pem') + config.set('config:server', 'ca_cert', '/home/gameserver/ca/appli/certs/cachaincert.pem') + config.set('config:server', 'address', '') + config.set('config:server', 'authentification', 'yes') + config.add_section('config:client') + config.set('config:client', 'port', '8000') + config.set('config:client', 'keyfile', '/home/gameserver/ca/appli/private/clientkey.pem') + config.set('config:client', 'certfile', '/home/gameserver/ca/appli/certs/clientcert.pem') + config.set('config:client', 'ca_cert', '/home/gameserver/ca/appli/certs/cachaincert.pem') + config.set('config:client', 'address', '127.0.0.1') + config.add_section('command:test') + config.set('command:test', 'path', '/home/gameserver') + config.set('command:test', 'command', '/bin/sleep 10') + config.set('command:test', 'logsize', '10') + config.set('command:test', 'bufsize', '10') + config.add_section('config:user') + config.set('config:user', 'usename', 'filter_all, filter_admin') + try: + client = Client.Client(None, None, None) + client._load_config(config) + self.assertTrue(True) + except: + self.fail('Error detected on load config') + + def test_load_config_empty(self): + config = configparser.ConfigParser() + config.add_section('config:server') + config.set('config:server', 'authentification', 'yes') + config.add_section('config:client') + #config.set('config:client', 'port', '8000') + config.add_section('command:test') + config.set('command:test', 'path', '/home/gameserver') + try: + client = Client.Client(None, None, None) + client._load_config(config) + self.assertTrue(True) + except: + self.fail('Error detected on load config') + + def test_load_config_bad_param_port(self): + config = configparser.ConfigParser() + config.add_section('config:server') + config.add_section('config:client') + config.set('config:client', 'port', 'A') + client = Client.Client(None, None, None) + with self.assertRaises(Exception): + client._load_config(config) + + def test_init_client(self): + client = Client.Client('username', True, 'password') + + def test_init_client_missing_password(self): + with self.assertRaises(Exception): + client = Client.Client('username', True, None) + + def test_init_client_stdin_password_but_send_param(self): + with self.assertRaises(Exception): + client = Client.Client('username', False, 'password') + + @patch("getpass.getpass") + def test_init_client_stdin_password(self, getpass): + getpass.return_value = "password" + client = Client.Client('username', False, None) + + def test_https_init(self): + https = Client.HTTPSConnectionCertificate(None, None, None) + + @patch("socket.create_connection") + @patch("ssl.wrap_socket") + def test_https_connection(self, socket, ssl): + #socket.return_value = "" + #ssl.return_value = "" + https = Client.HTTPSConnectionCertificate(None, None, None) + https.connect() + + @patch("socket.create_connection") + @patch("ssl.wrap_socket") + def test_https_connection_with_ca(self, socket, ssl): + #socket.return_value = "" + #ssl.return_value = "" + https = Client.HTTPSConnectionCertificate(None, None, 'ca') + https.connect() + + def test_client_send_json(self): + workdir = tempfile.mkdtemp(prefix='test_client_send_json') + workdir_cert_root = tempfile.mkdtemp(prefix='pymanager-certificate-root-') + workdir_cert_appli = tempfile.mkdtemp(prefix='pymanager-certificate-application-') + args=['--workdir-cert-root', workdir_cert_root, '--workdir-cert-appli', workdir_cert_appli] + cert.main(args) + + config = configparser.ConfigParser() + config.add_section('config:server') + config.set('config:server', 'port', '8000') + config.set('config:server', 'keyfile', os.path.join(workdir_cert_appli, 'private', 'serverkey.pem')) + config.set('config:server', 'certfile', os.path.join(workdir_cert_appli, 'certs', 'servercert.pem')) + config.set('config:server', 'ca_cert', os.path.join(workdir_cert_appli, 'certs', 'cachaincert.pem')) + config.add_section('config:client') + config.set('config:client', 'port', '8000') + config.set('config:client', 'keyfile', os.path.join(workdir_cert_appli, 'private', 'clientkey.pem')) + config.set('config:client', 'certfile', os.path.join(workdir_cert_appli, 'certs', 'clientcert.pem')) + config.set('config:client', 'ca_cert', os.path.join(workdir_cert_appli, 'certs', 'cachaincert.pem')) + config.set('config:client', 'address', '127.0.0.1') + config.add_section('config:user') + config.set('config:user', 'usename', 'filter_all, filter_admin') + try: + client = Client.Client(None, None, None) + client._load_config(config) + self.assertTrue(True) + except: + self.fail('Error detected on load config') + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_client_manager.py b/tests/test_client_manager.py new file mode 100644 index 0000000..ab11eb5 --- /dev/null +++ b/tests/test_client_manager.py @@ -0,0 +1,83 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# +# create certificate (use for test) +# Copyright (C) 2017 AleaJactaEst +# +# 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 . + +import unittest +import tempfile +import os +import configparser +from unittest.mock import patch + +try: + import pymanager.client as Client +except ImportError: + import sys + sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + import pymanager.client as Client + +try: + import pymanager.certificate as cert +except ImportError: + import sys + import os + sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + import pymanager.certificate as cert + +try: + import pymanager.manager as Manager +except ImportError: + import sys + sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + import pymanager.manager as Manager + +class TestManager(unittest.TestCase): + def setUp(self): + pass + + def test_client_send_json(self): + workdir = tempfile.mkdtemp(prefix='test_client_send_json') + workdir_cert_root = tempfile.mkdtemp(prefix='pymanager-certificate-root-') + workdir_cert_appli = tempfile.mkdtemp(prefix='pymanager-certificate-application-') + args=['--workdir-cert-root', workdir_cert_root, '--workdir-cert-appli', workdir_cert_appli] + cert.main(args) + + config = configparser.ConfigParser() + config.add_section('config:server') + config.set('config:server', 'port', '8000') + config.set('config:server', 'keyfile', os.path.join(workdir_cert_appli, 'private', 'serverkey.pem')) + config.set('config:server', 'certfile', os.path.join(workdir_cert_appli, 'certs', 'servercert.pem')) + config.set('config:server', 'ca_cert', os.path.join(workdir_cert_appli, 'certs', 'cachaincert.pem')) + config.add_section('config:client') + config.set('config:client', 'port', '8000') + config.set('config:client', 'keyfile', os.path.join(workdir_cert_appli, 'private', 'clientkey.pem')) + config.set('config:client', 'certfile', os.path.join(workdir_cert_appli, 'certs', 'clientcert.pem')) + config.set('config:client', 'ca_cert', os.path.join(workdir_cert_appli, 'certs', 'cachaincert.pem')) + config.set('config:client', 'address', '127.0.0.1') + config.add_section('command:test') + config.set('command:test', 'path', workdir) + config.set('command:test', 'command', os.path.join(os.path.abspath(__file__), 'simulate_program.py')) + try: + client = Client.Client(None, None, None) + client._load_config(config) + self.assertTrue(True) + except: + self.fail('Error detected on load config') + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_manager.py b/tests/test_manager.py new file mode 100644 index 0000000..21b54b8 --- /dev/null +++ b/tests/test_manager.py @@ -0,0 +1,460 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# +# create certificate (use for test) +# Copyright (C) 2017 AleaJactaEst +# +# 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 . + +import unittest +import tempfile +import os +import configparser +import multiprocessing +import time +import re +import queue + +try: + import pymanager.manager as Manager +except ImportError: + import sys + sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + import pymanager.manager as Manager +#try: +# import pymanager.certificate as cert +#except ImportError: +# import sys +# import os +# sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +# import pymanager.certificate as cert + +class TestManager(unittest.TestCase): + def setUp(self): + self.openssl = '/usr/bin/openssl' + self.size_root = 4096 + self.size_appli = 4096 + self.size_child = 2048 + self.passroot = 'BadPasswordRoot' + self.passappli = 'BadPasswordApplication' + self.country_name = 'FR' + self.state_or_province_name = 'France' + self.locality_name = 'Paris' + self.organization_name = 'khanat' + self.common_name = 'khanat' + self.path = os.path.dirname(os.path.abspath(__file__)) + self.program = os.path.join(self.path, 'simulate_program.py') + self.badprogram = os.path.join(self.path, 'test.cfg') + + def testMain(self): + logfile = tempfile.NamedTemporaryFile(suffix=".log") + workdir_cert_root = tempfile.mkdtemp(prefix='pymanager-certificate-root-') + workdir_cert_appli = tempfile.mkdtemp(prefix='pymanager-certificate-application-') + +# cert.root(logfile, +# 'DEBUG', +# False, +# workdir_cert_root, +# workdir_cert_appli, +# self.openssl, +# self.size_root, +# self.size_appli, +# self.size_child, +# self.passroot, +# self.passappli, +# self.country_name, +# self.state_or_province_name, +# self.locality_name, +# self.organization_name, +# self.common_name) +# +# conf = tempfile.NamedTemporaryFile(suffix="test.cfg") +# conf.write(bytes( +#'[config]\n' +#'port = 8000\n' +#'keyfile = %s/private/serverkey.pem\n' +#'certfile = %s/private/servercert.pem\n' +#'ca_cert = %s/certs/cachaincert.pem\n' +#'address =\n' %(workdir_cert_appli, workdir_cert_appli, workdir_cert_appli ), 'UTF-8')) +# args=['--conf', conf] +# #manager.main(args) + self.assertTrue(True) + + def test_load_config(self): + config = configparser.ConfigParser() + config.add_section('config:server') + config.set('config:server', 'port', '8000') + config.set('config:server', 'keyfile', '/home/gameserver/ca/appli/private/serverkey.pem') + config.set('config:server', 'certfile', '/home/gameserver/ca/appli/certs/servercert.pem') + config.set('config:server', 'ca_cert', '/home/gameserver/ca/appli/certs/cachaincert.pem') + config.set('config:server', 'address', '') + config.set('config:server', 'authentification', 'yes') + config.add_section('config:client') + config.set('config:client', 'port', '8000') + config.set('config:client', 'keyfile', '/home/gameserver/ca/appli/private/clientkey.pem') + config.set('config:client', 'certfile', '/home/gameserver/ca/appli/certs/clientcert.pem') + config.set('config:client', 'ca_cert', '/home/gameserver/ca/appli/certs/cachaincert.pem') + config.set('config:client', 'address', '127.0.0.1') + config.add_section('command:test') + config.set('command:test', 'path', '/home/gameserver') + config.set('command:test', 'command', '/bin/sleep 10') + config.set('command:test', 'logsize', '10') + config.set('command:test', 'bufsize', '10') + config.add_section('config:user') + config.set('config:user', 'usename', 'filter_all, filter_admin') + try: + manager = Manager.Manager(False) + manager._load_config(config) + self.assertTrue(True) + except: + self.fail('Error detected on load config') + + def test_load_config2(self): + config = configparser.ConfigParser() + config.add_section('config:server') + config.set('config:server', 'authentification', 'no') + config.add_section('command:test') + config.set('command:test', 'path', '/home/gameserver') + config.set('command:test', 'command', '/bin/sleep 10') + try: + manager = Manager.Manager(False) + manager._load_config(config) + self.assertTrue(True) + except: + self.fail('Error detected on load config') + + def test_load_config_bad_param_logsize(self): + config = configparser.ConfigParser() + config.add_section('command:test') + config.set('command:test', 'command', '/bin/sleep 10') + config.set('command:test', 'logsize', 'bidon') + with self.assertRaises(ValueError): + manager = Manager.Manager(False) + manager._load_config(config) + self.assertTrue(True) + + def test_load_config_bad_param_bufsize(self): + config = configparser.ConfigParser() + config.add_section('command:test') + config.set('command:test', 'command', '/bin/sleep 10') + config.set('command:test', 'bufsize', 'bidon') + with self.assertRaises(ValueError): + manager = Manager.Manager(False) + manager._load_config(config) + self.assertTrue(True) + + def test_load_config_empty(self): + config = configparser.ConfigParser() + config.add_section('config:server') + config.add_section('config:client') + config.set('config:client', 'port', '8000') + config.set('config:client', 'keyfile', '/home/gameserver/ca/appli/private/clientkey.pem') + config.set('config:client', 'certfile', '/home/gameserver/ca/appli/certs/clientcert.pem') + config.set('config:client', 'ca_cert', '/home/gameserver/ca/appli/certs/cachaincert.pem') + config.set('config:client', 'address', '127.0.0.1') + config.add_section('config:user') + config.add_section('command:test') + config.set('command:test', 'command', '/bin/sleep 10') + try: + manager = Manager.Manager(False) + manager._load_config(config) + self.assertTrue(True) + except: + self.fail('Error detected on load config') + + def test_load_config_file(self): + cfgfile = tempfile.NamedTemporaryFile(suffix="config.cfg", mode='w+t') + cfgfile.write('#\n[config:server]\nauthentification = No\n') + cfgfile.flush() + try: + manager = Manager.Manager(False) + manager.load_config(cfgfile) + self.assertTrue(True) + except: + self.fail('Error detected on load configuration') + + def test_load_config_file_none(self): + with self.assertRaises(ValueError): + manager = Manager.Manager(False) + manager.load_config(None) + + def test_load_password_file(self): + pwdfile = tempfile.NamedTemporaryFile(suffix="password.cfg", mode='w+t') + + config = configparser.ConfigParser() + config.add_section('config:server') + config.set('config:server', 'authentification', 'yes') + config.set('config:server', 'passwordfile', pwdfile.name) + pwdfile.write('username:$2a$12$2C97xW0KC/vFp3YyjlOgU.fWXJ3EiGT2Ihb0SWN9Mw0XI4WngiUqS\n\nusername2:badhash\n') + pwdfile.flush() + try: + manager = Manager.Manager(False) + manager._load_config(config) + manager.load_password() + self.assertTrue(True) + except: + self.fail('Error detected on load password') + + def test_constructor_manager_command(self): + try: + logsize = 10 + bufsize = 10 + queueIn = multiprocessing.Queue() + queueOut = multiprocessing.Queue() + event = multiprocessing.Event() + manageCommand = Manager.ManageCommand('test_constructor_manager_command', + self.program, + self.path, + logsize, + bufsize, + queueIn, + queueOut, + event) + manageCommand.list_thread() + self.assertTrue(True) + except: + self.fail('Error initialize object ManageCommand') + + def test_execute_manager_command(self): + try: + logsize = 10 + bufsize = 10 + queueIn = multiprocessing.Queue() + queueOut = multiprocessing.Queue() + event = multiprocessing.Event() + manageCommand = Manager.ManageCommand('test_execute_manager_command', + self.program, + self.path, + logsize, + bufsize, + queueIn, + queueOut, + event) + manageCommand.status() + manageCommand.start() + manageCommand.status() + manageCommand.start() + foundEnd = re.compile('.*(Started).*') + foundY = re.compile('.*(sendToStdinY).*') + loop = 10 + while loop > 0: + time.sleep(1) + out = manageCommand.getlog(0) + if foundEnd.match(out): + break + loop -= 1 + if not foundEnd.match(out): + manageCommand.stop() + self.assertTrue(False, 'Missing message in log') + manageCommand.list_thread() + retA = manageCommand.action("sendToStdinA") + self.assertEqual(retA, "ok", 'Error impossible to send to stdin') + for i in range(0, 120): + retX = manageCommand.action("sendToStdin%d" % i) + self.assertEqual(retX, "ok", 'Error impossible to send to stdin') + retY = manageCommand.action("sendToStdinY") + self.assertEqual(retY, "ok", 'Error impossible to send to stdin') + loop = 10 + while loop > 0: + time.sleep(1) + out = manageCommand.getlog(0) + if foundY.match(out): + break + loop -= 1 + if not foundY.match(out): + manageCommand.stop() + self.assertTrue(False, 'Missing message in log') + + manageCommand.stop() + manageCommand.status() + retZ = manageCommand.action("sendToStdinZ") + manageCommand.stop() + self.assertEqual(retZ, "ko", 'Error send to stdin when process is down') + self.assertTrue(True) + except: + self.fail('Error initialize object ManageCommand') + + def test_execute_crash_manager_command(self): + try: + logsize = 10 + bufsize = 10 + queueIn = multiprocessing.Queue() + queueOut = multiprocessing.Queue() + event = multiprocessing.Event() + manageCommand = Manager.ManageCommand('test_execute_crash_manager_command', + self.program + ' --no-loop --timeout 1', + self.path, + logsize, + bufsize, + queueIn, + queueOut, + event) + manageCommand.start() + time.sleep(3) + manageCommand.status() + manageCommand.list_thread() + manageCommand.stop() + manageCommand.status() + self.assertTrue(True) + except: + self.fail('Error initialize object ManageCommand') + + def test_execute_not_kill_manager_command(self): + try: + logsize = 10 + bufsize = 10 + queueIn = multiprocessing.Queue() + queueOut = multiprocessing.Queue() + event = multiprocessing.Event() + manageCommand = Manager.ManageCommand('test_execute_not_kill_manager_command', + self.program + " --disable-kill", + self.path, + logsize, + bufsize, + queueIn, + queueOut, + event, + maxWaitEnd = 2) + manageCommand.start() + time.sleep(1) + manageCommand.status() + manageCommand.list_thread() + manageCommand.stop() + manageCommand.status() + self.assertTrue(True) + except: + self.fail('Error initialize object ManageCommand') + + def test_execute_command_crashed(self): + try: + logsize = 10 + bufsize = 10 + queueIn = multiprocessing.Queue() + queueOut = multiprocessing.Queue() + event = multiprocessing.Event() + manageCommand = Manager.ManageCommand('test_execute_command_crashed', + self.program + " --no-loop --timeout=1", + self.path, + logsize, + bufsize, + queueIn, + queueOut, + event, + waitDelay = 10) + manageCommand.start() + time.sleep(5) + manageCommand.start() + time.sleep(5) + manageCommand.stop() + self.assertTrue(True) + except: + self.fail('Error initialize object ManageCommand') + + def test_execute_command_file_not_found(self): + try: + logsize = 10 + bufsize = 10 + queueIn = multiprocessing.Queue() + queueOut = multiprocessing.Queue() + event = multiprocessing.Event() + manageCommand = Manager.ManageCommand('test_execute_command_file_not_found', + self.program + "_not_exist", + self.path, + logsize, + bufsize, + queueIn, + queueOut, + event) + ret = manageCommand.start() + manageCommand.stop() + self.assertEqual(ret, "crashed", 'Error object not generate error when program not exist') + except: + self.fail('Error initialize object ManageCommand') + + def test_execute_command_permission(self): + try: + logsize = 10 + bufsize = 10 + queueIn = multiprocessing.Queue() + queueOut = multiprocessing.Queue() + event = multiprocessing.Event() + manageCommand = Manager.ManageCommand('test_execute_command_permission', + self.badprogram, + self.path, + logsize, + bufsize, + queueIn, + queueOut, + event) + ret = manageCommand.start() + manageCommand.stop() + self.assertEqual(ret, "crashed", 'Error object not generate error when bad permission') + except: + self.fail('Error initialize object ManageCommand') + + def _runCommand(self, name, command, path, logsize, bufsize, queueIn, queueOut, event): + """ + Thread to manage khaganat program + """ + manageCommand = Manager.ManageCommand(name=name, + command=command, + path=path, + logsize=logsize, + bufsize=bufsize, + queueIn=queueIn, + queueOut=queueOut, + event=event) + manageCommand.run() + +# def test_run_manager_command(self): +# # Doesn't work (we need enable --concurrency=multiprocessing on coverage command but we need coverage 4.0) +# logsize = 10 +# bufsize = 10 +# queueIn = multiprocessing.Queue() +# queueOut = multiprocessing.Queue() +# event = multiprocessing.Event() +# threadCommand = multiprocessing.Process(target=self._runCommand, +# args=('test_run_manager_command', +# self.program, +# self.path, +# logsize, +# bufsize, +# queueIn, +# queueOut, +# event)) +# threadCommand.start() +# +# event.set() +# queueIn.put("START") +# item = queueOut.get(timeout=4) +# self.assertEqual(item, "started", 'Error impossible to start program') +# time.sleep(1) +# event.set() +# queueIn.put("STATUS") +# item = queueOut.get(timeout=4) +# self.assertEqual(item, "started", 'Error impossible to start program') +# time.sleep(1) +# print("-" * 80, "shutdown" ) +# event.set() +# queueIn.put("SHUTDOWN") +# with self.assertRaises(queue.Empty): +# item = queueOut.get(timeout=4) +# print("-" * 80, "wait thread" ) +# threadCommand.join() +# +# self.assertTrue(True) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_password.py b/tests/test_password.py new file mode 100644 index 0000000..cdfdad9 --- /dev/null +++ b/tests/test_password.py @@ -0,0 +1,197 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# +# create certificate (use for test) +# Copyright (C) 2017 AleaJactaEst +# +# 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 . + +import unittest +import tempfile +import os +from unittest.mock import patch + + +try: + import pymanager.password as Password +except ImportError: + import sys + sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + import pymanager.password as Password + + +class TestPassword(unittest.TestCase): + def setUp(self): + pass + + def test_init_without_password_but_send(self): + pwdfile = tempfile.NamedTemporaryFile(suffix="passwordfile.tmp", mode='w+t') + pwdfile.write('username:$2a$12$2C97xW0KC/vFp3YyjlOgU.fWXJ3EiGT2Ihb0SWN9Mw0XI4WngiUqS\n\n') + pwdfile.flush() + with self.assertRaises(Exception): + password = Password.PasswordFile(pwdfile.name, False, False, 'username', 'password') + password.verify() + + def test_init_missing_password(self): + pwdfile = tempfile.NamedTemporaryFile(suffix="passwordfile.tmp", mode='w+t') + pwdfile.write('username:$2a$12$2C97xW0KC/vFp3YyjlOgU.fWXJ3EiGT2Ihb0SWN9Mw0XI4WngiUqS\n\n') + pwdfile.flush() + with self.assertRaises(Exception): + password = Password.PasswordFile(pwdfile.name, False, True, 'username', None) + password.verify() + + def test_init_without_password(self): + pwdfile = tempfile.NamedTemporaryFile(suffix="passwordfile.tmp", mode='w+t') + pwdfile.write('username:$2a$12$2C97xW0KC/vFp3YyjlOgU.fWXJ3EiGT2Ihb0SWN9Mw0XI4WngiUqS\n\n') + pwdfile.flush() + password = Password.PasswordFile(pwdfile.name, False, False, 'username', None) + + def test_init_create_file(self): + tmpDir = tempfile.mkdtemp(prefix='password-') + pwdfile = os.path.join(tmpDir, 'temp') + password = Password.PasswordFile(pwdfile , True, True, 'username', 'password') + + def test_init_create_file_impossible(self): + tmpDir = tempfile.mkdtemp(prefix='password-') + pwdfile = os.path.join('not', 'exist', 'directory', 'temp') + with self.assertRaises(FileNotFoundError): + password = Password.PasswordFile(pwdfile , True, True, 'username', 'password') + + def test_init_create_file_impossible(self): + tmpDir = tempfile.mkdtemp(prefix='password-') + pwdfile = os.path.join('not', 'exist', 'directory', 'temp') + with self.assertRaises(Exception): + password = Password.PasswordFile(pwdfile , False, True, 'username', 'password') + + def test_verify(self): + pwdfile = tempfile.NamedTemporaryFile(suffix="passwordfile.tmp", mode='w+t') + pwdfile.write('username:$2a$12$2C97xW0KC/vFp3YyjlOgU.fWXJ3EiGT2Ihb0SWN9Mw0XI4WngiUqS\n\n') + pwdfile.flush() + password = Password.PasswordFile(pwdfile.name, False, True, 'username', 'password') + password.verify() + + def test_verify_bad_password(self): + pwdfile = tempfile.NamedTemporaryFile(suffix="passwordfile.tmp", mode='w+t') + pwdfile.write('username:$2a$12$2C97xW0KC/vFp3YyjlOgU.fWXJ3EiGT2Ihb0SWN9Mw0XI4WngiUqS\n\n') + pwdfile.flush() + with self.assertRaises(Exception): + password = Password.PasswordFile(pwdfile.name, False, True, 'username', 'BadPassword') + password.verify() + + def test_save(self): + pwdfile = tempfile.NamedTemporaryFile(suffix="passwordfile.tmp", mode='w+t') + pwdfile.write('username:$2a$12$2C97xW0KC/vFp3YyjlOgU.fWXJ3EiGT2Ihb0SWN9Mw0XI4WngiUqS\n\n') + pwdfile.flush() + password = Password.PasswordFile(pwdfile.name, False, True, 'MyAccount', 'MyPassword') + password.save() + + def test_update(self): + pwdfile = tempfile.NamedTemporaryFile(suffix="passwordfile.tmp", mode='w+t') + pwdfile.write('username:$2a$12$2C97xW0KC/vFp3YyjlOgU.fWXJ3EiGT2Ihb0SWN9Mw0XI4WngiUqS\n\n') + pwdfile.flush() + + password = Password.PasswordFile(pwdfile.name, False, True, 'username', 'MyPassword2') + password.update() + password.save() + + password = Password.PasswordFile(pwdfile.name, False, True, 'username', 'MyPassword2') + password.verify() + + with self.assertRaises(Exception): + password = Password.PasswordFile(pwdfile.name, False, True, 'username', 'BadPassword') + password.verify() + + def test_delete(self): + pwdfile = tempfile.NamedTemporaryFile(suffix="passwordfile.tmp", mode='w+t') + pwdfile.write('username:$2a$12$2C97xW0KC/vFp3YyjlOgU.fWXJ3EiGT2Ihb0SWN9Mw0XI4WngiUqS\n\n') + pwdfile.write('username2:$2a$12$2C97xW0KC/vFp3YyjlOgU.fWXJ3EiGT2Ihb0SWN9Mw0XI4WngiUqS\n\n') + pwdfile.flush() + + password = Password.PasswordFile(pwdfile.name, False, True, 'username', 'MyPassword') + password.delete() + + def test_run_update(self): + pwdfile = tempfile.NamedTemporaryFile(suffix="passwordfile.tmp", mode='w+t') + pwdfile.write('username:$2a$12$2C97xW0KC/vFp3YyjlOgU.fWXJ3EiGT2Ihb0SWN9Mw0XI4WngiUqS\n\n') + pwdfile.flush() + Password.root(pwdfile.name, False, True, 'username', 'password', False, False) + + def test_run_verify(self): + pwdfile = tempfile.NamedTemporaryFile(suffix="passwordfile.tmp", mode='w+t') + pwdfile.write('username:$2a$12$2C97xW0KC/vFp3YyjlOgU.fWXJ3EiGT2Ihb0SWN9Mw0XI4WngiUqS\n\n') + pwdfile.flush() + Password.root(pwdfile.name, False, True, 'username', 'password', False, True) + + def test_run_verify_bad_password(self): + pwdfile = tempfile.NamedTemporaryFile(suffix="passwordfile.tmp", mode='w+t') + pwdfile.write('username:$2a$12$2C97xW0KC/vFp3YyjlOgU.fWXJ3EiGT2Ihb0SWN9Mw0XI4WngiUqS\n\n') + pwdfile.flush() + with self.assertRaises(Exception): + Password.root(pwdfile.name, False, True, 'username', 'BadPassword', False, True) + + def test_run_verify_bad_account(self): + pwdfile = tempfile.NamedTemporaryFile(suffix="passwordfile.tmp", mode='w+t') + pwdfile.write('username:$2a$12$2C97xW0KC/vFp3YyjlOgU.fWXJ3EiGT2Ihb0SWN9Mw0XI4WngiUqS\n\n') + pwdfile.flush() + with self.assertRaises(KeyError): + Password.root(pwdfile.name, False, True, 'BadUsername', 'password', False, True) + + def test_run_delete(self): + pwdfile = tempfile.NamedTemporaryFile(suffix="passwordfile.tmp", mode='w+t') + pwdfile.write('username:$2a$12$2C97xW0KC/vFp3YyjlOgU.fWXJ3EiGT2Ihb0SWN9Mw0XI4WngiUqS\n\n') + pwdfile.flush() + Password.root(pwdfile.name, False, True, 'username', 'password', True, False) + + def test_main(self): + pwdfile = tempfile.NamedTemporaryFile(suffix="passwordfile.tmp", mode='w+t') + pwdfile.write('username:$2a$12$2C97xW0KC/vFp3YyjlOgU.fWXJ3EiGT2Ihb0SWN9Mw0XI4WngiUqS\n\n') + pwdfile.flush() + args = ['-v', '-b', pwdfile.name, 'username', 'password'] + print(args) + Password.main(args=args) + + @patch("getpass.getpass") + def test_run_verify_stdin_password(self, getpass): + pwdfile = tempfile.NamedTemporaryFile(suffix="passwordfile.tmp", mode='w+t') + pwdfile.write('username:$2a$12$2C97xW0KC/vFp3YyjlOgU.fWXJ3EiGT2Ihb0SWN9Mw0XI4WngiUqS\n\n') + pwdfile.flush() + + getpass.return_value = "password" + password = Password.PasswordFile(pwdfile.name, False, False, 'username', None) + password.verify() + + @patch("getpass.getpass") + def test_run_update_stdin_password(self, getpass): + pwdfile = tempfile.NamedTemporaryFile(suffix="passwordfile.tmp", mode='w+t') + pwdfile.write('username:$2a$12$2C97xW0KC/vFp3YyjlOgU.fWXJ3EiGT2Ihb0SWN9Mw0XI4WngiUqS\n\n') + pwdfile.flush() + + getpass.return_value = "password" + password = Password.PasswordFile(pwdfile.name, False, False, 'username', None) + password.update() + + @patch("getpass.getpass") + def test_run_update_stdin_password2(self, getpass): + pwdfile = tempfile.NamedTemporaryFile(suffix="passwordfile.tmp", mode='w+t') + pwdfile.write('username:$2a$12$2C97xW0KC/vFp3YyjlOgU.fWXJ3EiGT2Ihb0SWN9Mw0XI4WngiUqS\n\n') + pwdfile.flush() + + getpass.side_effect = ["password", "password2"] + password = Password.PasswordFile(pwdfile.name, False, False, 'username', None) + with self.assertRaises(Exception): + password.update() + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_pycodestyle.py b/tests/test_pycodestyle.py new file mode 100644 index 0000000..37b14f3 --- /dev/null +++ b/tests/test_pycodestyle.py @@ -0,0 +1,36 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# +# check code style +# Copyright (C) 2017 AleaJactaEst +# +# 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 . + +import unittest +try: + import pycodestyle +except ImportError: + import pep8 as pycodestyle + + +class TestPythonCodeStyle(unittest.TestCase): + + def test_python_code_style(self): + style = pycodestyle.StyleGuide(quiet=False, config_file='setup.cfg') + result = style.check_files(['pymanager']) + self.assertEqual(result.total_errors, 0, + "Found code style errors (and warnings).") + +if __name__ == '__main__': + unittest.main()