commit 25ad05cce36d55ce1c55fd7e70a3ab74e321b66e Author: James Shubin Date: Fri Sep 25 01:07:56 2015 -0400 Initial public commit of mgmt This is a prototype that i'm attempting to "release early". Expect a lot of changes! It is intended to be a config management tool that will: * be event based * execute actions in parallel * function as a distributed system There are a bunch more design ideas going into this, please stay tuned! diff --git a/.ackrc b/.ackrc new file mode 100644 index 00000000..63d5baaa --- /dev/null +++ b/.ackrc @@ -0,0 +1,2 @@ +--ignore-dir=old/ +--ignore-dir=tmp/ diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..bb37f0bc --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +mgmt-documentation.pdf +old/ +tmp/ diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 00000000..42cf1969 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,7 @@ +This is a list of authors/contributors to the mgmt project. +If you're a contributor, please send a patch with your name. +If you appreciate the work of one of the contributors, thank them a beverage! +For a more exhaustive list please run: git log --format='%aN' | sort -u +This list is sorted alphabetically by first name. + +James Shubin diff --git a/COPYING b/COPYING new file mode 100644 index 00000000..dba13ed2 --- /dev/null +++ b/COPYING @@ -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. + + + Copyright (C) + + 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/COPYRIGHT b/COPYRIGHT new file mode 100644 index 00000000..e3d70a2b --- /dev/null +++ b/COPYRIGHT @@ -0,0 +1,16 @@ +Mgmt +Copyright (C) 2013-2015+ James Shubin and the project contributors +Written by James Shubin and the project contributors + +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/DOCUMENTATION.md b/DOCUMENTATION.md new file mode 100644 index 00000000..da6cc52b --- /dev/null +++ b/DOCUMENTATION.md @@ -0,0 +1,128 @@ +#mgmt + + + +##mgmt by [James](https://ttboj.wordpress.com/) +####Available from: +####[https://github.com/purpleidea/mgmt/](https://github.com/purpleidea/mgmt/) + +####This documentation is available in: [Markdown](https://github.com/purpleidea/mgmt/blob/master/DOCUMENTATION.md) or [PDF](https://pdfdoc-purpleidea.rhcloud.com/pdf/https://github.com/purpleidea/mgmt/blob/master/DOCUMENTATION.md) format. + +####Table of Contents + +1. [Overview](#overview) +2. [Project description - What the project does](#project-description) +3. [Setup - Getting started with mgmt](#setup) +4. [Usage/FAQ - Notes on usage and frequently asked questions](#usage-and-frequently-asked-questions) +5. [Reference - Detailed reference](#reference) + * [graph.yaml](#graph.yaml) + * [Command line](#command-line) +6. [Examples - Example configurations](#examples) +7. [Development - Background on module development and reporting bugs](#development) +8. [Authors - Authors and contact information](#authors) + +##Overview + +The `mgmt` tool is a research prototype to demonstrate next generation config +management techniques. Hopefully it will evolve into a useful, robust tool. + +##Project Description + +The mgmt tool is a distributed, event driven, config management tool, that +supports parallel execution, and librarification to be used as the management +foundation in and for, new and existing software. + +##Setup + +During this prototype phase, the tool can be run out of the source directory. +You'll probably want to use ```./run.sh run --file examples/graph1.yaml``` to +get started. Beware that this _can_ cause data loss. Understand what you're +doing first, or perform these actions in a virtual environment such as the one +provided by [Oh-My-Vagrant](https://github.com/purpleidea/oh-my-vagrant). + +##Usage and frequently asked questions +(Send your questions as a patch to this FAQ! I'll review it, merge it, and +respond by commit with the answer.) + +###Why did you start this project? + +I wanted a next generation config management solution that didn't have all of +the design flaws or limitations that the current generation of tools do, and no +tool existed! + +###You didn't answer my question, or I have a question! + +It's best to ask on [IRC](https://webchat.freenode.net/?channels=#mgmtconfig) +to see if someone can help you. Once we get a big enough community going, we'll +add a mailing list. If you don't get any response from the above, you can +contact me through my [technical blog](https://ttboj.wordpress.com/contact/) +and I'll do my best to help. If you have a good question, please add it as a +patch to this documentation. I'll merge your question, and add a patch with the +answer! + +##Reference +Please note that there are a number of undocumented options. For more +information on these options, please view the source at: +[https://github.com/purpleidea/mgmt/](https://github.com/purpleidea/mgmt/). +If you feel that a well used option needs documenting here, please patch it! + +###Overview of reference +* [graph.yaml](#graph.yaml): Main graph definition file. +* [Command line](#command-line): Command line parameters. + +###graph.yaml +This is the compiled graph definition file. The format is currently +undocumented, but by looking through the [examples/](https://github.com/purpleidea/mgmt/tree/master/examples) +you can probably figure out most of it, as it's fairly intuitive. + +###Command line +The main interface to the `mgmt` tool is the command line. For the most recent +documentation, please run `mgmt --help`. + +####`--file ` +Point to a graph file to run. + +##Examples +For example configurations, please consult the [examples/](https://github.com/purpleidea/mgmt/tree/master/examples) directory in the git +source repository. It is available from: + +[https://github.com/purpleidea/mgmt/tree/master/examples](https://github.com/purpleidea/mgmt/tree/master/examples) + +##Development + +This is a project that I started in my free time in 2013. Development is driven +by all of our collective patches! Dive right in, and start hacking! +Please contact me if you'd like to invite me to speak about this at your event. + +You can follow along [on my technical blog](https://ttboj.wordpress.com/). + +To report any bugs, please file a ticket at: [https://github.com/purpleidea/mgmt/issues](https://github.com/purpleidea/mgmt/issues). + +##Authors + +Copyright (C) 2013-2015+ James Shubin and the project contributors + +Please see the +[AUTHORS](https://github.com/purpleidea/mgmt/tree/master/AUTHORS) file +for more information. + +* [github](https://github.com/purpleidea/) +* [@purpleidea](https://twitter.com/#!/purpleidea) +* [https://ttboj.wordpress.com/](https://ttboj.wordpress.com/) diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..c24f5161 --- /dev/null +++ b/Makefile @@ -0,0 +1,45 @@ +SHELL = /bin/bash +.PHONY: all version run race build clean test format docs +.SILENT: clean + +VERSION := $(shell git describe --match '[0-9]*\.[0-9]*\.[0-9]*' --tags --dirty) +PROGRAM := $(notdir $(CURDIR)) + +all: docs + +# show the current version +version: + @echo $(VERSION) + +run: + find -maxdepth 1 -type f -name '*.go' -not -name '*_test.go' | xargs go run -ldflags "-X main.version $(VERSION) -X main.program $(PROGRAM)" + +# include race test +race: + find -maxdepth 1 -type f -name '*.go' -not -name '*_test.go' | xargs go run -race -ldflags "-X main.version $(VERSION) -X main.program $(PROGRAM)" + +build: mgmt + +mgmt: main.go + go build -ldflags "-X main.version $(VERSION) -X main.program $(PROGRAM)" + +clean: + [ ! -e mgmt ] || rm mgmt + +test: + ./test.sh + ./test/test-gofmt.sh + ./test/test-yamlfmt.sh + go test + #go test ./pgraph + go test -race + #go test -race ./pgraph + +format: + find -type f -name '*.go' -not -path './old/*' -not -path './tmp/*' -exec gofmt -w {} \; + find -type f -name '*.yaml' -not -path './old/*' -not -path './tmp/*' -exec ruby -e "require 'yaml'; x=YAML.load_file('{}').to_yaml; File.open('{}', 'w').write x" \; + +docs: mgmt-documentation.pdf + +mgmt-documentation.pdf: DOCUMENTATION.md + pandoc DOCUMENTATION.md -o 'mgmt-documentation.pdf' diff --git a/README b/README new file mode 100644 index 00000000..1c3c6ec0 --- /dev/null +++ b/README @@ -0,0 +1 @@ +Please see README.md diff --git a/README.md b/README.md new file mode 100644 index 00000000..4cc83adc --- /dev/null +++ b/README.md @@ -0,0 +1,36 @@ +# *mgmt*: This is: mgmt! + +[![Build Status](https://secure.travis-ci.org/purpleidea/mgmt.png)](http://travis-ci.org/purpleidea/mgmt) + +## Documentation: +Please see: [DOCUMENTATION.md](DOCUMENTATION.md) or [PDF](https://pdfdoc-purpleidea.rhcloud.com/pdf/https://github.com/purpleidea/mgmt/blob/master/DOCUMENTATION.md). + +## Questions: +Come join us in [#mgmtconfig](https://webchat.freenode.net/?channels=#mgmtconfig) on Freenode! + +## Examples: +Please look in the [examples/](examples/) folder for usage. If none exist, please contribute one! + +## Notes: +* This is currently a research project into next generation config management technologies! +* This is my first complex project in golang, please notify me of any issues. +* I have some well thought out designs for the future of this project, which I'll try and write up clearly and publish as soon as possible. +* Please don't expect stable interfaces, code, or any data safety. +* This design is the result of ideas I've had from hacking on advanced config management projects. +* I first started hacking on this in ~2013, even though I had very little time for it. +* I couldn't think of a good name for the project, so it's now being called `mgmt` until someone contributes a better one! +* I've published a number of articles about this tool: + * TODO +* There are some screencasts available: + * TODO + +## Dependencies: +* golang (available in most distros) +* pandoc (for building a pdf of the documentation) + +## Patches: +We'd love to have your patch! Please send it by email, or as a pull request. + +## + +Happy hacking! diff --git a/THANKS b/THANKS new file mode 100644 index 00000000..0c40c582 --- /dev/null +++ b/THANKS @@ -0,0 +1,16 @@ +Thanks to everyone who contributed to and supported this project in some way! +An exhaustive list is impossible, but a few honourable mentions are required. + +Thanks (alphabetically) to: + +Brian Bouters - For good config mgmt chats and encouraging me to release early. + +Chris Wright - For encouraging me to continue work on my prototype. + +Daniel Riek - For supporting and sheltering this project from bureaucracy. + +Jeff Darcy - For some algorithm recommendations, and NACKing my TopoSort idea! + +Red Hat, inc. - For paying my salary, thus financially supporting my hacking. + +And many others... diff --git a/config.go b/config.go new file mode 100644 index 00000000..8a3f6606 --- /dev/null +++ b/config.go @@ -0,0 +1,126 @@ +// Mgmt +// Copyright (C) 2013-2015+ James Shubin and the project contributors +// Written by James Shubin and the project contributors +// +// 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 . + +package main + +import ( + "errors" + "gopkg.in/yaml.v2" + "io/ioutil" + "log" +) + +type noopTypeConfig struct { + Name string `yaml:"name"` +} + +type fileTypeConfig struct { + Name string `yaml:"name"` + Path string `yaml:"path"` + Content string `yaml:"content"` + State string `yaml:"state"` +} + +type serviceTypeConfig struct { + Name string `yaml:"name"` + State string `yaml:"state"` + Startup string `yaml:"startup"` +} + +type vertexConfig struct { + Type string `yaml:"type"` + Name string `yaml:"name"` +} + +type edgeConfig struct { + Name string `yaml:"name"` + From vertexConfig `yaml:"from"` + To vertexConfig `yaml:"to"` +} + +type graphConfig struct { + Graph string `yaml:"graph"` + Types struct { + Noop []noopTypeConfig `yaml:"noop"` + File []fileTypeConfig `yaml:"file"` + Service []serviceTypeConfig `yaml:"service"` + } `yaml:"types"` + Edges []edgeConfig `yaml:"edges"` + Comment string `yaml:"comment"` +} + +func (c *graphConfig) Parse(data []byte) error { + if err := yaml.Unmarshal(data, c); err != nil { + return err + } + if c.Graph == "" { + return errors.New("Graph config: invalid `graph`") + } + return nil +} + +func GraphFromConfig(filename string) *Graph { + + var NoopMap map[string]*Vertex = make(map[string]*Vertex) + var FileMap map[string]*Vertex = make(map[string]*Vertex) + var ServiceMap map[string]*Vertex = make(map[string]*Vertex) + + var lookup map[string]map[string]*Vertex = make(map[string]map[string]*Vertex) + lookup["noop"] = NoopMap + lookup["file"] = FileMap + lookup["service"] = ServiceMap + + data, err := ioutil.ReadFile(filename) + if err != nil { + log.Fatal(err) + } + + var config graphConfig + if err := config.Parse(data); err != nil { + log.Fatal(err) + } + //fmt.Printf("%+v\n", config) // debug + + g := NewGraph(config.Graph) + + for _, t := range config.Types.Noop { + NoopMap[t.Name] = NewVertex(t.Name, "noop") + // FIXME: duplicate of name stored twice... where should it go? + NoopMap[t.Name].Associate(NewNoopType(t.Name)) + g.AddVertex(NoopMap[t.Name]) // call standalone in case not part of an edge + } + + for _, t := range config.Types.File { + FileMap[t.Name] = NewVertex(t.Name, "file") + // FIXME: duplicate of name stored twice... where should it go? + FileMap[t.Name].Associate(NewFileType(t.Name, t.Path, t.Content, t.State)) + g.AddVertex(FileMap[t.Name]) // call standalone in case not part of an edge + } + + for _, t := range config.Types.Service { + ServiceMap[t.Name] = NewVertex(t.Name, "service") + // FIXME: duplicate of name stored twice... where should it go? + ServiceMap[t.Name].Associate(NewServiceType(t.Name, t.State, t.Startup)) + g.AddVertex(ServiceMap[t.Name]) // call standalone in case not part of an edge + } + + for _, e := range config.Edges { + g.AddEdge(lookup[e.From.Type][e.From.Name], lookup[e.To.Type][e.To.Name], NewEdge(e.Name)) + } + + return g +} diff --git a/event.go b/event.go new file mode 100644 index 00000000..9f026314 --- /dev/null +++ b/event.go @@ -0,0 +1,36 @@ +// Mgmt +// Copyright (C) 2013-2015+ James Shubin and the project contributors +// Written by James Shubin and the project contributors +// +// 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 . + +package main + +import ( + "code.google.com/p/go-uuid/uuid" +) + +type Event struct { + uuid string + Name string + Type string +} + +func NewEvent(name, t string) *Event { + return &Event{ + uuid: uuid.New(), + Name: name, + Type: t, + } +} diff --git a/examples/graph1.yaml b/examples/graph1.yaml new file mode 100644 index 00000000..1ccff29c --- /dev/null +++ b/examples/graph1.yaml @@ -0,0 +1,41 @@ +--- +graph: mygraph +types: + noop: + - name: noop1 + file: + - name: file1 + path: /tmp/mgmt/f1 + content: | + i am f1 + state: exists + - name: file2 + path: /tmp/mgmt/f2 + content: | + i am f2 + state: exists + - name: file3 + path: /tmp/mgmt/f3 + content: | + i am f3 + state: exists + - name: file4 + path: /tmp/mgmt/f4 + content: | + i am f4 and i should not be here + state: absent +edges: +- name: e1 + from: + type: file + name: file1 + to: + type: file + name: file2 +- name: e2 + from: + type: file + name: file2 + to: + type: file + name: file3 diff --git a/examples/graph2.yaml b/examples/graph2.yaml new file mode 100644 index 00000000..cf316909 --- /dev/null +++ b/examples/graph2.yaml @@ -0,0 +1,30 @@ +--- +graph: mygraph +types: + noop: + - name: noop1 + file: + - name: file1 + path: /tmp/mgmt/f1 + content: | + i am f1 + state: exists + service: + - name: purpleidea + state: running + startup: enabled +edges: +- name: e1 + from: + type: noop + name: noop1 + to: + type: file + name: file1 +- name: e2 + from: + type: file + name: file1 + to: + type: service + name: purpleidea diff --git a/file.go b/file.go new file mode 100644 index 00000000..677b6178 --- /dev/null +++ b/file.go @@ -0,0 +1,339 @@ +// Mgmt +// Copyright (C) 2013-2015+ James Shubin and the project contributors +// Written by James Shubin and the project contributors +// +// 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 . + +package main + +import ( + "code.google.com/p/go-uuid/uuid" + "crypto/sha256" + "encoding/hex" + "fmt" + "gopkg.in/fsnotify.v1" + //"github.com/go-fsnotify/fsnotify" // git master of "gopkg.in/fsnotify.v1" + "io" + "log" + "math" + "os" + "path" + "strings" + "syscall" +) + +type FileType struct { + uuid string + Type string // always "file" + Name string // name variable + Events chan string // FIXME: eventually a struct for the event? + Path string // path variable (should default to name) + Content string + State string // state: exists/present?, absent, (undefined?) + sha256sum string +} + +func NewFileType(name, path, content, state string) *FileType { + // FIXME if path = nil, path = name ... + return &FileType{ + uuid: uuid.New(), + Type: "file", + Name: name, + Events: make(chan string, 1), // XXX: chan size? + Path: path, + Content: content, + State: state, + sha256sum: "", + } +} + +// File watcher for files and directories +// Modify with caution, probably important to write some test cases first! +func (obj FileType) Watch(v *Vertex) { + // obj.Path: file or directory + //var recursive bool = false + //var isdir = (obj.Path[len(obj.Path)-1:] == "/") // dirs have trailing slashes + //fmt.Printf("IsDirectory: %v\n", isdir) + + var safename = path.Clean(obj.Path) // no trailing slash + + watcher, err := fsnotify.NewWatcher() + if err != nil { + log.Fatal(err) + } + defer watcher.Close() + + patharray := PathSplit(safename) // tokenize the path + var index = len(patharray) // starting index + var current string // current "watcher" location + var delta_depth int // depth delta between watcher and event + var send = false // send event? + var extraCheck = false + + for { + current = strings.Join(patharray[0:index], "/") + if current == "" { // the empty string top is the root dir ("/") + current = "/" + } + log.Printf("Watching: %v\n", current) // attempting to watch... + + // initialize in the loop so that we can reset on rm-ed handles + err = watcher.Add(current) + if err != nil { + if err == syscall.ENOENT { + index-- // usually not found, move up one dir + } else if err == syscall.ENOSPC { + // XXX: i sometimes see: no space left on device + // XXX: why causes this to happen ? + log.Printf("Strange file[%v] error: %+v\n", obj.Name, err.Error) // 0x408da0 + log.Fatal(err) + } else { + log.Printf("Unknown file[%v] error:\n", obj.Name) + log.Fatal(err) + } + index = int(math.Max(1, float64(index))) + continue + } + + // XXX: check state after inotify started + // SMALL RACE: after we terminate watch, till when it's started + // something could have gotten created/changed/etc... right? + if extraCheck { + extraCheck = false + // XXX + //if exists ... { + // send signal + // continue + // change index? i don't think so. be thorough and check + //} + } + + select { + case event := <-watcher.Events: + // the deeper you go, the bigger the delta_depth is... + // this is the difference between what we're watching, + // and the event... doesn't mean we can't watch deeper + if current == event.Name { + delta_depth = 0 // i was watching what i was looking for + + } else if HasPathPrefix(event.Name, current) { + delta_depth = len(PathSplit(current)) - len(PathSplit(event.Name)) // -1 or less + + } else if HasPathPrefix(current, event.Name) { + delta_depth = len(PathSplit(event.Name)) - len(PathSplit(current)) // +1 or more + + } else { + // XXX multiple watchers receive each others events + // https://github.com/go-fsnotify/fsnotify/issues/95 + // this happened with two values such as: + // event.Name: /tmp/mgmt/f3 and current: /tmp/mgmt/f2 + // are the different watchers getting each others events?? + //log.Printf("The delta depth is NaN...\n") + //log.Printf("Value of event.Name is: %v\n", event.Name) + //log.Printf("........ current is: %v\n", current) + //log.Fatal("The delta depth is NaN!") + continue + } + //log.Printf("The delta depth is: %v\n", delta_depth) + + // if we have what we wanted, awesome, send an event... + if event.Name == safename { + //log.Println("Event!") + send = true + + // file removed, move the watch upwards + if delta_depth >= 0 && (event.Op&fsnotify.Remove == fsnotify.Remove) { + //log.Println("Removal!") + watcher.Remove(current) + index-- + } + + // we must be a parent watcher, so descend in + if delta_depth < 0 { + watcher.Remove(current) + index++ + } + + // if safename starts with event.Name, we're above, and no event should be sent + } else if HasPathPrefix(safename, event.Name) { + //log.Println("Above!") + + if delta_depth >= 0 && (event.Op&fsnotify.Remove == fsnotify.Remove) { + log.Println("Removal!") + watcher.Remove(current) + index-- + } + + if delta_depth < 0 { + watcher.Remove(current) + index++ + } + + // if event.Name startswith safename, send event, we're already deeper + } else if HasPathPrefix(event.Name, safename) { + //log.Println("Event2!") + send = true + } + + case err := <-watcher.Errors: + log.Println("error:", err) + log.Fatal(err) + v.Events <- fmt.Sprintf("file: %v", "error") + + case exit := <-obj.Events: + if exit == "exit" { + return + } else { + log.Fatal("Unknown event: %v\n", exit) + } + } + + // do all our event sending all together to avoid duplicate msgs + if send { + send = false + //log.Println("Sending event!") + //v.Events <- fmt.Sprintf("file(%v): %v", obj.Path, event.Op) + v.Events <- fmt.Sprintf("file(%v): %v", obj.Path, "event!") // FIXME: use struct + } + } +} + +func (obj FileType) Exit() bool { + obj.Events <- "exit" + return true +} + +func (obj FileType) HashSHA256fromContent() string { + if obj.sha256sum != "" { // return if already computed + return obj.sha256sum + } + + hash := sha256.New() + hash.Write([]byte(obj.Content)) + obj.sha256sum = hex.EncodeToString(hash.Sum(nil)) + return obj.sha256sum +} + +func (obj FileType) StateOK() bool { + if _, err := os.Stat(obj.Path); os.IsNotExist(err) { + // no such file or directory + if obj.State == "absent" { + return true // missing file should be missing, phew :) + } else { + // state invalid, skip expensive checksums + return false + } + } + + // TODO: add file mode check here... + + if PathIsDir(obj.Path) { + return obj.StateOKDir() + } else { + return obj.StateOKFile() + } +} + +func (obj FileType) StateOKFile() bool { + if PathIsDir(obj.Path) { + log.Fatal("This should only be called on a File type.") + } + + // run a diff, and return true if needs changing + + hash := sha256.New() + + f, err := os.Open(obj.Path) + if err != nil { + //log.Fatal(err) + return false + } + defer f.Close() + + if _, err := io.Copy(hash, f); err != nil { + //log.Fatal(err) + return false + } + + sha256sum := hex.EncodeToString(hash.Sum(nil)) + //fmt.Printf("sha256sum: %v\n", sha256sum) + + if obj.HashSHA256fromContent() == sha256sum { + return true + } + + return false +} + +func (obj FileType) StateOKDir() bool { + if !PathIsDir(obj.Path) { + log.Fatal("This should only be called on a Dir type.") + } + + // XXX: not implemented + log.Fatal("Not implemented!") + return false +} + +func (obj FileType) Apply() bool { + fmt.Printf("Apply->%v[%v]\n", obj.Type, obj.Name) + + if PathIsDir(obj.Path) { + return obj.ApplyDir() + } else { + return obj.ApplyFile() + } +} + +func (obj FileType) ApplyFile() bool { + + if PathIsDir(obj.Path) { + log.Fatal("This should only be called on a File type.") + } + + if obj.State == "absent" { + log.Printf("About to remove: %v\n", obj.Path) + err := os.Remove(obj.Path) + if err != nil { + return false + } + return true + } + + //fmt.Println("writing: " + filename) + f, err := os.Create(obj.Path) + if err != nil { + log.Println("error:", err) + return false + } + defer f.Close() + + _, err = io.WriteString(f, obj.Content) + if err != nil { + log.Println("error:", err) + return false + } + + return true +} + +func (obj FileType) ApplyDir() bool { + if !PathIsDir(obj.Path) { + log.Fatal("This should only be called on a Dir type.") + } + + // XXX: not implemented + log.Fatal("Not implemented!") + return true +} diff --git a/main.go b/main.go new file mode 100644 index 00000000..3b92fcfc --- /dev/null +++ b/main.go @@ -0,0 +1,147 @@ +// Mgmt +// Copyright (C) 2013-2015+ James Shubin and the project contributors +// Written by James Shubin and the project contributors +// +// 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 . + +package main + +import ( + "fmt" + "github.com/codegangsta/cli" + "log" + "os" + "os/signal" + "sync" + "syscall" + "time" +) + +// set at compile time +var ( + version string + program string +) + +const ( + DEBUG = false +) + +// signal handler +func waitForSignal(exit chan bool) { + signals := make(chan os.Signal, 1) + signal.Notify(signals, os.Interrupt) // catch ^C + //signal.Notify(signals, os.Kill) // catch signals + signal.Notify(signals, syscall.SIGTERM) + + select { + case e := <-signals: // any signal will do + if e == os.Interrupt { + fmt.Println() // put ^C char from terminal on its own line + log.Println("Interrupted by ^C") + } else { + log.Println("Interrupted by signal") + } + case <-exit: // or a manual signal + log.Println("Interrupted by exit signal") + } +} + +func run(c *cli.Context) { + var start int64 = time.Now().UnixNano() + var wg sync.WaitGroup + exit := make(chan bool) // exit signal + log.Printf("This is: %v, version: %v\n", program, version) + + // exit after `exittime` seconds for no reason at all... + if i := c.Int("exittime"); i > 0 { + go func() { + time.Sleep(time.Duration(i) * time.Second) + exit <- true + }() + } + + // build the graph from a config file + G := GraphFromConfig(c.String("file")) + log.Printf("Graph: %v\n", G) // show graph + + log.Printf("Start: %v\n", start) + + for x := range G.GetVerticesChan() { // XXX ? + log.Printf("Main->Starting[%v]\n", x.Name) + + wg.Add(1) + // must pass in value to avoid races... + // see: https://ttboj.wordpress.com/2015/07/27/golang-parallelism-issues-causing-too-many-open-files-error/ + go func(v *Vertex) { + defer wg.Done() + v.Start() + log.Printf("Main->Finish[%v]\n", v.Name) + }(x) + + // generate a startup "poke" so that an initial check happens + go func(v *Vertex) { + v.Events <- fmt.Sprintf("Startup(%v)", v.Name) + }(x) + } + + log.Println("Running...") + + waitForSignal(exit) // pass in exit channel to watch + + G.Exit() // tell all the children to exit + + if DEBUG { + for i := range G.GetVerticesChan() { + fmt.Printf("Vertex: %v\n", i) + } + fmt.Printf("Graph: %v\n", G) + } + + wg.Wait() // wait for primary go routines to exit + + // TODO: wait for each vertex to exit... + log.Println("Goodbye!") +} + +func main() { + app := cli.NewApp() + app.Name = program + app.Usage = "next generation config management" + app.Version = version + //app.Action = ... // without a default action, help runs + + app.Commands = []cli.Command{ + { + Name: "run", + Aliases: []string{"r"}, + Usage: "run", + Action: run, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "file, f", + Value: "", + Usage: "graph definition to run", + }, + cli.IntFlag{ + Name: "exittime", + Value: 0, + Usage: "exit after a maximum of approximately this many seconds", + }, + }, + }, + } + app.EnableBashCompletion = true + app.Run(os.Args) +} diff --git a/main_test.go b/main_test.go new file mode 100644 index 00000000..fa426c75 --- /dev/null +++ b/main_test.go @@ -0,0 +1,26 @@ +// Mgmt +// Copyright (C) 2013-2015+ James Shubin and the project contributors +// Written by James Shubin and the project contributors +// +// 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 . + +package main + +import ( +//"testing" +) + +//func TestT1(t *testing.T) { + +//} diff --git a/misc.go b/misc.go new file mode 100644 index 00000000..e7c79a1c --- /dev/null +++ b/misc.go @@ -0,0 +1,57 @@ +// Mgmt +// Copyright (C) 2013-2015+ James Shubin and the project contributors +// Written by James Shubin and the project contributors +// +// 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 . + +package main + +import ( + "path" + "strings" +) + +// Similar to the GNU dirname command +func Dirname(p string) string { + d, _ := path.Split(path.Clean(p)) + return d +} + +// Split a path into an array of tokens excluding any trailing empty tokens +func PathSplit(p string) []string { + return strings.Split(path.Clean(p), "/") +} + +// Does path string contain the given path prefix in it? +func HasPathPrefix(p, prefix string) bool { + + patharray := PathSplit(p) + prefixarray := PathSplit(prefix) + + if len(prefixarray) > len(patharray) { + return false + } + + for i := 0; i < len(prefixarray); i++ { + if prefixarray[i] != patharray[i] { + return false + } + } + + return true +} + +func PathIsDir(p string) bool { + return p[len(p)-1:] == "/" // a dir has a trailing slash in this context +} diff --git a/misc_test.go b/misc_test.go new file mode 100644 index 00000000..634f5c52 --- /dev/null +++ b/misc_test.go @@ -0,0 +1,101 @@ +// Mgmt +// Copyright (C) 2013-2015+ James Shubin and the project contributors +// Written by James Shubin and the project contributors +// +// 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 . + +package main + +import ( + "testing" +) + +func TestMiscT1(t *testing.T) { + + if Dirname("/foo/bar/baz") != "/foo/bar/" { + t.Errorf("Result is incorrect.") + } + + if Dirname("/foo/bar/baz/") != "/foo/bar/" { + t.Errorf("Result is incorrect.") + } + + if Dirname("/") != "/" { + t.Errorf("Result is incorrect.") + } +} + +func TestMiscT2(t *testing.T) { + + // TODO: compare the output with the actual list + p1 := "/foo/bar/baz" + r1 := []string{"", "foo", "bar", "baz"} + if len(PathSplit(p1)) != len(r1) { + //t.Errorf("Result should be: %q.", r1) + t.Errorf("Result should have a length of: %v.", len(r1)) + } + + p2 := "/foo/bar/baz/" + r2 := []string{"", "foo", "bar", "baz"} + if len(PathSplit(p2)) != len(r2) { + t.Errorf("Result should have a length of: %v.", len(r2)) + } +} + +func TestMiscT3(t *testing.T) { + + if HasPathPrefix("/foo/bar/baz", "/foo/ba") != false { + t.Errorf("Result should be false.") + } + + if HasPathPrefix("/foo/bar/baz", "/foo/bar") != true { + t.Errorf("Result should be true.") + } + + if HasPathPrefix("/foo/bar/baz", "/foo/bar/") != true { + t.Errorf("Result should be true.") + } + + if HasPathPrefix("/foo/bar/baz/", "/foo/bar") != true { + t.Errorf("Result should be true.") + } + + if HasPathPrefix("/foo/bar/baz/", "/foo/bar/") != true { + t.Errorf("Result should be true.") + } + + if HasPathPrefix("/foo/bar/baz/", "/foo/bar/baz/dude") != false { + t.Errorf("Result should be false.") + } +} + +func TestMiscT4(t *testing.T) { + + if PathIsDir("/foo/bar/baz/") != true { + t.Errorf("Result should be false.") + } + + if PathIsDir("/foo/bar/baz") != false { + t.Errorf("Result should be false.") + } + + if PathIsDir("/foo/") != true { + t.Errorf("Result should be true.") + } + + if PathIsDir("/") != true { + t.Errorf("Result should be true.") + } + +} diff --git a/pgraph.go b/pgraph.go new file mode 100644 index 00000000..91bc44c8 --- /dev/null +++ b/pgraph.go @@ -0,0 +1,414 @@ +// Mgmt +// Copyright (C) 2013-2015+ James Shubin and the project contributors +// Written by James Shubin and the project contributors +// +// 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 . + +// Pgraph (Pointer Graph) +package main + +import ( + "code.google.com/p/go-uuid/uuid" + //"container/list" // doubly linked list + "fmt" + "log" + "sync" + "time" +) + +// The graph abstract data type (ADT) is defined as follows: +// NOTE: the directed graph arrows point from left to right ( --> ) +// NOTE: the arrows point towards their dependencies (eg: arrows mean requires) +type Graph struct { + uuid string + Name string + Adjacency map[*Vertex]map[*Vertex]*Edge + //Directed bool + startcount int +} + +type Vertex struct { + uuid string + graph *Graph // store a pointer to the graph it's on + Name string + Type string + Timestamp int64 // last updated timestamp ? + Events chan string // FIXME: eventually a struct for the event? + Typedata Type + data map[string]string +} + +type Edge struct { + uuid string + Name string +} + +func NewGraph(name string) *Graph { + return &Graph{ + uuid: uuid.New(), + Name: name, + Adjacency: make(map[*Vertex]map[*Vertex]*Edge), + } +} + +func NewVertex(name, t string) *Vertex { + return &Vertex{ + uuid: uuid.New(), + Name: name, + Type: t, + Timestamp: -1, + Events: make(chan string, 1), // XXX: chan size? + data: make(map[string]string), + } +} + +func NewEdge(name string) *Edge { + return &Edge{ + uuid: uuid.New(), + Name: name, + } +} + +// Graph() creates a new, empty graph. +// addVertex(vert) adds an instance of Vertex to the graph. +func (g *Graph) AddVertex(v *Vertex) { + if _, exists := g.Adjacency[v]; !exists { + g.Adjacency[v] = make(map[*Vertex]*Edge) + + // store a pointer to the graph it's on for convenience and readability + v.graph = g + } +} + +// addEdge(fromVert, toVert) Adds a new, directed edge to the graph that connects two vertices. +func (g *Graph) AddEdge(v1, v2 *Vertex, e *Edge) { + // NOTE: this doesn't allow more than one edge between two vertexes... + // TODO: is this a problem? + g.AddVertex(v1) + g.AddVertex(v2) + g.Adjacency[v1][v2] = e +} + +// addEdge(fromVert, toVert, weight) Adds a new, weighted, directed edge to the graph that connects two vertices. +// getVertex(vertKey) finds the vertex in the graph named vertKey. +func (g *Graph) GetVertex(uuid string) chan *Vertex { + ch := make(chan *Vertex, 1) + go func(uuid string) { + for k := range g.Adjacency { + v := *k + if v.uuid == uuid { + ch <- k + break + } + } + close(ch) + }(uuid) + return ch +} + +func (g *Graph) NumVertices() int { + return len(g.Adjacency) +} + +func (g *Graph) NumEdges() int { + // XXX: not implemented + return -1 +} + +// get an array (slice) of all vertices in the graph +func (g *Graph) GetVertices() []*Vertex { + vertices := make([]*Vertex, 0) + for k := range g.Adjacency { + vertices = append(vertices, k) + } + return vertices +} + +// returns a channel of all vertices in the graph +func (g *Graph) GetVerticesChan() chan *Vertex { + ch := make(chan *Vertex) + // TODO: do you need to pass this through into the go routine? + go func(ch chan *Vertex) { + for k := range g.Adjacency { + ch <- k + } + close(ch) + }(ch) + return ch +} + +// make the graph pretty print +func (g *Graph) String() string { + return fmt.Sprintf("Vertices(%d), Edges(%d)", g.NumVertices(), g.NumEdges()) +} + +//func (s []*Vertex) contains(element *Vertex) bool { +// google/golang hackers apparently do not think contains should be a built-in! +func Contains(s []*Vertex, element *Vertex) bool { + for _, v := range s { + if element == v { + return true + } + } + return false +} + +// return an array (slice) of all vertices that connect to vertex v +func (g *Graph) GraphEdges(vertex *Vertex) []*Vertex { + // TODO: we might be able to implement this differently by reversing + // the Adjacency graph and then looping through it again... + s := make([]*Vertex, 0) // stack + for w, _ := range g.Adjacency[vertex] { // forward paths + //fmt.Printf("forward: %v -> %v\n", v.Name, w.Name) + s = append(s, w) + } + + for k, x := range g.Adjacency { // reverse paths + for w, _ := range x { + if w == vertex { + //fmt.Printf("reverse: %v -> %v\n", v.Name, k.Name) + s = append(s, k) + } + } + } + return s +} + +// return an array (slice) of all directed vertices to vertex v +func (g *Graph) DirectedGraphEdges(vertex *Vertex) []*Vertex { + // TODO: we might be able to implement this differently by reversing + // the Adjacency graph and then looping through it again... + s := make([]*Vertex, 0) // stack + for w, _ := range g.Adjacency[vertex] { // forward paths + //fmt.Printf("forward: %v -> %v\n", v.Name, w.Name) + s = append(s, w) + } + return s +} + +// get timestamp of a vertex +func (v *Vertex) GetTimestamp() int64 { + return v.Timestamp +} + +// update timestamp of a vertex +func (v *Vertex) UpdateTimestamp() int64 { + v.Timestamp = time.Now().UnixNano() // update + return v.Timestamp +} + +func (g *Graph) DFS(start *Vertex) []*Vertex { + d := make([]*Vertex, 0) // discovered + s := make([]*Vertex, 0) // stack + if _, exists := g.Adjacency[start]; !exists { + return nil // TODO: error + } + v := start + s = append(s, v) + for len(s) > 0 { + + v, s = s[len(s)-1], s[:len(s)-1] // s.pop() + + if !Contains(d, v) { // if not discovered + d = append(d, v) // label as discovered + + for _, w := range g.GraphEdges(v) { + s = append(s, w) + } + } + } + return d +} + +// build a new graph containing only vertices from the list... +func (g *Graph) FilterGraph(name string, vertices []*Vertex) *Graph { + newgraph := NewGraph(name) + + for k1, x := range g.Adjacency { + for k2, e := range x { + + //fmt.Printf("Filter: %v -> %v # %v\n", k1.Name, k2.Name, e.Name) + if Contains(vertices, k1) || Contains(vertices, k2) { + newgraph.AddEdge(k1, k2, e) + } + } + } + + return newgraph +} + +// return a channel containing the N disconnected graphs in our main graph +// we can then process each of these in parallel +func (g *Graph) GetDisconnectedGraphs() chan *Graph { + ch := make(chan *Graph) + go func() { + var start *Vertex + d := make([]*Vertex, 0) // discovered + c := g.NumVertices() + for len(d) < c { + + // get an undiscovered vertex to start from + for _, s := range g.GetVertices() { + if !Contains(d, s) { + start = s + } + } + + // dfs through the graph + dfs := g.DFS(start) + // filter all the collected elements into a new graph + newgraph := g.FilterGraph(g.Name, dfs) + + // add number of elements found to found variable + d = append(d, dfs...) // extend + + // return this new graph to the channel + ch <- newgraph + + // if we've found all the elements, then we're done + // otherwise loop through to continue... + } + + close(ch) + }() + return ch +} + +func (v *Vertex) Value(key string) (string, bool) { + if value, exists := v.data[key]; exists { + return value, true + } + return "", false +} + +func (v *Vertex) SetValue(key, value string) bool { + v.data[key] = value + return true +} + +func (g *Graph) GetVerticesKeyValue(key, value string) chan *Vertex { + ch := make(chan *Vertex) + go func() { + for vertex := range g.GetVerticesChan() { + if v, exists := vertex.Value(key); exists && v == value { + ch <- vertex + } + } + close(ch) + }() + return ch +} + +// return a pointer to the graph a vertex is on +func (v *Vertex) GetGraph() *Graph { + return v.graph +} + +func HeisenbergCount(ch chan *Vertex) int { + c := 0 + for x := range ch { + _ = x + c++ + } + return c +} + +func (v *Vertex) Associate(t Type) { + v.Typedata = t +} + +func (v *Vertex) OKTimestamp() bool { + g := v.GetGraph() + for _, n := range g.DirectedGraphEdges(v) { + if v.GetTimestamp() > n.GetTimestamp() { + return false + } + } + + return true +} + +// poke the XXX children? +func (v *Vertex) Poke() { + g := v.GetGraph() + + for _, n := range g.DirectedGraphEdges(v) { // XXX: do we want the reverse order? + // poke! + n.Events <- fmt.Sprintf("poke(%v)", v.Name) + } +} + +func (g *Graph) Exit() { + // tell all the vertices to exit... + for v := range g.GetVerticesChan() { + v.Exit() + } +} + +func (v *Vertex) Exit() { + v.Events <- "exit" +} + +// main loop for each vertex +// warning: this logic might be subtle and tricky. +// be careful as it might not even be correct now! +func (v *Vertex) Start() { + log.Printf("Main->Vertex[%v]->Start()\n", v.Name) + + //g := v.GetGraph() + var t = v.Typedata + + // this whole wg2 wait group is only necessary if we need to wait for + // the go routine to exit... + var wg2 sync.WaitGroup + + wg2.Add(1) + go func(v *Vertex, t Type) { + defer wg2.Done() + //fmt.Printf("About to watch [%v].\n", v.Name) + t.Watch(v) + }(v, t) + + var ok bool + //XXX make sure dependencies run and become more current first... + for { + select { + case event := <-v.Events: + + log.Printf("Event[%v]: %v\n", v.Name, event) + + if event == "exit" { + t.Exit() // type exit + wg2.Wait() // wait for worker to exit + return + } + + ok = true + if v.OKTimestamp() { + if !t.StateOK() { // TODO: can we rename this to something better? + // throw an error if apply fails... + // if this fails, don't UpdateTimestamp() + if !t.Apply() { // check for error + ok = false + } + } + + if ok { + v.UpdateTimestamp() // this was touched... + v.Poke() // XXX + } + } + } + } +} diff --git a/pgraph_test.go b/pgraph_test.go new file mode 100644 index 00000000..cf54e7f3 --- /dev/null +++ b/pgraph_test.go @@ -0,0 +1,201 @@ +// Mgmt +// Copyright (C) 2013-2015+ James Shubin and the project contributors +// Written by James Shubin and the project contributors +// +// 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 . + +// NOTE: this is pgraph, a pointer graph + +package main + +import ( + "testing" +) + +func TestPgraphT1(t *testing.T) { + + G := NewGraph("g1") + + if i := G.NumVertices(); i != 0 { + t.Errorf("Should have 0 vertices instead of: %d.", i) + } + + v1 := NewVertex("v1", "type") + v2 := NewVertex("v2", "type") + e1 := NewEdge("e1") + G.AddEdge(v1, v2, e1) + + if i := G.NumVertices(); i != 2 { + t.Errorf("Should have 2 vertices instead of: %d.", i) + } +} + +func TestPgraphT2(t *testing.T) { + + G := NewGraph("g2") + v1 := NewVertex("v1", "type") + v2 := NewVertex("v2", "type") + v3 := NewVertex("v3", "type") + v4 := NewVertex("v4", "type") + v5 := NewVertex("v5", "type") + v6 := NewVertex("v6", "type") + e1 := NewEdge("e1") + e2 := NewEdge("e2") + e3 := NewEdge("e3") + e4 := NewEdge("e4") + e5 := NewEdge("e5") + //e6 := NewEdge("e6") + G.AddEdge(v1, v2, e1) + G.AddEdge(v2, v3, e2) + G.AddEdge(v3, v1, e3) + + G.AddEdge(v4, v5, e4) + G.AddEdge(v5, v6, e5) + + if i := G.NumVertices(); i != 6 { + t.Errorf("Should have 6 vertices instead of: %d.", i) + } +} + +func TestPgraphT3(t *testing.T) { + + G := NewGraph("g3") + v1 := NewVertex("v1", "type") + v2 := NewVertex("v2", "type") + v3 := NewVertex("v3", "type") + v4 := NewVertex("v4", "type") + v5 := NewVertex("v5", "type") + v6 := NewVertex("v6", "type") + e1 := NewEdge("e1") + e2 := NewEdge("e2") + e3 := NewEdge("e3") + e4 := NewEdge("e4") + e5 := NewEdge("e5") + //e6 := NewEdge("e6") + G.AddEdge(v1, v2, e1) + G.AddEdge(v2, v3, e2) + G.AddEdge(v3, v1, e3) + + G.AddEdge(v4, v5, e4) + G.AddEdge(v5, v6, e5) + //G.AddEdge(v6, v4, e6) + out1 := G.DFS(v1) + if i := len(out1); i != 3 { + t.Errorf("Should have 3 vertices instead of: %d.", i) + t.Errorf("Found: %v", out1) + for _, v := range out1 { + t.Errorf("Value: %v", v.Name) + } + } + + out2 := G.DFS(v4) + if i := len(out2); i != 3 { + t.Errorf("Should have 3 vertices instead of: %d.", i) + t.Errorf("Found: %v", out1) + for _, v := range out1 { + t.Errorf("Value: %v", v.Name) + } + } +} + +func TestPgraphT4(t *testing.T) { + + G := NewGraph("g4") + v1 := NewVertex("v1", "type") + v2 := NewVertex("v2", "type") + v3 := NewVertex("v3", "type") + e1 := NewEdge("e1") + e2 := NewEdge("e2") + e3 := NewEdge("e3") + G.AddEdge(v1, v2, e1) + G.AddEdge(v2, v3, e2) + G.AddEdge(v3, v1, e3) + + out := G.DFS(v1) + if i := len(out); i != 3 { + t.Errorf("Should have 3 vertices instead of: %d.", i) + t.Errorf("Found: %v", out) + for _, v := range out { + t.Errorf("Value: %v", v.Name) + } + } +} + +func TestPgraphT5(t *testing.T) { + G := NewGraph("g5") + v1 := NewVertex("v1", "type") + v2 := NewVertex("v2", "type") + v3 := NewVertex("v3", "type") + v4 := NewVertex("v4", "type") + v5 := NewVertex("v5", "type") + v6 := NewVertex("v6", "type") + e1 := NewEdge("e1") + e2 := NewEdge("e2") + e3 := NewEdge("e3") + e4 := NewEdge("e4") + e5 := NewEdge("e5") + //e6 := NewEdge("e6") + G.AddEdge(v1, v2, e1) + G.AddEdge(v2, v3, e2) + G.AddEdge(v3, v1, e3) + + G.AddEdge(v4, v5, e4) + G.AddEdge(v5, v6, e5) + //G.AddEdge(v6, v4, e6) + + save := []*Vertex{v1, v2, v3} + out := G.FilterGraph("new g5", save) + if i := out.NumVertices(); i != 3 { + t.Errorf("Should have 3 vertices instead of: %d.", i) + } + +} + +func TestPgraphT6(t *testing.T) { + G := NewGraph("g6") + v1 := NewVertex("v1", "type") + v2 := NewVertex("v2", "type") + v3 := NewVertex("v3", "type") + v4 := NewVertex("v4", "type") + v5 := NewVertex("v5", "type") + v6 := NewVertex("v6", "type") + e1 := NewEdge("e1") + e2 := NewEdge("e2") + e3 := NewEdge("e3") + e4 := NewEdge("e4") + e5 := NewEdge("e5") + //e6 := NewEdge("e6") + G.AddEdge(v1, v2, e1) + G.AddEdge(v2, v3, e2) + G.AddEdge(v3, v1, e3) + + G.AddEdge(v4, v5, e4) + G.AddEdge(v5, v6, e5) + //G.AddEdge(v6, v4, e6) + + graphs := G.GetDisconnectedGraphs() + HeisenbergGraphCount := func(ch chan *Graph) int { + c := 0 + for x := range ch { + _ = x + c++ + } + return c + } + + if i := HeisenbergGraphCount(graphs); i != 2 { + t.Errorf("Should have 2 graphs instead of: %d.", i) + } + +} diff --git a/run.sh b/run.sh new file mode 100755 index 00000000..02b06456 --- /dev/null +++ b/run.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +# simple way to kick off runs of the project, since 'go run' sucks! +make build || exit 1 +sudo ./mgmt "$@" +e=$? +make clean +exit $e diff --git a/service.go b/service.go new file mode 100644 index 00000000..440f266d --- /dev/null +++ b/service.go @@ -0,0 +1,294 @@ +// Mgmt +// Copyright (C) 2013-2015+ James Shubin and the project contributors +// Written by James Shubin and the project contributors +// +// 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 . + +// NOTE: docs are found at: https://godoc.org/github.com/coreos/go-systemd/dbus + +package main + +import ( + "code.google.com/p/go-uuid/uuid" + "fmt" + systemd "github.com/coreos/go-systemd/dbus" // change namespace + "github.com/coreos/go-systemd/util" + "github.com/godbus/dbus" // namespace collides with systemd wrapper + "log" +) + +type ServiceType struct { + uuid string + Type string // always "service" + Name string // name variable + Events chan string // FIXME: eventually a struct for the event? + State string // state: running, stopped + Startup string // enabled, disabled, undefined +} + +func NewServiceType(name, state, startup string) *ServiceType { + return &ServiceType{ + uuid: uuid.New(), + Type: "service", + Name: name, + Events: make(chan string, 1), // XXX: chan size? + State: state, + Startup: startup, + } +} + +// Service watcher +func (obj ServiceType) Watch(v *Vertex) { + // obj.Name: service name + + if !util.IsRunningSystemd() { + log.Fatal("Systemd is not running.") + } + + conn, err := systemd.NewSystemdConnection() // needs root access + if err != nil { + log.Fatal("Failed to connect to systemd: ", err) + } + defer conn.Close() + + bus, err := dbus.SystemBus() + if err != nil { + log.Fatal("Failed to connect to bus: %v\n", err) + } + + // XXX: will this detect new units? + bus.BusObject().Call("org.freedesktop.DBus.AddMatch", 0, + "type='signal',interface='org.freedesktop.systemd1.Manager',member='Reloading'") + buschan := make(chan *dbus.Signal, 10) + bus.Signal(buschan) + + var service = fmt.Sprintf("%v.service", obj.Name) // systemd name + var send = false // send event? + var invalid = false // does the service exist or not? + var previous bool // previous invalid value + set := conn.NewSubscriptionSet() // no error should be returned + subChannel, subErrors := set.Subscribe() + var activeSet = false + + for { + // XXX: watch for an event for new units... + // XXX: detect if startup enabled/disabled value changes... + + previous = invalid + invalid = false + + // firstly, does service even exist or not? + loadstate, err := conn.GetUnitProperty(service, "LoadState") + if err != nil { + log.Printf("Failed to get property: %v\n", err) + invalid = true + } + + if !invalid { + var notFound = (loadstate.Value == dbus.MakeVariant("not-found")) + if notFound { // XXX: in the loop we'll handle changes better... + log.Printf("Failed to find service: %v\n", service) + invalid = true // XXX ? + } + } + + if previous != invalid { // if invalid changed, send signal + send = true + } + + if invalid { + log.Printf("Waiting for: %v\n", service) // waiting for service to appear... + if activeSet { + activeSet = false + set.Remove(service) // no return value should ever occur + } + + select { + case _ = <-buschan: // XXX wait for new units event to unstick + // loop so that we can see the changed invalid signal + log.Printf("Service[%v]->DaemonReload()\n", service) + + case exit := <-obj.Events: + if exit == "exit" { + return + } else { + log.Fatal("Unknown event: %v\n", exit) + } + } + } else { + if !activeSet { + activeSet = true + set.Add(service) // no return value should ever occur + } + + log.Printf("Watching: %v\n", service) // attempting to watch... + select { + case event := <-subChannel: + + log.Printf("Service event: %+v\n", event) + // NOTE: the value returned is a map for some reason... + if event[service] != nil { + // event[service].ActiveState is not nil + if event[service].ActiveState == "active" { + log.Printf("Service[%v]->Started()\n", service) + } else if event[service].ActiveState == "inactive" { + log.Printf("Service[%v]->Stopped!()\n", service) + } else { + log.Fatal("Unknown service state: ", event[service].ActiveState) + } + } else { + // service stopped (and ActiveState is nil...) + log.Printf("Service[%v]->Stopped\n", service) + } + send = true + + case err := <-subErrors: + log.Println("error:", err) + log.Fatal(err) + v.Events <- fmt.Sprintf("service: %v", "error") + + case exit := <-obj.Events: + if exit == "exit" { + return + } else { + log.Fatal("Unknown event: %v\n", exit) + } + } + } + + if send { + send = false + //log.Println("Sending event!") + v.Events <- fmt.Sprintf("service(%v): %v", obj.Name, "event!") // FIXME: use struct + } + } +} + +func (obj ServiceType) Exit() bool { + obj.Events <- "exit" + return true +} + +func (obj ServiceType) StateOK() bool { + + if !util.IsRunningSystemd() { + log.Fatal("Systemd is not running.") + } + + conn, err := systemd.NewSystemdConnection() // needs root access + if err != nil { + log.Fatal("Failed to connect to systemd: ", err) + } + defer conn.Close() + + var service = fmt.Sprintf("%v.service", obj.Name) // systemd name + + loadstate, err := conn.GetUnitProperty(service, "LoadState") + if err != nil { + log.Printf("Failed to get load state: %v\n", err) + return false + } + + // NOTE: we have to compare variants with other variants, they are really strings... + var notFound = (loadstate.Value == dbus.MakeVariant("not-found")) + if notFound { + log.Printf("Failed to find service: %v\n", service) + return false + } + + // XXX: check service "enabled at boot" or not status... + + //conn.GetUnitProperties(service) + activestate, err := conn.GetUnitProperty(service, "ActiveState") + if err != nil { + log.Fatal("Failed to get active state: ", err) + } + + var running = (activestate.Value == dbus.MakeVariant("active")) + + if obj.State == "running" { + if !running { + return false // we are in the wrong state + } + } else if obj.State == "stopped" { + if running { + return false + } + } else { + log.Fatal("Unknown state: ", obj.State) + } + + return true // all is good, no state change needed +} + +func (obj ServiceType) Apply() bool { + fmt.Printf("Apply->%v[%v]\n", obj.Type, obj.Name) + + if !util.IsRunningSystemd() { + log.Fatal("Systemd is not running.") + } + + conn, err := systemd.NewSystemdConnection() // needs root access + if err != nil { + log.Fatal("Failed to connect to systemd: ", err) + } + defer conn.Close() + + var service = fmt.Sprintf("%v.service", obj.Name) // systemd name + var files = []string{service} // the service represented in a list + if obj.Startup == "enabled" { + _, _, err = conn.EnableUnitFiles(files, false, true) + + } else if obj.Startup == "disabled" { + _, err = conn.DisableUnitFiles(files, false) + } else { + err = nil + } + if err != nil { + log.Printf("Unable to change startup status: %v\n", err) + return false + } + + result := make(chan string, 1) // catch result information + + if obj.State == "running" { + _, err := conn.StartUnit(service, "fail", result) + if err != nil { + log.Fatal("Failed to start unit: ", err) + return false + } + } else if obj.State == "stopped" { + _, err = conn.StopUnit(service, "fail", result) + if err != nil { + log.Fatal("Failed to stop unit: ", err) + return false + } + } else { + log.Fatal("Unknown state: ", obj.State) + } + + status := <-result + if &status == nil { + log.Fatal("Result is nil") + return false + } + if status != "done" { + log.Fatal("Unknown return string: ", status) + return false + } + + // XXX: also set enabled on boot + + return true +} diff --git a/test.sh b/test.sh new file mode 100755 index 00000000..4d90f302 --- /dev/null +++ b/test.sh @@ -0,0 +1,11 @@ +#!/bin/bash -e + +# test suite... +echo running test.sh + +# ensure there is no trailing whitespace or other whitespace errors +git diff-tree --check $(git hash-object -t tree /dev/null) HEAD + +# ensure entries to authors file are sorted +start=$(($(grep -n '^[[:space:]]*$' AUTHORS | awk -F ':' '{print $1}' | head -1) + 1)) +diff <(tail -n +$start AUTHORS | sort) <(tail -n +$start AUTHORS) diff --git a/test/test-gofmt.sh b/test/test-gofmt.sh new file mode 100755 index 00000000..92406777 --- /dev/null +++ b/test/test-gofmt.sh @@ -0,0 +1,35 @@ +#!/bin/bash +# original version of this script from kubernetes project, under ALv2 license + +set -o errexit +set -o nounset +set -o pipefail + +ROOT=$(dirname "${BASH_SOURCE}")/.. + +GO_VERSION=($(go version)) + +if [[ -z $(echo "${GO_VERSION[2]}" | grep -E 'go1.2|go1.3|go1.4') ]]; then + echo "Unknown go version '${GO_VERSION}', skipping gofmt." + exit 0 +fi + +cd "${ROOT}" + +find_files() { + find . -not \( \ + \( \ + -wholename './old' \ + -o -wholename './tmp' \ + \) -prune \ + \) -name '*.go' +} + +GOFMT="gofmt" # we prefer to not use the -s flag, which is pretty annoying... +bad_files=$(find_files | xargs $GOFMT -l) +if [[ -n "${bad_files}" ]]; then + echo 'FAIL' + echo 'The following files are not properly formatted:' + echo "${bad_files}" + exit 1 +fi diff --git a/test/test-yamlfmt.sh b/test/test-yamlfmt.sh new file mode 100755 index 00000000..68989d3d --- /dev/null +++ b/test/test-yamlfmt.sh @@ -0,0 +1,33 @@ +#!/bin/bash +# check for any yaml files that aren't properly formatted + +set -o errexit +set -o nounset +set -o pipefail + +ROOT=$(dirname "${BASH_SOURCE}")/.. +cd "${ROOT}" + +find_files() { + find . -not \( \ + \( \ + -wholename './old' \ + -o -wholename './tmp' \ + \) -prune \ + \) -name '*.yaml' +} + +bad_files=$( + for i in $(find_files); do + if ! diff -q <( ruby -e "require 'yaml'; puts YAML.load_file('$i').to_yaml" 2>/dev/null ) <( cat "$i" ) &>/dev/null; then + echo "$i" + fi + done +) + +if [[ -n "${bad_files}" ]]; then + echo 'FAIL' + echo 'The following files are not properly formatted:' + echo "${bad_files}" + exit 1 +fi diff --git a/types.go b/types.go new file mode 100644 index 00000000..f01eabed --- /dev/null +++ b/types.go @@ -0,0 +1,73 @@ +// Mgmt +// Copyright (C) 2013-2015+ James Shubin and the project contributors +// Written by James Shubin and the project contributors +// +// 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 . + +package main + +import ( + "code.google.com/p/go-uuid/uuid" + "fmt" + "log" +) + +type Type interface { + //Name() string + Watch(*Vertex) + StateOK() bool // TODO: can we rename this to something better? + Apply() bool + Exit() bool +} + +type NoopType struct { + uuid string + Type string // always "noop" + Name string // name variable + Events chan string // FIXME: eventually a struct for the event? +} + +func NewNoopType(name string) *NoopType { + return &NoopType{ + uuid: uuid.New(), + Type: "noop", + Name: name, + Events: make(chan string, 1), // XXX: chan size? + } +} + +func (obj NoopType) Watch(v *Vertex) { + select { + case exit := <-obj.Events: + if exit == "exit" { + return + } else { + log.Fatal("Unknown event: %v\n", exit) + } + } +} + +func (obj NoopType) Exit() bool { + obj.Events <- "exit" + return true +} + +func (obj NoopType) StateOK() bool { + return true // never needs updating +} + +func (obj NoopType) Apply() bool { + fmt.Printf("Apply->%v[%v]\n", obj.Type, obj.Name) + return true +}