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']
+ )