From 3ca56d7b5c1cccbc6c21a21fe78f2c0928a3cc0b Mon Sep 17 00:00:00 2001 From: brent s Date: Mon, 30 Sep 2019 22:08:37 -0400 Subject: [PATCH] man. some major restructuring, and envsetup.py is a kinda neat hack. --- .gitignore | 27 +- LICENSE | 674 ++++++++++++++++++++++++++++ README | 3 + aif-config.py | 1117 ---------------------------------------------- aif.xsd | 690 ++++++++++++++-------------- aif/__init__.py | 3 + aif/config.py | 17 + aif/constants.py | 0 aif/disk.py | 15 + aif/envsetup.py | 50 +++ aif/log.py | 1 + aif/network.py | 1 + aif/pacman.py | 6 + aif/users.py | 9 + aifclient.py | 958 --------------------------------------- docs/TODO | 7 +- setup.py | 35 ++ 17 files changed, 1184 insertions(+), 2429 deletions(-) create mode 100644 LICENSE create mode 100644 README delete mode 100755 aif-config.py create mode 100644 aif/__init__.py create mode 100644 aif/config.py create mode 100644 aif/constants.py create mode 100644 aif/disk.py create mode 100644 aif/envsetup.py create mode 100644 aif/log.py create mode 100644 aif/network.py create mode 100644 aif/pacman.py create mode 100644 aif/users.py delete mode 100755 aifclient.py create mode 100644 setup.py diff --git a/.gitignore b/.gitignore index ad5e813..72d4830 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,20 @@ +*.7z *.bak - -# We don't need these in git. -screenlog* -*.swp -*.lck -*~ -.~lock.* +*.deb +*.jar +*.pkg.tar.xz +*.rar +*.run +*.sig +*.tar +*.tar.bz2 +*.tar.gz +*.tar.xz +*.tbz +*.tbz2 +*.tgz +*.txz +*.zip +.*.swp .editix - -# and we DEFINITELY don't need these. __pycache__/ -*.pyc diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e72bfdd --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 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 General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is 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. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + 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. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + 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 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. Use with the GNU Affero General Public License. + + 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 Affero 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 special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU 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 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 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 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 General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + 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 GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. \ No newline at end of file diff --git a/README b/README new file mode 100644 index 0000000..3fc6cf7 --- /dev/null +++ b/README @@ -0,0 +1,3 @@ +AIF-NG (Arch Installation Framework, Next Generation) is a means to install Arch Linux (https://www.archlinux.org/) in an unattended and automated fashion. Think of it as something akin to RedHat's Kickstart or Debian's Preseed for Arch Linux. + +See https://aif-ng.io/ for more information about this project. \ No newline at end of file diff --git a/aif-config.py b/aif-config.py deleted file mode 100755 index 0b91bd1..0000000 --- a/aif-config.py +++ /dev/null @@ -1,1117 +0,0 @@ -#!/usr/bin/env python3 - -xmldebug = False -stdlibxmldebug = False - -if not stdlibxmldebug: - try: - from lxml import etree - lxml_avail = True - except ImportError: - import xml.etree.ElementTree as etree # https://docs.python.org/3/library/xml.etree.elementtree.html - lxml_avail = False -else: - # debugging - import xml.etree.ElementTree as etree - lxml_avail = False - # end debugging -import argparse -import crypt -import datetime -import errno -import ipaddress -import json -import getpass -import os -import re -import readline -import sys -import urllib.request as urlrequest -import urllib.parse as urlparse -import urllib.response as urlresponse -from ftplib import FTP_TLS - -xsd = 'https://aif.square-r00t.net/aif.xsd' - -# Ugh. You kids and your colors and bolds and crap. -class color(object): - PURPLE = '\033[95m' - CYAN = '\033[96m' - DARKCYAN = '\033[36m' - BLUE = '\033[94m' - GREEN = '\033[92m' - YELLOW = '\033[93m' - RED = '\033[91m' - BOLD = '\033[1m' - UNDERLINE = '\033[4m' - END = '\033[0m' - -class aifgen(object): - def __init__(self, args): - self.args = args - - def webFetch(self, uri, auth = False): # TODO: add commandline args support for extra auth? - # Sanitize the user specification and find which protocol to use - prefix = uri.split(':')[0].lower() - if uri.startswith('/'): - uri = 'file://{0}'.format(uri) - prefix = 'file' - # Use the urllib module - if prefix in ('http', 'https', 'file', 'ftp'): - if auth: - if 'user' in auth.keys() and 'password' in auth.keys(): - # Set up Basic or Digest auth. - passman = urlrequest.HTTPPasswordMgrWithDefaultRealm() - if not 'realm' in auth.keys(): - passman.add_password(None, uri, auth['user'], auth['password']) - else: - passman.add_password(auth['realm'], uri, auth['user'], auth['password']) - if auth['type'] == 'digest': - httpauth = urlrequest.HTTPDigestAuthHandler(passman) - else: - httpauth = urlrequest.HTTPBasicAuthHandler(passman) - httpopener = urlrequest.build_opener(httpauth) - urlrequest.install_opener(httpopener) - with urlrequest.urlopen(uri) as f: - data = f.read() - elif prefix == 'ftps': - if auth: - if 'user' in auth.keys(): - username = auth['user'] - else: - username = 'anonymous' - if 'password' in auth.keys(): - password = auth['password'] - else: - password = 'anonymous' - filepath = '/'.join(uri.split('/')[3:]) - server = uri.split('/')[2] - content = StringIO() - ftps = FTP_TLS(server) - ftps.login(username, password) - ftps.prot_p() - ftps.retrlines("RETR " + filepath, content.write) - data = content.getvalue() - else: - exit('{0} is not a recognised URI type specifier. Must be one of http, https, file, ftp, or ftps.'.format(prefix)) - return(data) - - def getXSD(self): - xsdobj = etree.fromstring(self.webFetch(xsd)) - return(xsdobj) - - def getXML(self): - xmlobj = etree.fromstring(self.webFetch(self.args['cfgfile'])) - return(xmlobj) - - def getOpts(self): - # Before anything else... a disclaimer. - print('\nWARNING: This tool is not guaranteed to generate a working configuration file,\n' + - '\t but for most basic cases it should work. I strongly encourage you to generate your own\n' + - '\t configuration file instead by reading the documentation: https://aif.square-r00t.net/#writing_an_xml_configuration_file\n\n') - # This whole thing is ugly. Really, really ugly. Patches 100% welcome. - def chkPrompt(prompt, urls): - txtin = None - txtin = input(prompt) - if txtin == 'wikihelp': - print('\n Articles/pages that you may find helpful for this option are:') - for h in urls: - print(' * {0}'.format(h)) - print() - txtin = input(prompt) - else: - return(txtin) - def sizeChk(startsize): - try: - startn = int(re.sub('[%\-+KMGTP]', '', startsize)) - modifier = re.sub('^(\+|-)?.*$', '\g<1>', startsize) - if re.match('^(\+|-)?[0-9]+%$', startsize): - sizetype = 'percentage' - elif re.match('^(\+|-)?[0-9]+[KMGTP]$', n): - sizetype = 'fixed' - else: - exit(' !! ERROR: The input you provided does not match a valid pattern.') - if sizetype == 'percentage': - if not (0 <= startn <= 100): - exit(' !! ERROR: You must provide a percentage or a size.') - except: - exit(' !! ERROR: You did not provide a valid size specifier!') - return(startsize) - def ifacePrompt(nethelp): - ifaces = {} - moreIfaces = True - print('\tNOTE: You must specify the "persistent device naming" name of the device when configuring.\n' + - '\tYou can instead specify \'auto\' for automatic configuration of the first found interface\n' + - '\twith an active link. (You can only specify one auto device per system, and all other\n' - '\tinterface entries will be ignored by AIF-NG.)\n') - while moreIfaces: - ifacein = chkPrompt('* Interface device: ', nethelp) - addrin = chkPrompt(('** Address for {0} in CIDR format (can be an IPv4 or IPv6 address; ' + - 'use \'auto\' for DHCP/DHCPv6): ').format(ifacein), nethelp) - if addrin == 'auto': - addrtype = 'auto' - ipver = (chkPrompt('** Would you like \'ipv4\', \'ipv6\', or \'both\' to be auto-configured? ', nethelp)).lower() - if ipver not in ('ipv4', 'ipv6', 'both'): - exit(' !! ERROR: Must be one of ipv4, ipv6, or both.') - else: - addrtype = 'static' - try: - ipaddress.ip_network(addrin, strict = False) - try: - ipaddress.IPv4Address(addrin.split('/')[0]) - ipver = 'ipv4' - except ipaddress.AddressValueError: - ipver = 'ipv6' - except ValueError: - exit(' !! ERROR: You did not enter a valid IPv4/IPv6 address.') - if addrtype == 'static': - gwin = chkPrompt('*** What is the gateway address for {0}? '.format(addrin), nethelp) - try: - ipaddress.ip_address(gwin) - except: - exit(' !! ERROR: You did not enter a valid IPv4/IPv6 address.') - ifaces[ifacein] = {'address': addrin, 'proto': ipver, 'gw': gwin, 'resolvers': []} - resolversin = chkPrompt('*** What DNS resolvers should we use? Can accept a comma-separated list: ', nethelp) - for rslv in resolversin.split(','): - rslvaddr = rslv.strip() - ifaces[ifacein]['resolvers'].append(rslvaddr) - try: - ipaddress.ip_address(rslvaddr) - except: - exit(' !! ERROR: {0} is not a valid resolver address.'.format(rslvaddr)) - else: - ifaces[ifacein] = {'address': 'auto', 'proto': ipver, 'gw': False, 'resolvers': False} - moreIfacesin = input('* Would you like to add more interfaces? (y/{0}n{1}) '.format(color.BOLD, color.END)) - if not re.match('^y(es)?$', moreIfacesin.lower()): - moreIfaces = False - return(ifaces) - def genPassHash(user): - # https://bugs.python.org/issue30360 - keep this disabled until we're ready for primetime. - passin = getpass.getpass('* Please enter the password you want to use for {0} (will not echo back): '.format(user)) - #passin = input('* Please enter the password you want to use for {0}: '.format(user)) - if passin not in ('', '!'): - salt = crypt.mksalt(crypt.METHOD_SHA512) - salthash = crypt.crypt(passin, salt) - else: - salthash = passin - return(salthash) - def userPrompt(syshelp): - users = {} - moreusers = True - while moreusers: - user = chkPrompt('* What username would you like to add? ', syshelp) - if len(user) > 32: - exit(' !! ERROR: Usernames must be less than 32 characters.') - if not re.match('^[a-z_][a-z0-9_-]*[$]?$', user): - exit(' !! ERROR: Your username does not match a valid pattern. See the man page for useradd (\'CAVEATS\').') - users[user] = {} - sudoin = chkPrompt('** Should {0} have (full!) sudo access? (y/{1}n{2}) '.format(user, color.BOLD, color.END), syshelp) - if re.match('^y(es)?$', sudoin.lower()): - users[user]['sudo'] = True - else: - users[user]['sudo'] = False - users[user]['password'] = genPassHash(user) - users[user]['comment'] = chkPrompt(('** What comment should {0} have? ' + - '(Typically this is the user\'s full name) ').format(user), syshelp) - uidin = chkPrompt(('** What UID should {0} have? Leave this blank if you don\'t care ' + - '(should be fine for most cases): ').format(user), syshelp) - if uidin != '': - try: - users[user]['uid'] = int(uidin) - except: - exit(' !! ERROR: The UID must be an integer.') - else: - users[user]['uid'] = False - grpin = chkPrompt(('** What group name would you like to use for {0}\'s primary group? ' + - '(You\'ll be able to add additional groups in a moment.)\n' + - '\tThe default, if left blank, is to simply create a group named {0} ' + - '(which is what you probably want): ').format(user), syshelp) - if grpin != '': - if len(grpin) > 32: - exit(' !! ERROR: Group names must be less than 32 characters.') - if not re.match('^[a-z_][a-z0-9_-]*[$]?$', grpin): - exit(' !! ERROR: Your group name does not match a valid pattern. See the man page for groupadd (\'CAVEATS\').') - users[user]['group'] = grpin - else: - users[user]['group'] = False - if grpin != '': - gidin = chkPrompt(('** What GID should {0} have? Leave this blank if you don\'t care ' + - '(should be fine for most cases): ').format(grpin), syshelp) - if gidin != '': - try: - users[user]['gid'] = int(gidin) - except: - exit(' !! ERROR: The GID must be an integer.') - else: - users[user]['gid'] = False - else: - users[user]['gid'] = False - syshelp.append('https://aif.square-r00t.net/#code_home_code') - homein = chkPrompt(('** What directory should {0} use for its home? Leave blank if you don\'t care ' + - '(should be fine for most cases): ').format(user), syshelp) - if homein != '': - if not re.match('^/([^/\x00\s]+(/)?)+)$', homein): - exit('!! ERROR: Path {0} does not seem to be valid.'.format(homein)) - users[user]['home'] = homein - homecrt = chkPrompt('*** Do we need to create {0}? (y/{1}n{2}) '.format(homein, color.BOLD, color.END), syshelp) - if re.match('^y(es)?$', homecrt): - users[user]['homecreate'] = True - else: - users[user]['homecreate'] = False - else: - users[user]['home'] = False - del(syshelp[-1]) - xgrouphelp = 'https://aif.square-r00t.net/#code_xgroup_code' - if xgrouphelp not in syshelp: - syshelp.append(xgrouphelp) - xgroupin = chkPrompt('** Would you like to add extra groups for {0}? (y/{1}n{2}) '.format(user, color.BOLD, color.END), syshelp) - if re.match('^y(es)?$', xgroupin.lower()): - morexgroups = True - users[user]['xgroups'] = {} - else: - morexgroups = False - users[user]['xgroups'] = False - while morexgroups: - xgrp = chkPrompt('*** What is the name of the group you would like to add to {0}? '.format(user), syshelp) - if len(xgrp) > 32: - exit(' !! ERROR: Group names must be less than 32 characters.') - if not re.match('^[a-z_][a-z0-9_-]*[$]?$', xgrp): - exit(' !! ERROR: Your group name does not match a valid pattern. See the man page for groupadd (\'CAVEATS\').') - users[user]['xgroups'][xgrp] = {} - xgrpcrt = chkPrompt('*** Does the group \'{0}\' need to be created? (y/{1}n{2}) '.format(xgrp, color.BOLD, color.END), syshelp) - if re.match('^y(es)?$', xgrpcrt.lower()): - users[user]['xgroups'][xgrp]['create'] = True - xgrpgid = chkPrompt(('*** What GID should {0} be? If the group will already exist on the new system or ' + - 'don\'t care,\nleave this blank (should be fine for most cases): ').format(xgrp), syshelp) - if xgrpgid != '': - try: - users[user]['xgroups'][xgrp]['gid'] = int(xgrpgid) - except: - exit(' !! ERROR: The GID must be an integer.') - else: - users[user]['xgroups'][xgrp]['gid'] = False - else: - users[user]['xgroups'][xgrp]['create'] = False - users[user]['xgroups'][xgrp]['gid'] = False - morexgrpsin = input('** Would you like to add additional extra groups for {0}? (y/{1}n{2}) '.format(user, - color.BOLD, - color.END)) - if not re.match('^y(es)?$', morexgrpsin.lower()): - morexgroups = False - moreusersin = chkPrompt('* Would you like to add additional users? (y/{0}n{1}) '.format(color.BOLD, color.END), syshelp) - if not re.match('^y(es)?$', moreusersin.lower()): - moreusers = False - return(users) - def svcsPrompt(svchelp): - svcs = {} - moresvcs = True - while moresvcs: - svc = chkPrompt('** What is the name of the service? If it\'s a .service unit, you can leave the .service off: ', svchelp) - if not re.match('^[A-Za-z0-9\-@]+(\.(service|timer|target|socket|mount|slice))?$', svc): - exit(' !! ERROR: You seem to have specified an invalid service name.') - svcstatusin = chkPrompt('** Should {0} be enabled? ({1}y{2}/n) '.format(svc, color.BOLD, color.END), svchelp) - if re.match('^no?$', svcstatusin.lower()): - svcs[svc] = False - else: - svcs[svc] = True - moreservices = input('* Would you like to manage another service? (y/{0}n{1}) '.format(color.BOLD, color.END)) - if not re.match('^y(es)?$', moreservices.lower()): - moresvcs = False - return(svcs) - def repoPrompt(repohelp): - # The default pacman.conf's repo setup - repos = {'core': {'mirror': 'file:///etc/pacman.d/mirrorlist', - 'siglevel': 'default', - 'enabled': True}, - 'extra': {'mirror': 'file:///etc/pacman.d/mirrorlist', - 'siglevel': 'default', - 'enabled': True}, - 'community-testing': {'mirror': 'file:///etc/pacman.d/mirrorlist', - 'siglevel': 'default', - 'enabled': False}, - 'community': {'mirror': 'file:///etc/pacman.d/mirrorlist', - 'siglevel': 'default', - 'enabled': True}, - 'multilib-testing': {'mirror': 'file:///etc/pacman.d/mirrorlist', - 'siglevel': 'default', - 'enabled': False}, - 'multilib': {'mirror': 'file:///etc/pacman.d/mirrorlist', - 'siglevel': 'default', - 'enabled': False}} - chkdefs = chkPrompt(('* Would you like to review the default repository configuration ' + - '(and possibly edit it)? ({0}y{1}/n) ').format(color.BOLD, color.END), repohelp) - fmtstr = '\t{0} {1:<20} {2:^10} {3:^10} {4}' # ('#', 'REPO', 'ENABLED', 'SIGLEVEL', 'URI') - if not re.match('^no?$', chkdefs.lower()): - print('{0}{1}{2}'.format(color.BOLD, fmtstr.format('#', 'REPO', 'ENABLED', 'SIGLEVEL', 'URI'), color.END)) - rcnt = 1 - for r in repos.keys(): - print(fmtstr.format(rcnt, r, str(repos[r]['enabled']), repos[r]['siglevel'], repos[r]['mirror'])) - rcnt += 1 - editdefs = chkPrompt('** Would you like to edit any of this? (y/{0}n{1}) '.format(color.BOLD, color.END), repohelp) - if re.match('^y(es)?$', editdefs.lower()): - repokeys = list(repos.keys()) - moreedits = True - while moreedits: - rnum = input('** What repository # would you like to edit? ') - try: - rnum = int(rnum) - rname = repokeys[rnum - 1] - except: - exit(' !! ERROR: You did not specify a valid repository #.') - enableedit = chkPrompt('*** Should {0} be enabled? (y/n/{1}nochange{2}) '.format(rname, color.BOLD, color.END), repohelp) - if re.match('^y(es)?$', enableedit.lower()): - repos[rname]['enabled'] = True - elif re.match('^no?$', enableedit.lower()): - repos[rname]['enabled'] = False - siglvledit = chkPrompt('*** What siglevel should {0} use? Leave blank for no change: '.format(rname), repohelp) - if siglvledit != '': - grp1 = re.compile('^((Package|Database)?(Never|Optional|Required)|default)$') - grp2 = re.compile('^(Package|Database)?Trust(edOnly|All)$') - siglst = siglvledit.split() - if len(siglist) > 2: - exit(' !! ERROR: That is not a valid SigLevel string. See the manpage for pacman.conf ' + - '(\'PACKAGE AND DATABASE SIGNATURE CHECKING\').') - if not grp1.match(siglist[0]): - exit((' !! ERROR: {0} is not valid. See the manpage for pacman.conf ' + - '(\'PACKAGE AND DATABASE SIGNATURE CHECKING\').').format(siglist[0])) - if len(siglist) == 1: - if not grp2.match(siglist[1]): - exit((' !! ERROR: {0} is not valid. See the manpage for pacman.conf ' + - '(\'PACKAGE AND DATABASE SIGNATURE CHECKING\').').format(siglist[1])) - repos[rname]['siglevel'] = siglvledit - uriedit = chkPrompt('*** What should the URI be?\n' + - '\tUse \'file:///absolute/path/to/file\' to use an Include directive. Leave blank for no change: ', repohelp) - if uriedit != '': - repos[rname]['mirror'] = uriedit - moreeditsin = chkPrompt(('** Would you like to edit another ' + - 'repository? (y/{0}n{1}) ').format(color.BOLD, color.END), repohelp) - if not re.match('^y(es)?$', moreeditsin.lower()): - moreedits = False - addreposin = chkPrompt('* Would you like to add any additional repositories? (y/{0}n{1}) '.format(color.BOLD, color.END), repohelp) - if re.match('^y(es)?$', addreposin.lower()): - addrepos = True - while addrepos: - reponamein = chkPrompt('** What should this repository be named? (Must match the repository name on the mirror): ', repohelp) - reponame = re.sub('(^\[|]$)', '', reponamein) - if not re.match('^[a-z0-9]', reponame.lower()): - exit(' !! ERROR: That is not a valid repository name.') - repos[reponame] = {} - enablein = chkPrompt('** Should {0}{1}{2} be enabled? ({0}y{2}/n) '.format(color.BOLD, reponame, color.END), repohelp) - if not re.match('^no?$', enablein.lower()): - repos[reponame]['enabled'] = True - else: - repos[reponame]['enabled'] = False - siglvlin = chkPrompt(('** What SigLevel string should we use for {0}{1}{2}? ' + - 'Leave blank for default: ').format(color.BOLD, reponame, color.END), repohelp) - if siglvlin != '': - grp1 = re.compile('^((Package|Database)?(Never|Optional|Required)|default)$') - grp2 = re.compile('^(Package|Database)?Trust(edOnly|All)$') - siglst = siglvlin.split() - if len(siglist) > 2: - exit(' !! ERROR: That is not a valid SigLevel string. See the manpage for pacman.conf ' + - '(\'PACKAGE AND DATABASE SIGNATURE CHECKING\').') - if not grp1.match(siglist[0]): - exit((' !! ERROR: {0} is not valid. See the manpage for pacman.conf ' + - '(\'PACKAGE AND DATABASE SIGNATURE CHECKING\').').format(siglist[0])) - if len(siglist) == 1: - if not grp2.match(siglist[1]): - exit((' !! ERROR: {0} is not valid. See the manpage for pacman.conf ' + - '(\'PACKAGE AND DATABASE SIGNATURE CHECKING\').').format(siglist[1])) - repos[reponame]['siglevel'] = siglvlin - else: - repos[reponame]['siglevel'] = 'default' - uriin = chkPrompt(('** What URI should be used for {0}{1}{2}?\n' + - '\tUse \'file:///absolute/path/to/file\' to use an Include directive: ').format(color.BOLD, - reponame, - color.END), repohelp) - if uriin == '': - exit(' !! ERROR: You cannot specify a blank repository URI.') - else: - repos[reponame]['mirror'] = uriin - morereposin = chkPrompt('* Would you like to add another repository? (y/{0}n{1}) '.format(color.BOLD, color.END), repohelp) - if not re.match('^y(es)?$', morereposin.lower()): - addrepos = False - return(repos) - def mirrorPrompt(mirrorhelp): - moremirrors = False - mirrors = False - mirrorchk = chkPrompt('* Would you like to replace the default mirrorlist? (y/{0}n{1}) '.format(color.BOLD, color.END), mirrorhelp) - if re.match('^y(es)?$', mirrorchk.lower()): - moremirrors = True - while moremirrors: - if not isinstance(mirrors, list): - mirrors = [] - mirrorin = chkPrompt('** What is the URI for the mirror you would like to add?\n' + - '\tCan be one of the following types of URIs:\n' + - '\thttp://, https://, or file:// (for directories on the newly-installed system): ', mirrorhelp) - if mirrorin == '': - exit(' !! ERROR: You cannot specify a blank mirror URI.') - else: - mirrors.append(mirrorin) - moremirrorschk = chkPrompt('* Would you like to add another mirror? (y/{0}n{1}) '.format(color.BOLD, color.END), mirrorhelp) - if not re.match('^y(es)?$', moremirrorschk.lower()): - moremirrors = False - return(mirrors) - def pkgsPrompt(repohelp): - pkgs = {} - morepkgs = True - while morepkgs: - pkgname = chkPrompt('** What is the name of the package? ', repohelp) - if pkgname == '': - exit(' !! ERROR: You must specify a package name.') - reponame = chkPrompt(('** What repository should we install {0} from? ' + - '({1}optional{2}, leave blank to skip) ').format(pkgname, color.BOLD, color.END), repohelp) - if reponame == '': - pkgs[pkgname] = None - else: - pkgs[pkgname] = reponame - morepkgsin = chkPrompt('** Would you like to add another package? (y/{0}n{1}) '.format(color.BOLD, color.END), repohelp) - if not re.match('^y(es)?$', morepkgsin.lower()): - morepkgs = False - return(pkgs) - def scrptPrompt(scrpthlp): - scrpts = {'pre': False, 'pkg': False, 'post': False} - morescrpts = True - while morescrpts: - hookin = chkPrompt('** What type of script is this? (pre/pkg/post) ', scrpthlp) - if not re.match('^p(re|kg|ost)$', hookin.lower()): - exit(' !! ERROR: The hook must be one of pre, pkg, or post.') - else: - hook = hookin.lower() - if not scrpts[hook]: - scrpts[hook] = {} - scrptin = chkPrompt('** What is the URI for this script? Can be an http://, https://, ftp://, ftps://, or file:// URI: ', scrpthlp) - if not re.match('^(https?|ftps?|file)://', scrptin.lower()): - exit(' !! ERROR: That is not a valid URI.') - orderin = chkPrompt(('** What order should this script be executed in during the {0} hook?\n' + - '\tMust be a unique integer ' + - '(lower numbers execute before higher numbers): ').format(hook), scrpthlp) - try: - orderint = int(orderin) - except: - exit(' !! ERROR: Must be an integer') - if order in scrpts[hook].keys(): - exit(' !! ERROR: You already have a {0} script at that order number.'.format(hook)) - scrpts[hook][orderint] = {'uri': scrptin} - if re.match('^(https?|ftps?)://', scrptin.lower()): - authin = chkPrompt('** Does this script URI require auth? (y/{0}n{1}) '.format(color.BOLD, color.END), scrpthlp) - if re.match('^y(es)?$', authin.lower()): - if re.match('^https?://', scrptin.lower()): - authtype = chkPrompt(('*** What type of auth does this URI require? ' + - '({0}basic{1}/digest) ').format(color.BOLD, color.END), scrpthlp) - if authtype == '': - scrpts[hook][orderint]['auth'] = 'basic' - elif re.match('^(basic|digest)$', authtype.lower()): - scrpts[hook][orderint]['auth'] = authtype.lower() - else: - exit(' !! ERROR: That is not a valid auth type.') - if authtype.lower() == 'digest': - realmin = chkPrompt('*** Do you know the realm needed for authentication?\n' + - '\tIf not, just leave this blank and AIF-NG will try to guess: ', scrpthlp) - if realmin != '': - scrpts[hook][orderint]['realm'] = realmin - scrpts[hook][orderint]['user'] = chkPrompt('*** What user should we use for auth? ', scrpthlp) - scrpts[hook][orderint]['password'] = chkPrompt('*** What password should we use for auth? ', scrpthlp) - else: - scrpts[hook][orderint][auth] = False - morescrptsin = chkPrompt('* Would you like to add another hook script? (y/{0}n{1}) '.format(color.BOLD, color.END), scrpthlp) - if not re.match('^y(es)?$', morescrptsin.lower()): - morescrpts = False - return(scrpts) - conf = {} - print('[{0}] Beginning configuration...'.format(datetime.datetime.now())) - print('\n\tYou may reply with \'wikihelp\' on the first prompt of a question for the relevant link(s) in the Arch wiki ' + - '(and other resources).') - # https://aif.square-r00t.net/#code_disk_code - diskhelp = ['https://wiki.archlinux.org/index.php/installation_guide#Partition_the_disks'] - print('{0}= DISKS ={1}'.format(color.BOLD, color.END)) - diskin = chkPrompt('* What disk(s) would you like to be configured on the target system?\n' + - '\tIf you have multiple disks, separate with a comma (e.g. \'/dev/sda,/dev/sdb\'): ', diskhelp) - # NOTE: the following is a dict of fstype codes to their description. - fstypes = {'0700': 'Microsoft basic data', '0c01': 'Microsoft reserved', '2700': 'Windows RE', '3000': 'ONIE config', '3900': 'Plan 9', '4100': 'PowerPC PReP boot', '4200': 'Windows LDM data', '4201': 'Windows LDM metadata', '4202': 'Windows Storage Spaces', '7501': 'IBM GPFS', '7f00': 'ChromeOS kernel', '7f01': 'ChromeOS root', '7f02': 'ChromeOS reserved', '8200': 'Linux swap', '8300': 'Linux filesystem', '8301': 'Linux reserved', '8302': 'Linux /home', '8303': 'Linux x86 root (/)', '8304': 'Linux x86-64 root (/', '8305': 'Linux ARM64 root (/)', '8306': 'Linux /srv', '8307': 'Linux ARM32 root (/)', '8400': 'Intel Rapid Start', '8e00': 'Linux LVM', 'a500': 'FreeBSD disklabel', 'a501': 'FreeBSD boot', 'a502': 'FreeBSD swap', 'a503': 'FreeBSD UFS', 'a504': 'FreeBSD ZFS', 'a505': 'FreeBSD Vinum/RAID', 'a580': 'Midnight BSD data', 'a581': 'Midnight BSD boot', 'a582': 'Midnight BSD swap', 'a583': 'Midnight BSD UFS', 'a584': 'Midnight BSD ZFS', 'a585': 'Midnight BSD Vinum', 'a600': 'OpenBSD disklabel', 'a800': 'Apple UFS', 'a901': 'NetBSD swap', 'a902': 'NetBSD FFS', 'a903': 'NetBSD LFS', 'a904': 'NetBSD concatenated', 'a905': 'NetBSD encrypted', 'a906': 'NetBSD RAID', 'ab00': 'Recovery HD', 'af00': 'Apple HFS/HFS+', 'af01': 'Apple RAID', 'af02': 'Apple RAID offline', 'af03': 'Apple label', 'af04': 'AppleTV recovery', 'af05': 'Apple Core Storage', 'bc00': 'Acronis Secure Zone', 'be00': 'Solaris boot', 'bf00': 'Solaris root', 'bf01': 'Solaris /usr & Mac ZFS', 'bf02': 'Solaris swap', 'bf03': 'Solaris backup', 'bf04': 'Solaris /var', 'bf05': 'Solaris /home', 'bf06': 'Solaris alternate sector', 'bf07': 'Solaris Reserved 1', 'bf08': 'Solaris Reserved 2', 'bf09': 'Solaris Reserved 3', 'bf0a': 'Solaris Reserved 4', 'bf0b': 'Solaris Reserved 5', 'c001': 'HP-UX data', 'c002': 'HP-UX service', 'ea00': 'Freedesktop $BOOT', 'eb00': 'Haiku BFS', 'ed00': 'Sony system partition', 'ed01': 'Lenovo system partition', 'ef00': 'EFI System', 'ef01': 'MBR partition scheme', 'ef02': 'BIOS boot partition', 'f800': 'Ceph OSD', 'f801': 'Ceph dm-crypt OSD', 'f802': 'Ceph journal', 'f803': 'Ceph dm-crypt journal', 'f804': 'Ceph disk in creation', 'f805': 'Ceph dm-crypt disk in creation', 'fb00': 'VMWare VMFS', 'fb01': 'VMWare reserved', 'fc00': 'VMWare kcore crash protection', 'fd00': 'Linux RAID'} - conf['disks'] = {} - for d in diskin.split(','): - disk = d.strip() - if not re.match('^/dev/[A-Za-z0]+', disk): - exit('!! ERROR: Disk {0} does not seem to be a valid device path.'.format(disk)) - conf['disks'][disk] = {} - print('\n{0}== DISK: {1} =={2}'.format(color.BOLD, disk, color.END)) - fmtin = chkPrompt('* What format should this disk use (gpt/bios)? ', diskhelp) - fmt = fmtin.lower() - if fmt not in ('gpt', 'bios'): - exit(' !! ERROR: Must be one of \'gpt\' or \'bios\'.') - conf['disks'][disk]['fmt'] = fmt - conf['disks'][disk]['parts'] = {} - if fmt == 'gpt': - maxpart = '256' - else: - maxpart = '4' # yeah, extended volumes can do more, but that's not supported in AIF-NG. yet? - partnumsin = chkPrompt('* How many partitions should this disk have? (Maximum: {0}) '.format(maxpart), diskhelp) - try: - int(partnumsin) - except: - exit(' !! ERROR: Must be an integer.') - if int(partnumsin) < 1: - exit(' !! ERROR: Must be a positive integer.') - if int(partnumsin) > int(maxpart): - exit(' !! ERROR: Must be less than {0}'.format(maxpart)) - parthelp = diskhelp + ['https://wiki.archlinux.org/index.php/installation_guide#Format_the_partitions', - 'https://aif.square-r00t.net/#code_part_code'] - for partn in range(1, int(partnumsin) + 1): - # https://aif.square-r00t.net/#code_part_code - conf['disks'][disk]['parts'][partn] = {} - print('{0}=== PARTITION: {1}{2}==={3}'.format(color.BOLD, disk, partn, color.END)) - for s in ('start', 'stop'): - conf['disks'][disk]['parts'][partn][s] = None - sizein = chkPrompt(('* Where should partition {0} {1}? Can be percentage [n%] ' + - 'or size [(+/-)n(K/M/G/T/P)]: ').format(partn, s), parthelp) - conf['disks'][disk]['parts'][partn][s] = sizeChk(sizein) - newhelp = 'https://aif.square-r00t.net/#fstypes' - if newhelp not in parthelp: - parthelp.append(newhelp) - fstypein = chkPrompt(('* What filesystem type should partition {0} be? ' + - 'See wikihelp for valid fstypes: ').format(partn), parthelp) - if fstypein not in fstypes.keys(): - exit(' !! ERROR: {0} is not a valid filesystem type.'.format(fstypein)) - else: - print('\t(Selected {0})'.format(fstypes[fstypein])) - conf['disks'][disk]['parts'][partn]['fstype'] = fstypein - mnthelp = ['https://wiki.archlinux.org/index.php/installation_guide#Mount_the_file_systems', - 'https://aif.square-r00t.net/#code_mount_code'] - print('\n{0}= MOUNTS ={1}'.format(color.BOLD, color.END)) - mntin = chkPrompt('* What mountpoint(s) would you like to be configured on the target system?\n' + - '\tIf you have multiple mountpoints, separate with a comma (e.g. \'/mnt/aif,/mnt/aif/boot\').\n' + - '\t(NOTE: Can be \'swap\' for swapspace.): ', mnthelp) - conf['mounts'] = {} - for m in mntin.split(','): - mount = m.strip() - if not re.match('^(/([^/\x00\s]+(/)?)+|swap)$', mount): - exit('!! ERROR: Mountpoint {0} does not seem to be a valid path/specifier.'.format(mount)) - print('\n{0}== MOUNT: {1} =={2}'.format(color.BOLD, mount, color.END)) - dvcin = chkPrompt('* What device/partition should be mounted here? ', mnthelp) - if not re.match('^/dev/[A-Za-z0]+', dvcin): - exit(' !! ERROR: Must be a full path to a device/partition.') - ordrin = chkPrompt('* What order should this mount occur in relation to others?\n\t'+ - 'Must be a unique integer (lower numbers mount before higher numbers): ', mnthelp) - try: - order = int(ordrin) - except: - exit(' !! ERROR: Must be an integer') - if order in conf['mounts'].keys(): - exit(' !! ERROR: You already have a mountpoint at that order number.') - conf['mounts'][order] = {} - conf['mounts'][order]['target'] = mount - conf['mounts'][order]['device'] = dvcin - if mount != 'swap': - fstypein = chkPrompt('* What filesystem type should this be mounted as (i.e. mount\'s -t option)? This is optional,\n\t' + - 'but may be required for more exotic filesystem types. If you don\'t have to specify one,\n\t' + - 'just leave this blank: ', mnthelp) - if fstypein == '': - conf['mounts'][order]['fstype'] = False - elif not re.match('^[a-z]+([0-9]+)?$', fstypein): # Not 100%, but should catch most faulty entries - exit(' !! ERROR: {0} does not seem to be a valid filesystem type.'.format(fstypein)) - else: - conf['mounts'][order]['fstype'] = fstypein - mntoptsin = chkPrompt('** What, if any, mount option(s) (mount\'s -o option) do you require? (Multiple options should be separated\n' + - '\twith a comma). If none, leave this blank: ', mnthelp) - if mntoptsin == '': - conf['mounts'][order]['opts'] = False - elif not re.match('^[A-Za-z0-9_\.\-=]+(,[A-Za-z0-9_\.\-=]+)*', re.sub('\s', '', mntoptsin)): # TODO: shlex split this instead? - exit(' !! ERROR: You seem to have not specified valid mount options.') - else: - # TODO: slex this instead? is it possible for mount opts to contain whitespace? - conf['mounts'][order]['opts'] = re.sub('\s', '', mntoptsin) - else: - conf['mounts'][order]['fstype'] = False - conf['mounts'][order]['opts'] = False - print(('\n{0}= NETWORK ={1}\n' + - '\tNOTE: At this time, wireless/more exotic networking is not supported by AIF-NG.').format(color.BOLD, color.END)) - conf['network'] = {} - nethelp = ['https://wiki.archlinux.org/index.php/installation_guide#Network_configuration', - 'https://aif.square-r00t.net/#code_network_code'] - hostnamein = chkPrompt('* What should the newly-installed system\'s hostname be?\n' + - '\tIt must be in FQDN format, but can be a non-existent domain: ', nethelp) - hostname = hostnamein.lower() - if len(hostname) > 253: - exit(' !! ERROR: A FQDN cannot be more than 253 characters (RFC 1035, 2.3.4)') - hostnamelst = hostname.split('.') - for c in hostnamelst: - if len(c) > 63: - exit(' !! ERROR: No component of an FQDN can be more than 63 characters (RFC 1035, 2.3.4)') - if not re.match('^[a-zA-Z\d-]{,63}(\.[a-zA-Z\d-]{,63})*', hostname): - exit(' !! ERROR: That does not seem to be a valid FQDN.') - else: - conf['network']['hostname'] = hostname - conf['network']['ifaces'] = {} - nethelp.append('https://aif.square-r00t.net/#code_iface_code') - conf['network']['ifaces'] = ifacePrompt(nethelp) - print('\n{0}= SYSTEM ={1}'.format(color.BOLD, color.END)) - syshelp = ['https://aif.square-r00t.net/#code_system_code'] - syshelp.append('https://wiki.archlinux.org/index.php/installation_guide#Time_zone') - tzin = chkPrompt('* What timezone should the newly installed system use? (Default is UTC): ', syshelp) - if tzin == '': - tzin = 'UTC' - syshelp[1] = 'https://wiki.archlinux.org/index.php/installation_guide#Locale' - localein = chkPrompt('* What locale should the new system use? (Default is en_US.UTF-8): ', syshelp) - if localein == '': - localein = 'en_US.UTF-8' - syshelp[1] = 'https://aif.square-r00t.net/#code_mount_code' - chrootpathin = chkPrompt('* What chroot path should the host use? This should be one of the mounts you specified above: ', syshelp) - if not re.match('^/([^/\x00\s]+(/)?)+$', chrootpathin): - exit('!! ERROR: Your chroot path does not seem to be a valid path/specifier.') - syshelp[1] = 'https://wiki.archlinux.org/index.php/installation_guide#Set_the_keyboard_layout' - kbdin = chkPrompt('* What keyboard layout should the newly installed system use? (Default is US): ', syshelp) - if kbdin == '': - kbdin = 'US' - del(syshelp[1]) - rbtin = chkPrompt('* Would you like to reboot the host system after installation completes? ({0}y{1}/n): '.format(color.BOLD, color.END), syshelp) - if not re.match('^no?$', rbtin.lower()): - rebootme = True - else: - rebootme = False - conf['system'] = {'timezone': tzin, 'locale': localein, 'chrootpath': chrootpathin, 'kbd': kbdin, 'reboot': rebootme} - syshelp.append('https://aif.square-r00t.net/#code_users_code') - print(('\n{0}== USERS =={1}\n\tNOTE: For passwords, you can either enter the password you want to use,\n' + - '\ta \'!\' (in which case TTY login will be disabled but e.g. SSH will still work), or just hit enter to leave it blank\n' + - '\t(which is HIGHLY not recommended - it means anyone can login by just pressing enter at the login!)').format(color.BOLD, color.END)) - print('{0}=== ROOT ==={1}'.format(color.BOLD, color.END)) - conf['system']['rootpass'] = genPassHash('root') - print('{0}=== REGULAR USERS ==={1}'.format(color.BOLD, color.END)) - moreusers = input('* Would you like to add regular user(s)? (y/{0}n{1}) '.format(color.BOLD, color.END)) - if re.match('^y(es)?$', moreusers.lower()): - syshelp.append('https://aif.square-r00t.net/#code_user_code') - conf['system']['users'] = userPrompt(syshelp) - else: - conf['system']['users'] = False - svchelp = ['https://wiki.archlinux.org/index.php/Systemd', - 'https://aif.square-r00t.net/#code_service_code'] - print('{0}== SERVICES =={1}'.format(color.BOLD, color.END)) - svcin = chkPrompt('* Would you like to configure (enable/disable) services? (y/{0}n{1}) '.format(color.BOLD, color.END), svchelp) - if re.match('^y(es)?$', svcin.lower()): - conf['system']['services'] = svcsPrompt(svchelp) - else: - conf['system']['services'] = False - print('\n{0}== PACKAGES/SOFTWARE =={1}'.format(color.BOLD, color.END)) - conf['software'] = {} - pkgrhelp = ['https://wiki.archlinux.org/index.php/Pacman', - 'https://wiki.archlinux.org/index.php/AUR_helpers', - 'https://aif.square-r00t.net/#code_pacman_code'] - pkgrcmd = chkPrompt('* If you won\'t be using pacman for a package manager, what command should be used to install packages?\n' + - '\t(Remember that you would need to install/configure it in a \'pkg\' hook script.)\n' + - '\tLeave blank if you\'ll only be using pacman: ', pkgrhelp) - if pkgrcmd == '': - conf['software']['pkgr'] = False - else: - conf['software']['pkgr'] = pkgrcmd - print('\n{0}=== REPOSITORIES/PACKAGES ==={1}'.format(color.BOLD, color.END)) - repohelp = ['https://aif.square-r00t.net/#code_repos_code'] - conf['software']['repos'] = repoPrompt(repohelp) - mirrorhelp = ['https://wiki.archlinux.org/index.php/installation_guide#Select_the_mirrors', - 'https://aif.square-r00t.net/#code_mirrorlist_code', - 'https://aif.square-r00t.net/#code_mirror_code'] - conf['software']['mirrors'] = mirrorPrompt(mirrorhelp) - if pkgrcmd == '': - pkgrcmd = 'pacman --needed --noconfirm -S' - pkgsin = chkPrompt(('* Would you like to install extra packages?\n' + - '\t(Note that they must be available in your configured repositories or\n' + - '\tinstallable via "{0} ".) (y/{1}n{2}) ').format(pkgrcmd, color.BOLD, color.END), repohelp) - if re.match('^y(es)?$', pkgsin.lower()): - repohelp.append('https://aif.square-r00t.net/#code_package_code') - conf['software']['packages'] = pkgsPrompt(repohelp) - else: - conf['software']['packages'] = False - btldrhelp = ['https://wiki.archlinux.org/index.php/installation_guide#Boot_loader', - 'https://aif.square-r00t.net/#code_bootloader_code'] - conf['boot'] = {} - print('{0}== BOOTLOADER =={1}'.format(color.BOLD, color.END)) - btldrin = chkPrompt('* Please choose a bootloader. ({0}grub{1}/systemd) '.format(color.BOLD, color.END), btldrhelp) - if btldrin == '': - btldrin = 'grub' - elif not re.match('^(grub|systemd)$', btldrin.lower()): - exit(' !! ERROR: You must choose a bootloader between grub or systemd.') - - conf['boot']['bootloader'] = btldrin.lower() - bttgtstr = 'boot partition/disk' - btrgx = re.compile('^/dev/[A-Za-z0]+') - if btldrin.lower() == 'grub': - efienable = chkPrompt('** Is this system (U)EFI-capable? ({0}y{1}/n) '.format(color.BOLD, color.END), btldrhelp) - if re.match('^no?$', efienable.lower()): - conf['boot']['efi'] = False - else: - conf['boot']['efi'] = True - bttgtstr = 'ESP (EFI System Partition)' - btrgx = re.compile('^/([^/\x00\s]+(/)?)+$') - bttgtin = chkPrompt('** What is the target for {0}? That is, the path to the {1} (within the chroot): '.format(btldrin.lower(), bttgtstr), btldrhelp) - if not btrgx.match(bttgtin): - exit(' !! ERROR: That doesn\'t seem to be a valid {0}.'.format(bttgtstr)) - else: - conf['boot']['target'] = bttgtin - scrpthlp = ['https://aif.square-r00t.net/#code_script_code'] - print('{0}= HOOK SCRIPTS ={1}'.format(color.BOLD, color.END)) - scrptsin = chkPrompt('* Do you have any hook scripts you\'d like to add? (y/{0}n{1}) '.format(color.BOLD, color.END), scrpthlp) - if re.match('^y(es)?$', scrptsin.lower()): - conf['scripts'] = scrptPrompt(scrpthlp) - else: - conf['scripts'] = False - print('\n\n[{0}] {1}ALL DONE!{2} Whew. You can find your configuration file at: {3}{4}{2}\n'.format(datetime.datetime.now(), - color.BOLD, - color.END, - color.BLUE, - self.args['cfgfile'])) - if self.args['verbose']: - import pprint - pprint.pprint(conf) - if self.args['verbose_raw']: - print(conf) - return(conf) - - def convertJSON(self): - with open(self.args['inputfile'], 'r') as f: - try: - conf = json.load(f) - except: - exit(' !! ERROR: {0} does not seem to be a strict JSON file.'.format(args['inputfile'])) - return(conf) - - def validateXML(self): - # First we validate the XSD. - if not lxml_avail: - exit('\nXML validation is only supported by LXML.\n' + - 'If you want to validate the XML, install the lxml python module (python-lxml) ' + - 'and run:\n\t{0} validate -f {1}.\n'.format(sys.argv[0], self.args['cfgfile'])) - try: - xsd = etree.XMLSchema(self.getXSD()) - print('\nXSD: {0}PASSED{1}'.format(color.BOLD, color.END)) - except Exception as e: - exit('\nXSD: {0}FAILED{1}: {2}'.format(color.BOLD, color.END, e)) - # Then we can validate the XML. - try: - xml = xsd.validate(self.getXML()) - print('XML: {0}PASSED{1}\n'.format(color.BOLD, color.END)) - except Exception as e: - print('XML: {0}FAILED{1}: {2}\n'.format(color.BOLD, color.END, e)) - - def genXMLFile(self, conf): - namespaces = {'aif': 'http://aif.square-r00t.net/', 'xsi': 'http://www.w3.org/2001/XMLSchema-instance'} - xsi = {'{http://www.w3.org/2001/XMLSchema-instance}schemaLocation' : 'http://aif.square-r00t.net aif.xsd'} - #for ns in namespaces.keys(): - # etree.register_namespace(ns, namespaces[ns]) - if lxml_avail: - genname = 'LXML (http://lxml.de/)' - root = etree.Element('aif', nsmap = namespaces, attrib = xsi) - #xml = etree.ElementTree(root) - else: - genname = 'Python stdlib "xml" module' - for ns in namespaces.keys(): - etree.register_namespace(ns, namespaces[ns]) - root = etree.Element('aif') - if self.args['oper'] == 'convert': - fromstr = self.args['inputfile'] - else: - fromstr = 'interactive commandline' - root.append(etree.Comment('Generated by {0} on {1} from {2} via {3}'.format(sys.argv[0], datetime.datetime.now(), fromstr, genname))) - root.append(etree.Comment('THIS FILE CONTAINS SENSITIVE INFORMATION. SHARE/SCRUB WISELY.')) - # /aif/ required sections - for e in ('storage', 'network', 'system', 'pacman', 'bootloader'): - root.append(etree.Element(e)) - # /aif/ optional sections - if 'scripts' in conf.keys() and conf['scripts']: - root.append(etree.Element('scripts')) - # /aif/storage - strg = root.find('storage') - for d in conf['disks'].keys(): - # /aif/storage/disk - disk = etree.Element('disk', device = d, diskfmt = conf['disks'][d]['fmt']) - for p in conf['disks'][d]['parts'].keys(): - # /aif/storage/disk/part - start = conf['disks'][d]['parts'][p]['start'] - stop = conf['disks'][d]['parts'][p]['stop'] - fstype = conf['disks'][d]['parts'][p]['fstype'] - disk.append(etree.Element('part', num = p, start = start, stop = stop, fstype = fstype)) - strg.append(disk) - # /aif/storage/mount - for m in conf['mounts'].keys(): - mnt = {} - mnt['order'] = m - mnt['source'] = conf['mounts'][m]['device'] - mnt['target'] = conf['mounts'][m]['target'] - # These are optional, hence the splat and mnt dict. - for o in ('fstype', 'opts'): - if o in conf['mounts'][m].keys() and conf['mounts'][m][o]: - mnt[o] = conf['mounts'][m][o] - mount = etree.Element('mount', **mnt) - strg.append(mount) - # /aif/network - ntwk = root.find('network') - ntwk.set('hostname', conf['network']['hostname']) - for i in conf['network']['ifaces'].keys(): - # /aif/network/iface - optmap = {'gw': 'gateway', 'proto': 'netproto', 'resolvers': 'resolvers'} - iface = {} - iface['device'] = i - iface['address'] = conf['network']['ifaces'][i]['address'] - for o in optmap.keys(): - if conf['network']['ifaces'][i][o]: - if o == 'resolvers': - iface[optmap[o]] = ','.join(conf['network']['ifaces'][i][o]) - else: - iface[optmap[o]] = conf['network']['ifaces'][i][o] - interface = etree.Element('iface', **iface) - ntwk.append(interface) - # /aif/system - systm = root.find('system') - for a in ('timezone', 'locale', 'chrootpath', 'kbd', 'reboot'): - if isinstance(conf['system'][a], bool): - val = str(conf['system'][a]).lower() - else: - val = conf['system'][a] - systm.set(a, val) - # /aif/system/users - usrs = etree.Element('users', rootpass = conf['system']['rootpass']) - subs = ('home', 'xgroups') - optional = ('uid', 'group', 'gid') - if conf['system']['users']: - for u in conf['system']['users'].keys(): - # /aif/system/users/user - o = {} - o['name'] = u - for i in conf['system']['users'][u].keys(): - if isinstance(conf['system']['users'][u][i], bool): - val = str(conf['system']['users'][u][i]).lower() - else: - val = conf['system']['users'][u][i] - if i not in subs: # we handle "subs" as subelements - if i in optional: # and we only add optional attribs if they're populated - if conf['system']['users'][u][i]: - o[i] = val - else: - o[i] = val - user = etree.Element('user', **o) - # /aif/system/users/user/home - if conf['system']['users'][u]['home']: - o = {} - o['create'] = str(conf['system']['users'][u]['home']['create']).lower() - if 'path' in conf['system']['users'][u]['home'].keys(): - o['path'] = conf['system']['users'][u]['home']['path'] - home = etree.Element('home', **o) - user.append(home) - # /aig/system/users/user/xgroup - if conf['system']['users'][u]['xgroups']: - for g in conf['system']['users'][u]['xgroups'].keys(): - o = {} - o['name'] = g - o['create'] = str(conf['system']['users'][u]['xgroups'][g]['create']).lower() - if 'gid' in conf['system']['users'][u]['xgroups'][g].keys() and conf['system']['users'][u]['xgroups'][g]['gid']: - o['gid'] = conf['system']['users'][u]['xgroups'][g]['gid'] - xgrp = etree.Element('xgroup', **o) - user.append(xgrp) - usrs.append(user) - systm.append(usrs) - # /aif/system/service - if conf['system']['services']: - for s in conf['system']['services'].keys(): - o = {} - o['name'] = s - o['status'] = str(conf['system']['services'][s]).lower() - svc = etree.Element('service', **o) - systm.append(svc) - # /aif/pacman - pcmn = root.find('pacman') - if conf['software']['pkgr']: - pcmn.set('command', conf['software']['pkgr']) - # /aif/pacman/repo - repos = etree.Element('repos') - for r in conf['software']['repos'].keys(): - o = {} - o['name'] = r - o['enabled'] = str(conf['software']['repos'][r]['enabled']).lower() - o['siglevel'] = conf['software']['repos'][r]['siglevel'] - o['mirror'] = conf['software']['repos'][r]['mirror'] - repo = etree.Element('repo', **o) - repos.append(repo) - pcmn.append(repos) - # /aif/pacman/mirrorlist - if 'mirrors' in conf['software'].keys() and conf['software']['mirrors']: - mrlst = etree.Element('mirrorlist') - for m in conf['software']['mirrors']: - # /aif/pacman/mirrorlist/mirror - mirror = etree.Element('mirror') - mirror.text = m - mrlst.append(mirror) - pcmn.append(mrlst) - # /aif/pacman/software - if 'packages' in conf['software'].keys() and conf['software']['packages']: - sftwr = etree.Element('software') - for p in conf['software']['packages'].keys(): - # /aif/pacman/software/package - pkg = etree.Element('package') - pkg.set('name', p) - if conf['software']['packages'][p]: - if conf['software']['packages'][p] not in (None, 'None'): # fix JSON not parsing "None" - pkg.set('repo', conf['software']['packages'][p]) - sftwr.append(pkg) - pcmn.append(sftwr) - # /aif/bootloader - btldr = root.find('bootloader') - optmap = {'bttype': 'type', 'efi': 'efi', 'bttgt': 'target'} - opts = {} - opts['bttype'] = conf['boot']['bootloader'] - opts['efi'] = str(conf['boot']['efi']).lower() - opts['bttgt'] = conf['boot']['target'] - for k in optmap.keys(): - btldr.set(optmap[k], opts[k]) - # /aif/scripts - if 'scripts' in conf.keys() and conf['scripts']: - scrpts = root.find('scripts') - # /aif/scripts/script@execution - for t in ('pre', 'pkg', 'post'): - # /aif/scripts/script@order - if t in conf['scripts'].keys() and conf['scripts'][t]: - for n in conf['scripts'][t].keys(): - # /aif/scripts/script@uri - uri = conf['scripts'][t][n]['uri'] - scrpt = etree.Element('script', execution = t, order = n, uri = uri) - # /aif/scripts/script@authtype - if 'auth' in conf['scripts'][t][n].keys() and conf['scripts'][t][n]['auth']: - scrpt.set('authtype', conf['scripts'][t][n]['auth']) - # /aif/scripts/script@realm - if conf['scripts'][t][n]['auth'] == 'digest': - if 'realm' in conf['scripts'][t][n].keys(): - scrpt.set('realm', conf['scripts'][t][n]['realm']) - # /aif/scripts/script@user - scrpt.set('user', conf['scripts'][t][n]['user']) - # /aif/scripts/script@password - scrpt.set('password', conf['scripts'][t][n]['password']) - scrpts.append(scrpt) - # debugging - if xmldebug: - if lxml_avail: - # LXML - print(etree.tostring(root, xml_declaration = True, encoding = 'utf-8', pretty_print = True).decode('utf-8')) - else: - # XML - import xml.dom.minidom - xmlstr = etree.tostring(root, encoding = 'utf-8') - # holy cats, the xml module sucks. - nsstr = '' - for ns in namespaces.keys(): - nsstr += ' xmlns:{0}="{1}"'.format(ns, namespaces[ns]) - for x in xsi.keys(): - xsiname = x.split('}')[1] - nsstr += ' xsi:{0}="{1}"'.format(xsiname, xsi[x]) - outstr = xml.dom.minidom.parseString(xmlstr).toprettyxml(indent = ' ').splitlines() - outstr[0] = '' - outstr[1] = ''.format(nsstr) - print('\n'.join(outstr)) - # end debugging - # https://stackoverflow.com/questions/4886189/python-namespaces-in-xml-elementtree-or-lxml - if lxml_avail: - xml = etree.ElementTree(root) - with open(self.args['cfgfile'], 'wb') as f: - xml.write(f, xml_declaration = True, encoding='utf-8', pretty_print = True) - else: - import xml.dom.minidom - xmlstr = etree.tostring(root, encoding = 'utf-8') - # holy cats, the xml module sucks. - nsstr = '' - for ns in namespaces.keys(): - nsstr += ' xmlns:{0}="{1}"'.format(ns, namespaces[ns]) - for x in xsi.keys(): - xsiname = x.split('}')[1] - nsstr += ' xsi:{0}="{1}"'.format(xsiname, xsi[x]) - outstr = xml.dom.minidom.parseString(xmlstr).toprettyxml(indent = ' ').splitlines() - outstr[0] = '' - outstr[1] = ''.format(nsstr) - with open(self.args['cfgfile'], 'w') as f: # TODO: test this. print() wrap it necessary? - f.write('\n'.join(outstr)) - return(root) - - def main(self): - if self.args['oper'] == 'create': - conf = self.getOpts() - elif self.args['oper'] == 'convert': - conf = self.convertJSON() - if self.args['oper'] in ('create', 'convert'): - self.genXMLFile(conf) - if self.args['oper'] in ('create', 'convert', 'validate'): - self.validateXML() - -def parseArgs(): - args = argparse.ArgumentParser(description = 'AIF-NG Configuration Generator', - epilog = 'TIP: this program has context-specific help. e.g. try:\n\t%(prog)s create --help', - formatter_class = argparse.RawTextHelpFormatter) - commonargs = argparse.ArgumentParser(add_help = False) - commonargs.add_argument('-f', - '--file', - dest = 'cfgfile', - help = 'The file to create/validate. If not specified, defaults to ./aif.xml', - default = '{0}/aif.xml'.format(os.getcwd())) - subparsers = args.add_subparsers(help = 'Operation to perform', - dest = 'oper') - createargs = subparsers.add_parser('create', - help = 'Create an AIF-NG XML configuration file.', - parents = [commonargs]) - validateargs = subparsers.add_parser('validate', - help = 'Validate an AIF-NG XML configuration file.', - parents = [commonargs]) - convertargs = subparsers.add_parser('convert', - help = 'Convert a "more" human-readable JSON configuration file to AIF-NG-compatible XML.', - parents = [commonargs]) - createargs.add_argument('-v', - '--verbose', - dest = 'verbose', - action = 'store_true', - help = 'Print the dict of raw values used to create the XML. Mostly/only useful for debugging.') - createargs.add_argument('-v:r', - '--verbose-raw', - dest = 'verbose_raw', - action = 'store_true', - help = 'Like -v, but prints the unformatted dict.') - convertargs.add_argument('-i', - '--input', - dest = 'inputfile', - required = True, - help = 'The JSON file to import and convert into XML.') - return(args) - -def verifyArgs(args): - args['cfgfile'] = os.path.normpath(os.path.abspath(os.path.expanduser(args['cfgfile']))) - args['cfgfile'] = re.sub('^/+', '/', args['cfgfile']) - # Path/file handling - make sure we can create the parent dir if it doesn't exist, - # check that we can write to the file, etc. - if args['oper'] in ('create', 'convert'): - args['cfgbak'] = '{0}.bak.{1}'.format(args['cfgfile'], int(datetime.datetime.utcnow().timestamp())) - try: - temp = True - if os.path.lexists(args['cfgfile']): - temp = False - os.makedirs(os.path.dirname(args['cfgfile']), exist_ok = True) - with open(args['cfgfile'], 'a') as f: - f.write('') - if temp: - os.remove(args['cfgfile']) - except OSError as e: - print('\nERROR: {0}: {1}'.format(e.strerror, e.filename)) - exit(('\nWe encountered an error when trying to use path {0}.\n' + - 'Please review the output and address any issues present.').format(args['cfgfile'])) - if args['oper'] == 'convert': - # And we need to make sure we have read perms to the JSON input file. - try: - with open(args['inputfile'], 'r') as f: - f.read() - except OSError as e: - print('\nERROR: {0}: {1}'.format(e.strerror, e.filename)) - exit(('\nWe encountered an error when trying to read path {0}.\n' + - 'Please review the output and address any issues present.').format(args['inputfile'])) - return(args) - -def main(): - args = vars(parseArgs().parse_args()) - if not args['oper']: - parseArgs().print_help() - else: - aif = aifgen(verifyArgs(args)) - aif.main() - -if __name__ == '__main__': - main() diff --git a/aif.xsd b/aif.xsd index d20925a..ceb5b8e 100644 --- a/aif.xsd +++ b/aif.xsd @@ -3,352 +3,358 @@ targetNamespace="http://aif.square-r00t.net" xmlns="http://aif.square-r00t.net" elementFormDefault="qualified"> - - - See https://aif.square-r00t.net/ for more information about this project. - - - - - - - This element specifies a type to be used for validating storage devices, such as hard disks or mdadm-managed devices. - - - - - - + + + See https://aif.square-r00t.net/ for more information about this project. + + + + + + + This element specifies a type to be used for validating storage devices, such as hard disks or + mdadm-managed devices. + + + + + + - - - - This element specifies a type to validate what kind of disk formatting. Accepts either GPT or BIOS (for MBR systems) only. - - - - - - + + + + This element specifies a type to validate what kind of disk formatting. Accepts either GPT or BIOS (for + MBR systems) only. + + + + + + - - - - This element validates a disk size specification for a partition. Same rules apply as those in parted's size specification. - - - - - - + + + + This element validates a disk size specification for a partition. Same rules apply as those in parted's + size specification. + + + + + + - - - - This element validates a filesystem type to be specified for formatting a partition. See sgdisk -L (or the table at http://www.rodsbooks.com/gdisk/walkthrough.html) for valid filesystem codes. - - - - - - - - - - - - + + + + This element validates a filesystem type to be specified for formatting a partition. See sgdisk -L (or + the table at http://www.rodsbooks.com/gdisk/walkthrough.html) for valid filesystem codes. + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + - - - - - - - - - - - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/aif/__init__.py b/aif/__init__.py new file mode 100644 index 0000000..2c80a0a --- /dev/null +++ b/aif/__init__.py @@ -0,0 +1,3 @@ +class AIF(object): + def __init__(self): + pass diff --git a/aif/config.py b/aif/config.py new file mode 100644 index 0000000..ff4df42 --- /dev/null +++ b/aif/config.py @@ -0,0 +1,17 @@ +import os +## +from lxml import etree + +class Config(object): + def __init__(self): + self.xml = None + + def parseLocalFile(self, fpath): + fpath = os.path.abspath(os.path.expanduser(fpath)) + pass + + def parseRemoteFile(self, url): + pass + + def parseRawContent(self, content): + pass diff --git a/aif/constants.py b/aif/constants.py new file mode 100644 index 0000000..e69de29 diff --git a/aif/disk.py b/aif/disk.py new file mode 100644 index 0000000..905bc15 --- /dev/null +++ b/aif/disk.py @@ -0,0 +1,15 @@ +# To reproduce sgdisk behaviour in v1 of AIF-NG: +# https://gist.github.com/herry13/5931cac426da99820de843477e41e89e +# https://github.com/dcantrell/pyparted/blob/master/examples/query_device_capacity.py +# TODO: Remember to replicate genfstab behaviour. + +try: + # https://stackoverflow.com/a/34812552/733214 + # https://github.com/karelzak/util-linux/blob/master/libmount/python/test_mount_context.py#L6 + import libmount as mount +except ImportError: + # We should never get here. util-linux is part of core (base) in Arch and uses "libmount". + import pylibmount as mount +## +import parted +import psutil diff --git a/aif/envsetup.py b/aif/envsetup.py new file mode 100644 index 0000000..143b315 --- /dev/null +++ b/aif/envsetup.py @@ -0,0 +1,50 @@ +# We use a temporary venv to ensure we have all the external libraries we need. +# This removes the necessity of extra libs at runtime. If you're in an environment that doesn't have access to PyPI/pip, +# you'll need to customize the install host (typically the live CD/live USB) to have them installed as system packages. +# Before you hoot and holler about this, Let's Encrypt's certbot-auto does the same thing. +# Except I segregate it out even further; I don't even install pip into the system python. + +import ensurepip +import json +import os +import tempfile +import subprocess +import sys +import venv + +# TODO: a more consistent way of managing deps? +depmods = ['gpg', 'requests', 'lxml', 'psutil', 'pyparted', 'pytz', 'passlib', 'validators'] + +class EnvBuilder(object): + def __init__(self): + self.vdir = tempfile.mkdtemp(prefix = '.aif_', suffix = '_VENV') + self.venv = venv.create(self.vdir, system_site_packages = True, clear = True, with_pip = True) + ensurepip.bootstrap(root = self.vdir) + # pip does some dumb env var things and doesn't clean up after itself. + for v in ('PIP_CONFIG_FILE', 'ENSUREPIP_OPTIONS', 'PIP_REQ_TRACKER', 'PLAT'): + if os.environ.get(v): + del(os.environ[v]) + moddir_raw = subprocess.run([os.path.join(self.vdir, + 'bin', + 'python3'), + '-c', + ('import site; ' + 'import json; ' + 'print(json.dumps(site.getsitepackages(), indent = 4))')], + stdout = subprocess.PIPE) + self.modulesdir = json.loads(moddir_raw.stdout.decode('utf-8'))[0] + # This is SO. DUMB. WHY DO I HAVE TO CALL PIP FROM A SHELL. IT'S WRITTEN IN PYTHON. + # https://pip.pypa.io/en/stable/user_guide/#using-pip-from-your-program + # TODO: logging + for m in depmods: + pip_cmd = [os.path.join(self.vdir, + 'bin', + 'python3'), + '-m', + 'pip', + 'install', + '--disable-pip-version-check', + m] + subprocess.run(pip_cmd) + # And now make it available to other components. + sys.path.insert(1, self.modulesdir) diff --git a/aif/log.py b/aif/log.py new file mode 100644 index 0000000..eb6bb1d --- /dev/null +++ b/aif/log.py @@ -0,0 +1 @@ +import logging \ No newline at end of file diff --git a/aif/network.py b/aif/network.py new file mode 100644 index 0000000..41c438f --- /dev/null +++ b/aif/network.py @@ -0,0 +1 @@ +import ipaddress diff --git a/aif/pacman.py b/aif/pacman.py new file mode 100644 index 0000000..4d0270c --- /dev/null +++ b/aif/pacman.py @@ -0,0 +1,6 @@ +# We can manually bootstrap and alter pacman's keyring. But check the bootstrap tarball; we might not need to. +# TODO. + +import os +## +import gpg diff --git a/aif/users.py b/aif/users.py new file mode 100644 index 0000000..62c6cce --- /dev/null +++ b/aif/users.py @@ -0,0 +1,9 @@ +# There isn't a python package that can manage *NIX users (well), unfortunately. +# So we do something stupid: +# https://www.tldp.org/LDP/sag/html/adduser.html +# https://unix.stackexchange.com/a/153227/284004 +# https://wiki.archlinux.org/index.php/users_and_groups#File_list + +import os +## +import passlib # validate password hash/gen hash \ No newline at end of file diff --git a/aifclient.py b/aifclient.py deleted file mode 100755 index e8ea2bf..0000000 --- a/aifclient.py +++ /dev/null @@ -1,958 +0,0 @@ -#!/usr/bin/env python3 - -## REQUIRES: ## -# parted # -# sgdisk ### (yes, both) -# python 3 with standard library -# (OPTIONAL) lxml -# pacman in the host environment -# arch-install-scripts: https://www.archlinux.org/packages/extra/any/arch-install-scripts/ -# a network connection -# the proper kernel arguments. - -try: - from lxml import etree - lxml_avail = True -except ImportError: - import xml.etree.ElementTree as etree # https://docs.python.org/3/library/xml.etree.elementtree.html - lxml_avail = False -import datetime -import shlex -import fileinput -import os -import shutil -import re -import socket -import subprocess -import ipaddress -import copy -import urllib.request as urlrequest -import urllib.parse as urlparse -import urllib.response as urlresponse -from ftplib import FTP_TLS -from io import StringIO - -logfile = '/root/aif.log.{0}'.format(int(datetime.datetime.utcnow().timestamp())) - -class aif(object): - - def __init__(self): - pass - - def kernelargs(self): - if 'DEBUG' in os.environ.keys(): - kernelparamsfile = '/tmp/cmdline' - else: - kernelparamsfile = '/proc/cmdline' - args = {} - args['aif'] = False - # For FTP or HTTP auth - args['aif_user'] = False - args['aif_password'] = False - args['aif_auth'] = False - args['aif_realm'] = False - args['aif_auth'] = 'basic' - with open(kernelparamsfile, 'r') as f: - cmdline = f.read() - for p in shlex.split(cmdline): - if p.startswith('aif'): - param = p.split('=') - if len(param) == 1: - param.append(True) - args[param[0]] = param[1] - if not args['aif']: - exit('You do not have AIF enabled. Exiting.') - args['aif_auth'] = args['aif_auth'].lower() - return(args) - - def getConfig(self, args = False): - if not args: - args = self.kernelargs() - # Sanitize the user specification and find which protocol to use - prefix = args['aif_url'].split(':')[0].lower() - # Use the urllib module - if prefix in ('http', 'https', 'file', 'ftp'): - if args['aif_user'] and args['aif_password']: - # Set up Basic or Digest auth. - passman = urlrequest.HTTPPasswordMgrWithDefaultRealm() - if not args['aif_realm']: - passman.add_password(None, args['aif_url'], args['aif_user'], args['aif_password']) - else: - passman.add_password(args['aif_realm'], args['aif_url'], args['aif_user'], args['aif_password']) - if args['aif_auth'] == 'digest': - httpauth = urlrequest.HTTPDigestAuthHandler(passman) - else: - httpauth = urlrequest.HTTPBasicAuthHandler(passman) - httpopener = urlrequest.build_opener(httpauth) - urlrequest.install_opener(httpopener) - with urlrequest.urlopen(args['aif_url']) as f: - conf = f.read() - elif prefix == 'ftps': - if args['aif_user']: - username = args['aif_user'] - else: - username = 'anonymous' - if args['aif_password']: - password = args['aif_password'] - else: - password = 'anonymous' - filepath = '/'.join(args['aif_url'].split('/')[3:]) - server = args['aif_url'].split('/')[2] - content = StringIO() - ftps = FTP_TLS(server) - ftps.login(username, password) - ftps.prot_p() - ftps.retrlines("RETR " + filepath, content.write) - conf = content.getvalue() - else: - exit('{0} is not a recognised URI type specifier. Must be one of http, https, file, ftp, or ftps.'.format(prefix)) - return(conf) - - def webFetch(self, uri, auth = False): - # Sanitize the user specification and find which protocol to use - prefix = uri.split(':')[0].lower() - # Use the urllib module - if prefix in ('http', 'https', 'file', 'ftp'): - if auth: - if 'user' in auth.keys() and 'password' in auth.keys(): - # Set up Basic or Digest auth. - passman = urlrequest.HTTPPasswordMgrWithDefaultRealm() - if not 'realm' in auth.keys(): - passman.add_password(None, uri, auth['user'], auth['password']) - else: - passman.add_password(auth['realm'], uri, auth['user'], auth['password']) - if auth['type'] == 'digest': - httpauth = urlrequest.HTTPDigestAuthHandler(passman) - else: - httpauth = urlrequest.HTTPBasicAuthHandler(passman) - httpopener = urlrequest.build_opener(httpauth) - urlrequest.install_opener(httpopener) - with urlrequest.urlopen(uri) as f: - data = f.read() - elif prefix == 'ftps': - if auth: - if 'user' in auth.keys(): - username = auth['user'] - else: - username = 'anonymous' - if 'password' in auth.keys(): - password = auth['password'] - else: - password = 'anonymous' - filepath = '/'.join(uri.split('/')[3:]) - server = uri.split('/')[2] - content = StringIO() - ftps = FTP_TLS(server) - ftps.login(username, password) - ftps.prot_p() - ftps.retrlines("RETR " + filepath, content.write) - data = content.getvalue() - else: - exit('{0} is not a recognised URI type specifier. Must be one of http, https, file, ftp, or ftps.'.format(prefix)) - return(data) - - def getXML(self, confobj = False): - if not confobj: - confobj = self.getConfig() - xmlobj = etree.fromstring(confobj) - return(xmlobj) - - def buildDict(self, xmlobj = False): - if not xmlobj: - xmlobj = self.getXML() - # Set up the skeleton dicts - aifdict = {} - for i in ('disk', 'mount', 'network', 'system', 'users', 'software', 'scripts'): - aifdict[i] = {} - for i in ('network.ifaces', 'system.bootloader', 'system.services', 'users.root'): - i = i.split('.') - dictname = i[0] - keyname = i[1] - aifdict[dictname][keyname] = {} - aifdict['scripts']['pre'] = False - aifdict['scripts']['post'] = False - aifdict['users']['root']['password'] = False - for i in ('repos', 'mirrors', 'packages'): - aifdict['software'][i] = {} - # Set up the dict elements for disk partitioning - for i in xmlobj.findall('storage/disk'): - disk = i.attrib['device'] - fmt = i.attrib['diskfmt'].lower() - if not fmt in ('gpt', 'bios'): - exit('Device {0}\'s format "{1}" is not a valid type (one of gpt, bios).'.format(disk, - fmt)) - aifdict['disk'][disk] = {} - aifdict['disk'][disk]['fmt'] = fmt - aifdict['disk'][disk]['parts'] = {} - for x in i: - if x.tag == 'part': - partnum = x.attrib['num'] - aifdict['disk'][disk]['parts'][partnum] = {} - for a in x.attrib: - aifdict['disk'][disk]['parts'][partnum][a] = x.attrib[a] - # Set up mountpoint dicts - for i in xmlobj.findall('storage/mount'): - device = i.attrib['source'] - mntpt = i.attrib['target'] - order = int(i.attrib['order']) - if 'fstype' in i.keys(): - fstype = i.attrib['fstype'] - else: - fstype = None - if 'opts' in i.keys(): - opts = i.attrib['opts'] - else: - opts = None - aifdict['mount'][order] = {} - aifdict['mount'][order]['device'] = device - aifdict['mount'][order]['mountpt'] = mntpt - aifdict['mount'][order]['fstype'] = fstype - aifdict['mount'][order]['opts'] = opts - # Set up networking dicts - aifdict['network']['hostname'] = xmlobj.find('network').attrib['hostname'] - for i in xmlobj.findall('network/iface'): - # Create a dict for the iface name. - iface = i.attrib['device'] - proto = i.attrib['netproto'] - address = i.attrib['address'] - if 'gateway' in i.attrib.keys(): - gateway = i.attrib['gateway'] - else: - gateway = False - if 'resolvers' in i.attrib.keys(): - resolvers = i.attrib['resolvers'] - else: - resolvers = False - if iface not in aifdict['network']['ifaces'].keys(): - aifdict['network']['ifaces'][iface] = {} - if proto not in aifdict['network']['ifaces'][iface].keys(): - aifdict['network']['ifaces'][iface][proto] = {} - if 'gw' not in aifdict['network']['ifaces'][iface][proto].keys(): - aifdict['network']['ifaces'][iface][proto]['gw'] = gateway - aifdict['network']['ifaces'][iface][proto]['addresses'] = [] - aifdict['network']['ifaces'][iface][proto]['addresses'].append(address) - aifdict['network']['ifaces'][iface]['resolvers'] = [] - if resolvers: - for ip in filter(None, re.split('[,\s]+', resolvers)): - if ip not in aifdict['network']['ifaces'][iface]['resolvers']: - aifdict['network']['ifaces'][iface]['resolvers'].append(ip) - else: - aifdict['network']['ifaces'][iface][proto]['resolvers'] = False - # Set up the users dicts - aifdict['users']['root']['password'] = xmlobj.find('system/users').attrib['rootpass'] - for i in xmlobj.findall('system/users'): - for x in i: - username = x.attrib['name'] - aifdict['users'][username] = {} - for a in ('uid', 'group', 'gid', 'password', 'comment', 'sudo'): - if a in x.attrib.keys(): - aifdict['users'][username][a] = x.attrib[a] - else: - aifdict['users'][username][a] = None - sudo = (x.attrib['sudo']).lower() in ('true', '1') - aifdict['users'][username]['sudo'] = sudo - # And we also need to handle the homedir and xgroup situation - for n in ('home', 'xgroup'): - aifdict['users'][username][n] = False - for a in x: - if not aifdict['users'][username][a.tag]: - aifdict['users'][username][a.tag] = {} - for b in a.attrib: - if a.tag == 'xgroup': - if b == 'name': - groupname = a.attrib[b] - if groupname not in aifdict['users'][username]['xgroup'].keys(): - aifdict['users'][username]['xgroup'][a.attrib[b]] = {} - else: - aifdict['users'][username]['xgroup'][a.attrib['name']][b] = a.attrib[b] - else: - aifdict['users'][username][a.tag][b] = a.attrib[b] - # And fill in any missing values. We could probably use the XSD and use of defaults to do this, but... oh well. - if isinstance(aifdict['users'][username]['xgroup'], dict): - for g in aifdict['users'][username]['xgroup'].keys(): - for k in ('create', 'gid'): - if k not in aifdict['users'][username]['xgroup'][g].keys(): - aifdict['users'][username]['xgroup'][g][k] = False - elif k == 'create': - aifdict['users'][username]['xgroup'][g][k] = aifdict['users'][username]['xgroup'][g][k].lower() in ('true', '1') - if isinstance(aifdict['users'][username]['home'], dict): - for k in ('path', 'create'): - if k not in aifdict['users'][username]['home'].keys(): - aifdict['users'][username]['home'][k] = False - elif k == 'create': - aifdict['users'][username]['home'][k] = aifdict['users'][username]['home'][k].lower() in ('true', '1') - # Set up the system settings, if applicable. - aifdict['system']['timezone'] = False - aifdict['system']['locale'] = False - aifdict['system']['kbd'] = False - aifdict['system']['chrootpath'] = False - aifdict['system']['reboot'] = False - for i in ('locale', 'timezone', 'kbd', 'chrootpath', 'reboot'): - if i in xmlobj.find('system').attrib: - aifdict['system'][i] = xmlobj.find('system').attrib[i] - aifdict['system']['reboot'] = aifdict['system']['reboot'].lower() in ('true', '1') - # And now services... - if xmlobj.find('system/service') is None: - aifdict['system']['services'] = False - else: - for x in xmlobj.findall('system/service'): - svcname = x.attrib['name'] - state = x.attrib['status'].lower() in ('true', '1') - aifdict['system']['services'][svcname] = {} - aifdict['system']['services'][svcname]['status'] = state - # And software. First the mirror list. - if xmlobj.find('pacman/mirrorlist') is None: - aifdict['software']['mirrors'] = False - else: - aifdict['software']['mirrors'] = [] - for x in xmlobj.findall('pacman/mirrorlist'): - for i in x: - aifdict['software']['mirrors'].append(i.text) - # Then the command - if 'command' in xmlobj.find('pacman').attrib: - aifdict['software']['command'] = xmlobj.find('pacman').attrib['command'] - else: - aifdict['software']['command'] = False - # And then the repo list. - for x in xmlobj.findall('pacman/repos/repo'): - repo = x.attrib['name'] - aifdict['software']['repos'][repo] = {} - aifdict['software']['repos'][repo]['enabled'] = x.attrib['enabled'].lower() in ('true', '1') - aifdict['software']['repos'][repo]['siglevel'] = x.attrib['siglevel'] - aifdict['software']['repos'][repo]['mirror'] = x.attrib['mirror'] - # And packages. - if xmlobj.find('pacman/software') is None: - aifdict['software']['packages'] = False - else: - aifdict['software']['packages'] = {} - for x in xmlobj.findall('pacman/software/package'): - aifdict['software']['packages'][x.attrib['name']] = {} - if 'repo' in x.attrib: - aifdict['software']['packages'][x.attrib['name']]['repo'] = x.attrib['repo'] - else: - aifdict['software']['packages'][x.attrib['name']]['repo'] = None - # The bootloader setup... - for x in xmlobj.find('bootloader').attrib: - aifdict['system']['bootloader'][x] = xmlobj.find('bootloader').attrib[x] - # The script setup... - if xmlobj.find('scripts') is not None: - aifdict['scripts']['pre'] = [] - aifdict['scripts']['post'] = [] - aifdict['scripts']['pkg'] = [] - tempscriptdict = {'pre': {}, 'post': {}, 'pkg': {}} - for x in xmlobj.find('scripts'): - if all(keyname in list(x.attrib.keys()) for keyname in ('user', 'password')): - auth = {} - auth['user'] = x.attrib['user'] - auth['password'] = x.attrib['password'] - if 'realm' in x.attrib.keys(): - auth['realm'] = x.attrib['realm'] - if 'authtype' in x.attrib.keys(): - auth['type'] = x.attrib['authtype'] - scriptcontents = self.webFetch(x.attrib['uri'], auth).decode('utf-8') - else: - scriptcontents = self.webFetch(x.attrib['uri']).decode('utf-8') - tempscriptdict[x.attrib['execution']][x.attrib['order']] = scriptcontents - for d in ('pre', 'post', 'pkg'): - keylst = list(tempscriptdict[d].keys()) - keylst.sort() - for s in keylst: - aifdict['scripts'][d].append(tempscriptdict[d][s]) - return(aifdict) - -class archInstall(object): - def __init__(self, aifdict): - for k, v in aifdict.items(): - setattr(self, k, v) - - def format(self): - # NOTE: the following is a dict of fstype codes to their description. - fstypes = {'0700': 'Microsoft basic data', '0c01': 'Microsoft reserved', '2700': 'Windows RE', '3000': 'ONIE config', '3900': 'Plan 9', '4100': 'PowerPC PReP boot', '4200': 'Windows LDM data', '4201': 'Windows LDM metadata', '4202': 'Windows Storage Spaces', '7501': 'IBM GPFS', '7f00': 'ChromeOS kernel', '7f01': 'ChromeOS root', '7f02': 'ChromeOS reserved', '8200': 'Linux swap', '8300': 'Linux filesystem', '8301': 'Linux reserved', '8302': 'Linux /home', '8303': 'Linux x86 root (/)', '8304': 'Linux x86-64 root (/', '8305': 'Linux ARM64 root (/)', '8306': 'Linux /srv', '8307': 'Linux ARM32 root (/)', '8400': 'Intel Rapid Start', '8e00': 'Linux LVM', 'a500': 'FreeBSD disklabel', 'a501': 'FreeBSD boot', 'a502': 'FreeBSD swap', 'a503': 'FreeBSD UFS', 'a504': 'FreeBSD ZFS', 'a505': 'FreeBSD Vinum/RAID', 'a580': 'Midnight BSD data', 'a581': 'Midnight BSD boot', 'a582': 'Midnight BSD swap', 'a583': 'Midnight BSD UFS', 'a584': 'Midnight BSD ZFS', 'a585': 'Midnight BSD Vinum', 'a600': 'OpenBSD disklabel', 'a800': 'Apple UFS', 'a901': 'NetBSD swap', 'a902': 'NetBSD FFS', 'a903': 'NetBSD LFS', 'a904': 'NetBSD concatenated', 'a905': 'NetBSD encrypted', 'a906': 'NetBSD RAID', 'ab00': 'Recovery HD', 'af00': 'Apple HFS/HFS+', 'af01': 'Apple RAID', 'af02': 'Apple RAID offline', 'af03': 'Apple label', 'af04': 'AppleTV recovery', 'af05': 'Apple Core Storage', 'bc00': 'Acronis Secure Zone', 'be00': 'Solaris boot', 'bf00': 'Solaris root', 'bf01': 'Solaris /usr & Mac ZFS', 'bf02': 'Solaris swap', 'bf03': 'Solaris backup', 'bf04': 'Solaris /var', 'bf05': 'Solaris /home', 'bf06': 'Solaris alternate sector', 'bf07': 'Solaris Reserved 1', 'bf08': 'Solaris Reserved 2', 'bf09': 'Solaris Reserved 3', 'bf0a': 'Solaris Reserved 4', 'bf0b': 'Solaris Reserved 5', 'c001': 'HP-UX data', 'c002': 'HP-UX service', 'ea00': 'Freedesktop $BOOT', 'eb00': 'Haiku BFS', 'ed00': 'Sony system partition', 'ed01': 'Lenovo system partition', 'ef00': 'EFI System', 'ef01': 'MBR partition scheme', 'ef02': 'BIOS boot partition', 'f800': 'Ceph OSD', 'f801': 'Ceph dm-crypt OSD', 'f802': 'Ceph journal', 'f803': 'Ceph dm-crypt journal', 'f804': 'Ceph disk in creation', 'f805': 'Ceph dm-crypt disk in creation', 'fb00': 'VMWare VMFS', 'fb01': 'VMWare reserved', 'fc00': 'VMWare kcore crash protection', 'fd00': 'Linux RAID'} - # We want to build a mapping of commands to run after partitioning. This will be fleshed out in the future to hopefully include more. - formatting = {} - # TODO: we might want to provide a way to let users specify extra options here. - # TODO: label support? - formatting['ef00'] = ['mkfs.vfat', '-F', '32', '%PART%'] - formatting['ef01'] = formatting['ef00'] - formatting['ef02'] = formatting['ef00'] - formatting['8200'] = ['mkswap', '-c', '%PART%'] - formatting['8300'] = ['mkfs.ext4', '-c', '-q', '%PART%'] # some people are DEFINITELY not going to be happy about this. we need to figure out a better way to customize this. - for fs in ('8301', '8302', '8303', '8304', '8305', '8306', '8307'): - formatting[fs] = formatting['8300'] - #formatting['8e00'] = FOO # TODO: LVM configuration - #formatting['fd00'] = FOO # TODO: MDADM configuration - cmds = [] - for d in self.disk: - partnums = [int(x) for x in self.disk[d]['parts'].keys()] - partnums.sort() - cmds.append(['sgdisk', '-Z', d]) - if self.disk[d]['fmt'] == 'gpt': - diskfmt = 'gpt' - if len(partnums) >= 129 or partnums[-1] >= 129: - exit('GPT only supports 128 partitions (and partition allocations).') - cmds.append(['sgdisk', '-og', d]) - elif self.disk[d]['fmt'] == 'bios': - diskfmt = 'msdos' - cmds.append(['sgdisk', '-om', d]) - cmds.append(['parted', d, '--script', '-a', 'optimal']) - with open(logfile, 'a') as log: - for c in cmds: - subprocess.call(c, stdout = log, stderr = subprocess.STDOUT) - cmds = [] - disksize = {} - disksize['start'] = subprocess.check_output(['sgdisk', '-F', d]) - disksize['max'] = subprocess.check_output(['sgdisk', '-E', d]) - for p in partnums: - # Need to do some mathz to get the actual sectors if we're using percentages. - for s in ('start', 'stop'): - val = self.disk[d]['parts'][str(p)][s] - if '%' in val: - stripped = val.replace('%', '') - modifier = re.sub('[0-9]+%', '', val) - percent = re.sub('(-|\+)*', '', stripped) - decimal = float(percent) / float(100) - newval = int(float(disksize['max']) * decimal) - if s == 'start': - newval = newval + int(disksize['start']) - self.disk[d]['parts'][str(p)][s] = modifier + str(newval) - if self.disk[d]['fmt'] == 'gpt': - for p in partnums: - size = {} - size['start'] = self.disk[d]['parts'][str(p)]['start'] - size['end'] = self.disk[d]['parts'][str(p)]['stop'] - fstype = self.disk[d]['parts'][str(p)]['fstype'].lower() - if fstype not in fstypes.keys(): - print('Filesystem type {0} is not valid. Must be a code from:\nCODE:FILESYSTEM'.format(fstype)) - for k, v in fstypes.items(): - print(k + ":" + v) - exit() - cmds.append(['sgdisk', - '-n', '{0}:{1}:{2}'.format(str(p), - self.disk[d]['parts'][str(p)]['start'], - self.disk[d]['parts'][str(p)]['stop']), - #'-c', '{0}:"{1}"'.format(str(p), self.disk[d]['parts'][str(p)]['label']), # TODO: add support for partition labels - '-t', '{0}:{1}'.format(str(p), fstype), - d]) - mkformat = formatting[fstype] - for x, y in enumerate(mkformat): - if y == '%PART%': - mkformat[x] = d + str(p) - cmds.append(mkformat) - # TODO: add non-gpt stuff here? - with open(logfile, 'a') as log: - for p in cmds: - subprocess.call(p, stdout = log, stderr = subprocess.STDOUT) - usermntidx = list(self.mount.keys()) - usermntidx.sort() # We want to make sure we do this in order. - for k in usermntidx: - if self.mount[k]['mountpt'] == 'swap': - subprocess.call(['swapon', self.mount[k]['device']], stdout = log, stderr = subprocess.STDOUT) - else: - os.makedirs(self.mount[k]['mountpt'], exist_ok = True) - os.chown(self.mount[k]['mountpt'], 0, 0) - cmd = ['mount'] - if self.mount[k]['fstype']: - cmd.extend(['-t', self.mount[k]['fstype']]) - if self.mount[k]['opts']: - cmd.extend(['-o', self.mount[k]['opts']]) - cmd.extend([self.mount[k]['device'], self.mount[k]['mountpt']]) - subprocess.call(cmd, stdout = log, stderr = subprocess.STDOUT) - return() - - def mounts(self): - mntorder = list(self.mount.keys()) - mntorder.sort() - for m in mntorder: - mnt = self.mount[m] - if mnt['mountpt'].lower() == 'swap': - cmd = ['swapon', mnt['device']] - else: - cmd = ['mount', mnt['device'], mnt['mountpt']] - if mnt['opts']: - cmd.insert(1, '-o {0}'.format(mnt['opts'])) - if mnt['fstype']: - cmd.insert(1, '-t {0}'.format(mnt['fstype'])) -# with open(os.devnull, 'w') as DEVNULL: -# for p in cmd: -# subprocess.call(p, stdout = DEVNULL, stderr = subprocess.STDOUT) - # And we need to add some extra mounts to support a chroot. We also need to know what was mounted before. - with open('/proc/mounts', 'r') as f: - procmounts = f.read() - mountlist = {} - for i in procmounts.splitlines(): - mountlist[i.split()[1]] = i - cmounts = {} - for m in ('chroot', 'resolv', 'proc', 'sys', 'efi', 'dev', 'pts', 'shm', 'run', 'tmp'): - cmounts[m] = None - chrootdir = self.system['chrootpath'] - # chroot (bind mount... onto itself. it's so stupid, i know. see https://bugs.archlinux.org/task/46169) - if chrootdir not in mountlist.keys(): - cmounts['chroot'] = ['mount', '--bind', chrootdir, chrootdir] - # resolv.conf (for DNS resolution in the chroot) - if (chrootdir + '/etc/resolv.conf') not in mountlist.keys(): - cmounts['resolv'] = ['/bin/mount', '--bind', '-o', 'ro', '/etc/resolv.conf', chrootdir + '/etc/resolv.conf'] - # proc - if (chrootdir + '/proc') not in mountlist.keys(): - cmounts['proc'] = ['/bin/mount', '-t', 'proc', '-o', 'nosuid,noexec,nodev', 'proc', chrootdir + '/proc'] - # sys - if (chrootdir + '/sys') not in mountlist.keys(): - cmounts['sys'] = ['/bin/mount', '-t', 'sysfs', '-o', 'nosuid,noexec,nodev,ro', 'sys', chrootdir + '/sys'] - # efi (if it exists on the host) - if '/sys/firmware/efi/efivars' in mountlist.keys(): - if (chrootdir + '/sys/firmware/efi/efivars') not in mountlist.keys(): - cmounts['efi'] = ['/bin/mount', '-t', 'efivarfs', '-o', 'nosuid,noexec,nodev', 'efivarfs', chrootdir + '/sys/firmware/efi/efivars'] - # dev - if (chrootdir + '/dev') not in mountlist.keys(): - cmounts['dev'] = ['/bin/mount', '-t', 'devtmpfs', '-o', 'mode=0755,nosuid', 'udev', chrootdir + '/dev'] - # pts - if (chrootdir + '/dev/pts') not in mountlist.keys(): - cmounts['pts'] = ['/bin/mount', '-t', 'devpts', '-o', 'mode=0620,gid=5,nosuid,noexec', 'devpts', chrootdir + '/dev/pts'] - # shm (if it exists on the host) - if '/dev/shm' in mountlist.keys(): - if (chrootdir + '/dev/shm') not in mountlist.keys(): - cmounts['shm'] = ['/bin/mount', '-t', 'tmpfs', '-o', 'mode=1777,nosuid,nodev', 'shm', chrootdir + '/dev/shm'] - # run (if it exists on the host) - if '/run' in mountlist.keys(): - if (chrootdir + '/run') not in mountlist.keys(): - cmounts['run'] = ['/bin/mount', '-t', 'tmpfs', '-o', 'nosuid,nodev,mode=0755', 'run', chrootdir + '/run'] - # tmp (if it exists on the host) - if '/tmp' in mountlist.keys(): - if (chrootdir + '/tmp') not in mountlist.keys(): - cmounts['tmp'] = ['/bin/mount', '-t', 'tmpfs', '-o', 'mode=1777,strictatime,nodev,nosuid', 'tmp', chrootdir + '/tmp'] - # Because the order of these mountpoints is so ridiculously important, we hardcode it. - # Yeah, python 3.6 has ordered dicts, but do we really want to risk it? - # Okay. So we finally have all the mounts bound. Whew. - return(cmounts) - - def setup(self, mounts = False): - # TODO: could we leverage https://github.com/hartwork/image-bootstrap somehow? I want to keep this close - # to standard Python libs, though, to reduce dependency requirements. - hostscript = [] - chrootcmds = [] - locales = [] - locale = [] - if not mounts: - mounts = self.mounts() - # Get the necessary fstab additions for the guest - chrootfstab = subprocess.check_output(['genfstab', '-U', self.system['chrootpath']]) - # Set up the time, and then kickstart the guest install. - hostscript.append(['timedatectl', 'set-ntp', 'true']) - # Also start haveged if we have it. - try: - with open(os.devnull, 'w') as devnull: - subprocess.call(['haveged'], stderr = devnull) - except: - pass - # Make sure we get the keys, in case we're running from a minimal live env. - hostscript.append(['pacman-key', '--init']) - hostscript.append(['pacman-key', '--populate']) - hostscript.append(['pacstrap', self.system['chrootpath'], 'base']) - # Run the basic host prep - #with open(os.devnull, 'w') as DEVNULL: - with open(logfile, 'a') as log: - for c in hostscript: - subprocess.call(c, stdout = log, stderr = subprocess.STDOUT) - with open('{0}/etc/fstab'.format(self.system['chrootpath']), 'a') as f: - f.write('# Generated by AIF-NG.\n') - f.write(chrootfstab.decode('utf-8')) - with open(logfile, 'a') as log: - for m in ('resolv', 'proc', 'sys', 'efi', 'dev', 'pts', 'shm', 'run', 'tmp'): - if mounts[m]: - subprocess.call(mounts[m], stdout = log, stderr = subprocess.STDOUT) - - # Validating this would be better with pytz, but it's not stdlib. dateutil would also work, but same problem. - # https://stackoverflow.com/questions/15453917/get-all-available-timezones - tzlist = subprocess.check_output(['timedatectl', 'list-timezones']).decode('utf-8').splitlines() - if self.system['timezone'] not in tzlist: - print('WARNING (non-fatal): {0} does not seem to be a valid timezone, but we\'re continuing anyways.'.format(self.system['timezone'])) - tzfile = '{0}/etc/localtime'.format(self.system['chrootpath']) - if os.path.lexists(tzfile): - os.remove(tzfile) - os.symlink('/usr/share/zoneinfo/{0}'.format(self.system['timezone']), tzfile) - # This is an ugly hack. TODO: find a better way of determining if the host is set to UTC in the RTC. maybe the datetime module can do it. - utccheck = subprocess.check_output(['timedatectl', 'status']).decode('utf-8').splitlines() - utccheck = [x.strip(' ') for x in utccheck] - for i, v in enumerate(utccheck): - if v.startswith('RTC in local'): - utcstatus = (v.split(': ')[1]).lower() in ('yes') - break - if utcstatus: - chrootcmds.append(['hwclock', '--systohc']) - # We need to check the locale, and set up locale.gen. - with open('{0}/etc/locale.gen'.format(self.system['chrootpath']), 'r') as f: - localeraw = f.readlines() - for line in localeraw: - if not line.startswith('# '): # Comments, thankfully, have a space between the leading octothorpe and the comment. Locales have no space. - i = line.strip().strip('#') - if i != '': # We also don't want blank entries. Keep it clean, folks. - locales.append(i) - for i in locales: - localelst = i.split() - if localelst[0].lower().startswith(self.system['locale'].lower()): - locale.append(' '.join(localelst).strip()) - for i, v in enumerate(localeraw): - for x in locale: - if v.startswith('#{0}'.format(x)): - localeraw[i] = x + '\n' - with open('{0}/etc/locale.gen'.format(self.system['chrootpath']), 'w') as f: - f.write('# Modified by AIF-NG.\n') - f.write(''.join(localeraw)) - with open('{0}/etc/locale.conf'.format(self.system['chrootpath']), 'a') as f: - f.write('# Added by AIF-NG.\n') - f.write('LANG={0}\n'.format(locale[0].split()[0])) - chrootcmds.append(['locale-gen']) - # Set up the kbd layout. - # Currently there is NO validation on this. TODO. - if self.system['kbd']: - with open('{0}/etc/vconsole.conf'.format(self.system['chrootpath']), 'a') as f: - f.write('# Generated by AIF-NG.\nKEYMAP={0}\n'.format(self.system['kbd'])) - # Set up the hostname. - with open('{0}/etc/hostname'.format(self.system['chrootpath']), 'w') as f: - f.write('# Generated by AIF-NG.\n') - f.write(self.network['hostname'] + '\n') - with open('{0}/etc/hosts'.format(self.system['chrootpath']), 'a') as f: - f.write('# Added by AIF-NG.\n127.0.0.1\t{0}\t{1}\n'.format(self.network['hostname'], - (self.network['hostname']).split('.')[0])) - # Set up networking. - ifaces = [] - # Ideally we'd find a better way to do... all of this. Patches welcome. TODO. - if 'auto' in self.network['ifaces'].keys(): - # Get the default route interface. - for line in subprocess.check_output(['ip', '-oneline', 'route', 'show']).decode('utf-8').splitlines(): - line = line.split() - if line[0] == 'default': - autoiface = line[4] - break - ifaces = list(self.network['ifaces'].keys()) - ifaces.sort() - if autoiface in ifaces: - ifaces.remove(autoiface) - for iface in ifaces: - resolvers = False - if 'resolvers' in self.network['ifaces'][iface].keys(): - resolvers = self.network['ifaces'][iface]['resolvers'] - if iface == 'auto': - ifacedev = autoiface - iftype = 'dhcp' - else: - ifacedev = iface - iftype = 'static' - netprofile = 'Description=\'A basic {0} ethernet connection ({1})\'\nInterface={1}\nConnection=ethernet\n'.format(iftype, ifacedev) - if 'ipv4' in self.network['ifaces'][iface].keys(): - if self.network['ifaces'][iface]['ipv4']: - netprofile += 'IP={0}\n'.format(iftype) - if 'ipv6' in self.network['ifaces'][iface].keys(): - if self.network['ifaces'][iface]['ipv6']: - netprofile += 'IP6={0}\n'.format(iftype) # TODO: change this to stateless if iftype='dhcp' instead? - for proto in ('ipv4', 'ipv6'): - addrs = [] - if proto in self.network['ifaces'][iface].keys(): - if proto == 'ipv4': - addr = 'Address' - gwstring = 'Gateway' - elif proto == 'ipv6': - addr = 'Address6' - gwstring = 'Gateway6' - gw = self.network['ifaces'][iface][proto]['gw'] - for ip in self.network['ifaces'][iface][proto]['addresses']: - if ip == 'auto': - continue - else: - try: - ipver = ipaddress.ip_network(ip, strict = False) - addrs.append(ip) - except ValueError: - exit('{0} was specified but is NOT a valid IPv4/IPv6 address!'.format(ip)) - if iftype == 'static': - # Static addresses - netprofile += '{0}=(\'{1}\')\n'.format(addr, ('\' \'').join(addrs)) - # Gateway - if gw: - netprofile += '{0}={1}\n'.format(gwstring, gw) - # DNS resolvers - if resolvers: - netprofile += 'DNS=(\'{0}\')\n'.format('\' \''.join(resolvers)) - filename = '{0}/etc/netctl/{1}'.format(self.system['chrootpath'], ifacedev) - sysdfile = '{0}/etc/systemd/system/netctl@{1}.service'.format(self.system['chrootpath'], ifacedev) - # The good news is since it's a clean install, we only have to account for our own data, not pre-existing. - with open(filename, 'w') as f: - f.write('# Generated by AIF-NG.\n') - f.write(netprofile) - with open(sysdfile, 'w') as f: - f.write('# Generated by AIF-NG.\n') - f.write(('.include /usr/lib/systemd/system/netctl@.service\n\n[Unit]\n' + - 'Description=A basic {0} ethernet connection\n' + - 'BindsTo=sys-subsystem-net-devices-{1}.device\n' + - 'After=sys-subsystem-net-devices-{1}.device\n').format(iftype, ifacedev)) - os.symlink('/etc/systemd/system/netctl@{0}.service'.format(ifacedev), - '{0}/etc/systemd/system/multi-user.target.wants/netctl@{1}.service'.format(self.system['chrootpath'], ifacedev)) - os.symlink('/usr/lib/systemd/system/netctl.service', - '{0}/etc/systemd/system/multi-user.target.wants/netctl.service'.format(self.system['chrootpath'])) - # Root password - if self.users['root']['password']: - roothash = self.users['root']['password'] - else: - roothash = '!' - with fileinput.input('{0}/etc/shadow'.format(self.system['chrootpath']), inplace = True) as f: - for line in f: - linelst = line.split(':') - if linelst[0] == 'root': - linelst[1] = roothash - print(':'.join(linelst), end = '') - # Add users - for user in self.users.keys(): - # We already handled root user - if user != 'root': - cmd = ['useradd'] - if self.users[user]['home']['create']: - cmd.append('-m') - if self.users[user]['home']['path']: - cmd.append('-d {0}'.format(self.users[user]['home']['path'])) - if self.users[user]['comment']: - cmd.append('-c "{0}"'.format(self.users[user]['comment'])) - if self.users[user]['gid']: - cmd.append('-g {0}'.format(self.users[user]['gid'])) - if self.users[user]['uid']: - cmd.append('-u {0}'.format(self.users[user]['uid'])) - if self.users[user]['password']: - cmd.append('-p "{0}"'.format(self.users[user]['password'])) - cmd.append(user) - chrootcmds.append(cmd) - # Add groups - if self.users[user]['xgroup']: - for group in self.users[user]['xgroup'].keys(): - gcmd = False - if self.users[user]['xgroup'][group]['create']: - gcmd = ['groupadd'] - if self.users[user]['xgroup'][group]['gid']: - gcmd.append('-g {0}'.format(self.users[user]['xgroup'][group]['gid'])) - gcmd.append(group) - chrootcmds.append(gcmd) - chrootcmds.append(['usermod', '-aG', '{0}'.format(','.join(self.users[user]['xgroup'].keys())), user]) - # Handle sudo - if self.users[user]['sudo']: - os.makedirs('{0}/etc/sudoers.d'.format(self.system['chrootpath']), exist_ok = True) - os.chmod('{0}/etc/sudoers.d'.format(self.system['chrootpath']), 0o750) - with open('{0}/etc/sudoers.d/{1}'.format(self.system['chrootpath'], user), 'w') as f: - f.write('# Generated by AIF-NG.\nDefaults:{0} !lecture\n{0} ALL=(ALL) ALL\n'.format(user)) - # Base configuration- initcpio, etc. - chrootcmds.append(['mkinitcpio', '-p', 'linux']) - return(chrootcmds) - - def bootloader(self): - # Bootloader configuration - btldr = self.system['bootloader']['type'] - bootcmds = [] - chrootpath = self.system['chrootpath'] - bttarget = self.system['bootloader']['target'] - if btldr == 'grub': - bootcmds.append(['pacman', '--needed', '--noconfirm', '-S', 'grub', 'efibootmgr']) - bootcmds.append(['grub-install']) - if self.system['bootloader']['efi']: - bootcmds[1].extend(['--target=x86_64-efi', '--efi-directory={0}'.format(bttarget), '--bootloader-id=Arch']) - else: - bootcmds[1].extend(['--target=i386-pc', bttarget]) - bootcmds.append(['grub-mkconfig', '-o', '{0}/grub/grub.cfg'.format(bttarget)]) - elif btldr == 'systemd': - if self.system['bootloader']['target'] != '/boot': - shutil.copy2('{0}/boot/vmlinuz-linux'.format(chrootpath), - '{0}/{1}/vmlinuz-linux'.format(chrootpath, bttarget)) - shutil.copy2('{0}/boot/initramfs-linux.img'.format(chrootpath), - '{0}/{1}/initramfs-linux.img'.format(chrootpath, bttarget)) - with open('{0}/{1}/loader/loader.conf'.format(chrootpath, bttarget), 'w') as f: - f.write('# Generated by AIF-NG.\ndefault arch\ntimeout 4\neditor 0\n') - # Gorram, I wish there was a better way to get the partition UUID in stdlib. - majmindev = os.lstat('{0}/{1}'.format(chrootpath, bttarget)).st_dev - majdev = os.major(majmindev) - mindev = os.minor(majmindev) - btdev = os.path.basename(os.readlink('/sys/dev/block/{0}:{1}'.format(majdev, mindev))) - partuuid = False - for d in os.listdir('/dev/disk/by-uuid'): - linktarget = os.path.basename(os.readlink(d)) - if linktarget == btdev: - partuuid = linktarget - break - if not partuuid: - exit('ERROR: Cannot determine PARTUUID for /dev/{0}.'.format(btdev)) - with open('{0}/{1}/loader/entries/arch.conf'.format(chrootpath, bttarget)) as f: - f.write(('# Generated by AIF-NG.\ntitle\t\tArch Linux\nlinux /vmlinuz-linux\n') + - ('initrd /initramfs-linux.img\noptions root=PARTUUID={0} rw\n').format(partuuid)) - bootcmds.append(['bootctl', '--path={0}', 'install']) - # TODO: Add a bit here to alter EFI boot order so we boot right to the newly-installed env. - # should probably be optional. - return(bootcmds) - - def scriptcmds(self, scripttype): - t = scripttype - if t in self.scripts.keys(): - for i, s in enumerate(self.scripts[t]): - dirpath = '/root/scripts/{0}'.format(t) - os.makedirs(dirpath, exist_ok = True) - filepath = '{0}/{1}'.format(dirpath, i) - with open(filepath, 'w') as f: - f.write(s) - os.chmod(filepath, 0o700) - os.chown(filepath, 0, 0) # shouldn't be necessary, but just in case the umask's messed up or something. - if t in ('pre', 'pkg'): - # We want to run these right away. - with open(logfile, 'a') as log: - for i, s in enumerate(self.scripts[t]): - subprocess.call('/root/scripts/{0}/{1}'.format(t, i), - stdout = log, - stderr = subprocess.STDOUT) - return() - - def pacmanSetup(self): - # This should be run outside the chroot. - conf = '{0}/etc/pacman.conf'.format(self.system['chrootpath']) - with open(conf, 'r') as f: - confdata = f.readlines() - # This... is not 100% sane, and we need to change it if the pacman.conf upstream changes order of the default repos. - # Here be dragons; you have been warned. TODO. - idx = confdata.index('#[testing]\n') - shutil.copy2(conf, '{0}.arch'.format(conf)) - newconf = confdata[:idx] - newconf.append('# Modified by AIF-NG.\n') - for r in self.software['repos']: - if self.software['repos'][r]['mirror'].startswith('file://'): - mirror = 'Include = {0}'.format(re.sub('^file://', '', self.software['repos'][r]['mirror'])) - else: - mirror = 'Server = {0}'.format(self.software['repos'][r]['mirror']) - newentry = ['[{0}]\n'.format(r), '{0}\n'.format(mirror)] - if self.software['repos'][r]['siglevel'] != 'default': - newentry.append('Siglevel = {0}\n'.format(self.software['repos'][r]['siglevel'])) - if self.software['repos'][r]['enabled']: - pass # I know, shame on me. We want this because we explicitly want it to be set as True - else: - newentry = ["#" + i for i in newentry] - newentry.append('\n') - newconf.extend(newentry) - with open(conf, 'w') as f: - f.write(''.join(newconf)) - if self.software['mirrors']: - mirrorlst = '{0}/etc/pacman.d/mirrorlist'.format(self.system['chrootpath']) - shutil.copy2(mirrorlst, '{0}.arch'.format(mirrorlst)) - # TODO: file vs. server? - with open(mirrorlst, 'w') as f: - for m in self.software['mirrors']: - if m.startswith('file://'): - mirror = 'Include = {0}'.format(re.sub('^file://', '', m)) - else: - mirror = 'Server = {0}'.format(m) - f.write('{0}\n'.format(mirror)) - return() - - def packagecmds(self): - pkgcmds = [] - # This should be run in the chroot, unless we find a way to pacstrap - # packages separate from chrooting - if self.software['command']: - pkgr = shlex.split(self.software['command']) - else: - pkgr = ['pacman', '--needed', '--noconfirm', '-S'] - if self.software['packages']: - for p in self.software['packages'].keys(): - if self.software['packages'][p]['repo']: - pkgname = '{0}/{1}'.format(self.software['packages'][p]['repo'], p) - else: - pkgname = p - pkgr.append(pkgname) - pkgcmds.append(pkgr) - return(pkgcmds) - - def serviceSetup(self): - # this runs inside the chroot - for s in self.system['services'].keys(): - if not re.match('\.(service|socket|target|timer)$', s): # i don't bother with .path, .busname, etc.- i might in the future? TODO. - svcname = '{0}.service'.format(s) - service = '/usr/lib/systemd/system/{0}'.format(svcname) - sysdunit = '/etc/systemd/system/multi-user.target.wants/{0}'.format(svcname) - if self.system['services'][s]: - if not os.path.lexists(sysdunit): - os.symlink(service, sysdunit) - else: - if os.path.lexists(sysdunit): - os.remove(sysdunit) - return() - - def chroot(self, chrootcmds = False, bootcmds = False, scriptcmds = False, pkgcmds = False): - if not chrootcmds: - chrootcmds = self.setup() - if not bootcmds: - bootcmds = self.bootloader() - if not scriptcmds: - scripts = self.scripts - if not pkgcmds: - pkgcmds = self.packagecmds() - # Switch in the log, and link. - os.rename(logfile, '{0}/{1}'.format(self.system['chrootpath'], logfile)) - os.symlink('{0}/{1}'.format(self.system['chrootpath'], logfile), logfile) - self.pacmanSetup() # This needs to be done before the chroot - # We don't need this currently, but we might down the road. - #chrootscript = '#!/bin/bash\n# https://aif.square-r00t.net/\n\n' - #with open('{0}/root/aif.sh'.format(self.system['chrootpath']), 'w') as f: - # f.write(chrootscript) - #os.chmod('{0}/root/aif.sh'.format(self.system['chrootpath']), 0o700) - real_root = os.open("/", os.O_RDONLY) - os.chroot(self.system['chrootpath']) - # Does this even work with an os.chroot()? Let's hope so! - with open(logfile, 'a') as log: - for c in chrootcmds: - subprocess.call(c, stdout = log, stderr = subprocess.STDOUT) - if scripts['pkg']: - self.scriptcmds('pkg') - for i, s in enumerate(scripts['pkg']): - subprocess.call('/root/scripts/pkg/{0}'.format(i), - stdout = log, - stderr = subprocess.STDOUT) - for p in pkgcmds: - subprocess.call(p, stdout = log, stderr = subprocess.STDOUT) - for b in bootcmds: - subprocess.call(b, stdout = log, stderr = subprocess.STDOUT) - if scripts['post']: - self.scriptcmds('post') - for i, s in enumerate(scripts['post']): - subprocess.call('/root/scripts/post/{0}'.format(i), - stdout = log, - stderr = subprocess.STDOUT) - self.serviceSetup() - #os.system('{0}/root/aif-pre.sh'.format(self.system['chrootpath'])) - #os.system('{0}/root/aif-post.sh'.format(self.system['chrootpath'])) - os.fchdir(real_root) - os.chroot('.') - os.close(real_root) - if not os.path.isfile('{0}/sbin/init'.format(self.system['chrootpath'])): - os.symlink('../lib/systemd/systemd', '{0}/sbin/init'.format(self.system['chrootpath'])) - return() - - def unmount(self): - with open(logfile, 'a') as log: - subprocess.call(['umount', '-lR', self.system['chrootpath']], stdout = log, stderr = subprocess.STDOUT) - # We should also remove the (now dead) log symlink. - #Note that this does NOT delete the logfile on the installed system. - os.remove(logfile) - return() - -def runInstall(confdict): - install = archInstall(confdict) - install.scriptcmds('pre') - install.format() - install.chroot() - install.unmount() - return() - -def main(): - if os.getuid() != 0: - exit('This must be run as root.') - conf = aif() - instconf = conf.buildDict() - if 'DEBUG' in os.environ.keys(): - import pprint - with open(logfile, 'a') as log: - pprint.pprint(instconf, stream = log) - runInstall(instconf) - if instconf['system']['reboot']: - subprocess.run(['reboot']) - -if __name__ == "__main__": - main() diff --git a/docs/TODO b/docs/TODO index bf0af76..6c766e5 100644 --- a/docs/TODO +++ b/docs/TODO @@ -3,7 +3,8 @@ - config layout -- need to apply defaults and annotate/document --- is this necessary since i doc with asciidoctor now? -- how to support mdadm, lvm? +- how to support mdadm, lvm, LUKS FDE? +-- cryptsetup support- use new child type, "cryptPart", under storage/disk/ and new mount attrib, "isLUKS"? - support serverside "autoconfig"- a mechanism to let servers automatically generate xml build configs. e.g.: kernel ... aif_url="https://build.domain.tld/aif-ng.php" auto=yes would yield the *client* sending info via URL params (actually, this might be better as a JSON POST, since we already have a way to generate JSON. sort of.), @@ -11,6 +12,7 @@ or something like that. - parser: make sure to use https://mikeknoop.com/lxml-xxe-exploit/ fix - convert use of confobj or whatever to maybe be suitable to use webFetch instead. LOTS of duplicated code there. +- support XInclude - can i install packages the way pacstrap does, without a chroot? i still need to do it, unfortunately, for setting up efibootmgr etc. but..: pacman -r /mnt/aif -Sy base --cachedir=/mnt/aif/var/cache/pacman/pkg --noconfirm /dev/sda2 on /mnt/aif type ext4 (rw,relatime,data=ordered) @@ -28,10 +30,11 @@ DOCUMENTATION: aif-config.py (and note sample json as well) for network configuration, add in support for using a device's MAC address instead of interface name -also create: +also: -create boot media with bdisk since default arch doesn't even have python 3 -- this is.. sort of? done. but iPXE/mini build is failing, need to investigate why -- i tihnk i fixed iPXE but i need to generate another one once 1.5 is released +-- PENDING BDISK REWRITE docs: http://lxml.de/parsing.html https://www.w3.org/2001/XMLSchema.xsd diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..ce63f76 --- /dev/null +++ b/setup.py @@ -0,0 +1,35 @@ +import setuptools + +with open('README', 'r') as fh: + long_description = fh.read() + +setuptools.setup( + name = 'aif', + version = '0.2.0', + author = 'Brent S.', + author_email = 'bts@square-r00t.net', + description = 'Arch Installation Framework (Next Generation)', + long_description = long_description, + long_description_content_type = 'text/plain', + url = 'https://aif-ng.io', + packages = setuptools.find_packages(), + classifiers = ['Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3 :: Only', + 'Operating System :: POSIX :: Linux', + 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)', + 'Intended Audience :: Developers', + 'Intended Audience :: Information Technology', + 'Topic :: Software Development :: Build Tools', + 'Topic :: Software Development :: Testing', + 'Topic :: System :: Installation/Setup', + 'Topic :: System :: Recovery Tools'], + python_requires = '>=3.6', + project_urls = {'Documentation': 'https://aif-ng.io/', + 'Source': 'https://git.square-r00t.net/AIF-NG/', + 'Tracker': 'https://bugs.square-r00t.net/index.php?project=9'}, + install_requires = ['gpg', 'requests', 'lxml', 'psutil', 'pyparted', 'pytz', 'passlib', 'validators'] + )