├── .gitignore ├── .travis.yml ├── COPYING ├── Makefile.am ├── README ├── autogen.sh ├── autoinstall.expect ├── boot-canary.service ├── boot-canary.sh ├── check-canary.sh ├── cloud-config.json ├── configure.ac ├── container-config.json ├── copy_files.sh ├── create_pxe.sh ├── dnf-post-update.sh ├── encryption.expect ├── etc.conf ├── full-good.json ├── good-ister.conf ├── install-canary.sh ├── installation-image-post-update-version.py ├── installer-config-vm.json ├── installer-config.json ├── installer-design ├── ister-encryption.service ├── ister-expect.service ├── ister-manexpect.service ├── ister-provision.service ├── ister-test.service ├── ister.conf ├── ister.json ├── ister.py ├── ister.service ├── ister_gui.py ├── ister_test.py ├── key ├── key.pub ├── live-config.json ├── live-image-post-update-version.py ├── maninstall.expect ├── mbr.json ├── min-good.json ├── post-chroot.sh ├── post-encryption.expect ├── post-non-chroot.sh ├── pre-post.json ├── provision-config.json ├── provision-image-post-update-version.py ├── release-image-config.json ├── requirements.txt ├── script.exp ├── spinup.sh ├── test.json ├── update_gui_expect.sh ├── update_installer.sh ├── update_usb.sh ├── usr.conf ├── validate_release.py └── vm-installation-image-post-update-version.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.img 2 | *.pyc 3 | *.xz 4 | .*.swp 5 | Makefile 6 | Makefile.in 7 | __pycache__/ 8 | aclocal.m4 9 | autom4te.cache/ 10 | config.log 11 | config.status 12 | configure 13 | initrd 14 | install-sh 15 | installenv/ 16 | missing 17 | org.clearlinux.native* 18 | repos/ 19 | test/ 20 | test-log 21 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.6" 4 | # command to install dependencies 5 | install: "pip install -r requirements.txt" 6 | # command to run tests 7 | script: python3 ister_test.py 8 | 9 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /Makefile.am: -------------------------------------------------------------------------------- 1 | systemdsystemunitdir = @SYSTEMD_SYSTEMUNITDIR@ 2 | dist_systemdsystemunit_DATA = ister.service ister-provision.service 3 | 4 | statelessdir = $(datarootdir)/defaults/$(PACKAGE) 5 | dist_stateless_DATA = ister.conf ister.json release-image-config.json 6 | 7 | dist_bin_SCRIPTS = ister.py ister_gui.py 8 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | ister is a template based installer for linux 2 | 3 | Ister aims to be pylint and pep8 clean. New code should verify both pass without 4 | exception before being submitted. If a new exception makes sense it can be added 5 | on a case by case basis. 6 | 7 | Testing is supported through ister_test.py. 8 | 9 | Currently requires netifaces and python3. 10 | -------------------------------------------------------------------------------- /autogen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | autoreconf --force --install --symlink --warnings=all 6 | 7 | args="\ 8 | --prefix=/usr" 9 | 10 | ./configure $args "$@" 11 | make clean 12 | -------------------------------------------------------------------------------- /autoinstall.expect: -------------------------------------------------------------------------------- 1 | #!/usr/bin/expect -f 2 | # 3 | # This Expect script was generated by autoexpect on Wed Feb 3 18:49:12 2016 4 | # Expect and autoexpect were both written by Don Libes, NIST. 5 | # 6 | # Note that autoexpect does not guarantee a working script. It 7 | # necessarily has to guess about certain things. Two reasons a script 8 | # might fail are: 9 | # 10 | # 1) timing - A surprising number of programs (rn, ksh, zsh, telnet, 11 | # etc.) and devices discard or ignore keystrokes that arrive "too 12 | # quickly" after prompts. If you find your new script hanging up at 13 | # one spot, try adding a short sleep just before the previous send. 14 | # Setting "force_conservative" to 1 (see below) makes Expect do this 15 | # automatically - pausing briefly before sending each character. This 16 | # pacifies every program I know of. The -c flag makes the script do 17 | # this in the first place. The -C flag allows you to define a 18 | # character to toggle this mode off and on. 19 | 20 | set force_conservative 1 ;# set to 1 to force conservative mode even if 21 | ;# script wasn't run conservatively originally 22 | if {$force_conservative} { 23 | set send_slow {1 .1} 24 | proc send {ignore arg} { 25 | sleep .1 26 | exp_send -s -- $arg 27 | } 28 | } 29 | 30 | # 31 | # 2) differing output - Some programs produce different output each time 32 | # they run. The "date" command is an obvious example. Another is 33 | # ftp, if it produces throughput statistics at the end of a file 34 | # transfer. If this causes a problem, delete these patterns or replace 35 | # them with wildcards. An alternative is to use the -p flag (for 36 | # "prompt") which makes Expect only look for the last line of output 37 | # (i.e., the prompt). The -P flag allows you to define a character to 38 | # toggle this mode off and on. 39 | # 40 | # Read the man page for more info. 41 | # 42 | # -Don 43 | 44 | 45 | set timeout -1 46 | # set env(https_proxy) 47 | spawn /usr/bin/python3 /usr/bin/ister_gui.py --exit-after 48 | match_max 100000 49 | expect -re ".*Clear Linux. OS Installer.*" 50 | send -- "\r" 51 | send -- "\t" 52 | expect -re ".*Network Requirements.*" 53 | send -- "\t" 54 | send -- "\t" 55 | send -- "\t" 56 | send -- "\t" 57 | send -- "\t" 58 | send -- "\t" 59 | send -- "\t" 60 | send -- "\t" 61 | send -- "\t" 62 | send -- "\t" 63 | send -- "\t" 64 | send -- "\t" 65 | send -- "\t" 66 | send -- "\r" 67 | expect -re ".*Choose Action.*" 68 | send -- "\r" 69 | expect -re ".*Stability Enhancement Program.*" 70 | send -- "\r" 71 | send -- "\r" 72 | expect -re ".*Choose Installation Type.*" 73 | send -- "\r" 74 | expect -re ".*Choose target device.*" 75 | send -- "\r" 76 | expect -re ".*Warning.*" 77 | send -- "\t" 78 | send -- "\r" 79 | expect -re ".*Ok.*" 80 | send -- "\r" 81 | expect -re ".*will be rebooted.*" 82 | send -- "\r" 83 | expect eof 84 | -------------------------------------------------------------------------------- /boot-canary.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Boot Validation 3 | 4 | [Service] 5 | Type=oneshot 6 | ExecStart=/usr/bin/bash /usr/bin/boot-canary.sh 7 | ExecStartPost=/usr/sbin/poweroff 8 | 9 | [Install] 10 | WantedBy=multi-user.target 11 | -------------------------------------------------------------------------------- /boot-canary.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | echo 'boot chirp' > /canary 3 | -------------------------------------------------------------------------------- /check-canary.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | 3 | function usage() 4 | { 5 | echo "Usage: $0 " 6 | exit 1 7 | } 8 | 9 | if [ $# -ne 1 ] 10 | then 11 | usage 12 | fi 13 | 14 | if [ $1 == "-h" ] 15 | then 16 | usage 17 | fi 18 | 19 | if [ ! -e $1 ] 20 | then 21 | echo $1: Does not exist 22 | exit 1 23 | fi 24 | 25 | mnt=$(/usr/bin/mktemp -d) 26 | next_dev=$(sudo losetup -f --show -P $1) 27 | sudo /usr/bin/mount ${next_dev}p3 $mnt 28 | if [ -e ${mnt}/canary ] 29 | then 30 | rc=0 31 | else 32 | rc=1 33 | fi 34 | sudo /usr/bin/umount $mnt 35 | sudo /usr/bin/losetup -D 36 | exit $rc 37 | -------------------------------------------------------------------------------- /cloud-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "DestinationType" : "virtual", 3 | "PartitionLayout" : [ { "disk" : "cloud.img", "partition" : 1, "size" : "512M", "type" : "EFI" }, 4 | { "disk" : "cloud.img", "partition" : 2, "size" : "800M", "type" : "linux" } ], 5 | "FilesystemTypes" : [ { "disk" : "cloud.img", "partition" : 1, "type" : "vfat" }, 6 | { "disk" : "cloud.img", "partition" : 2, "type" : "ext4" } ], 7 | "PartitionMountPoints" : [ { "disk" : "cloud.img", "partition" : 1, "mount" : "/boot" }, 8 | { "disk" : "cloud.img", "partition" : 2, "mount" : "/" } ], 9 | "Version": 7340, 10 | "Bundles": ["kernel-kvm", "os-core", "os-core-update", "os-cloudguest", "telemetrics"] 11 | } 12 | -------------------------------------------------------------------------------- /configure.ac: -------------------------------------------------------------------------------- 1 | 2 | AC_PREREQ([2.68]) 3 | AC_INIT([ister],[69],[william.douglas@intel.com],[ister],[https://github.com/bryteise/ister]) 4 | AM_INIT_AUTOMAKE([foreign silent-rules color-tests no-dist-gzip dist-xz]) 5 | AC_CONFIG_FILES([Makefile]) 6 | AC_PREFIX_DEFAULT(/usr/local) 7 | 8 | AM_PATH_PYTHON([3.0]) 9 | 10 | PKG_PROG_PKG_CONFIG 11 | 12 | # Options 13 | AC_ARG_WITH([systemdsystemunitdir], AS_HELP_STRING([--with-systemdsystemunitdir=DIR], 14 | [path to systemd system service directory]), [path_systemdsystemunit=${withval}], 15 | [path_systemdsystemunit="`$PKG_CONFIG --variable=systemdsystemunitdir systemd`"]) 16 | SYSTEMD_SYSTEMUNITDIR="${path_systemdsystemunit}" 17 | AC_SUBST(SYSTEMD_SYSTEMUNITDIR) 18 | AM_CONDITIONAL(SYSTEMD, test -n "${path_systemdsystemunit}") 19 | 20 | AC_OUTPUT 21 | 22 | AC_MSG_RESULT([ 23 | ister $VERSION 24 | ======== 25 | 26 | prefix: ${prefix} 27 | datarootdir: ${datarootdir} 28 | sysconfdir: ${sysconfdir} 29 | exec_prefix: ${exec_prefix} 30 | bindir: ${bindir} 31 | systemdsystemunitdir: ${systemdsystemunitdir} 32 | ]) 33 | -------------------------------------------------------------------------------- /container-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "DestinationType" : "virtual", 3 | "PartitionLayout" : [ { "disk" : "container.img", "partition" : 1, "size" : "224M", "type" : "linux" } ], 4 | "FilesystemTypes" : [ { "disk" : "container.img", "partition" : 1, "type" : "ext4", "options" : "-b 4096" } ], 5 | "PartitionMountPoints" : [ { "disk" : "container.img", "partition" : 1, "mount" : "/" } ], 6 | "Version": 2580, 7 | "Bundles": ["os-core"] 8 | } 9 | -------------------------------------------------------------------------------- /copy_files.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | # usage: update_gui_expect.sh 3 | 4 | function usage() 5 | { 6 | echo "Usage: $0 " 7 | exit 1 8 | } 9 | 10 | if [ $1 == "-h" ] 11 | then 12 | usage 13 | fi 14 | 15 | if [ ! -e $1 ] 16 | then 17 | echo $1: Does not exist 18 | exit 1 19 | fi 20 | 21 | mnt=$(/usr/bin/mktemp -d) 22 | next_dev=$(sudo losetup -f --show -P $1) 23 | sudo /usr/bin/mount ${next_dev}p2 $mnt 24 | sudo cp $2 ${mnt}$3 25 | sudo /usr/bin/umount $mnt 26 | sudo /usr/bin/losetup -D 27 | echo "$1 up to date with latest gui and installer" 28 | -------------------------------------------------------------------------------- /create_pxe.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash -x 2 | 3 | TYPE=provision 4 | TDIR=pxe 5 | MDIR=/tmp/mntpxe 6 | IMG=${TYPE}.img 7 | PXE_NAME=clear-pxe.tar.xz 8 | mkdir -p ${TDIR} 9 | mkdir -p ${MDIR} 10 | #FIXME lets not use a magic number here, this is for a 64M 11 | #offset but we should read it with fdisk or something. 12 | sudo mount -o loop,ro,offset=67108864 ${IMG} ${MDIR} 13 | sudo cp -r ${MDIR}/* ${TDIR}/ 14 | sudo umount ${MDIR} 15 | cd ${TDIR} 16 | sudo rm -rf boot home lib64 lost+found media mnt root srv var 17 | ln -s /usr/lib64/ lib64 18 | ln -s /usr/lib/systemd/systemd init 19 | cp -a lib/kernel/org.clearlinux.native* ../ 20 | sudo find . | cpio -o -H newc | gzip > ../initrd 21 | cd .. 22 | XZ_OPT=-9 tar cJf ${PXE_NAME} initrd org.clearlinux.native* 23 | sudo rm -rf ./${TDIR} ${MDIR} 24 | sudo rm ./initrd ./org.clearlinux.native* 25 | 26 | -------------------------------------------------------------------------------- /dnf-post-update.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | "$1"/usr/bin/update-helper "$1" 3 | "$1"/usr/bin/clr-boot-manager update -p "$1" 4 | -------------------------------------------------------------------------------- /encryption.expect: -------------------------------------------------------------------------------- 1 | #!/usr/bin/expect -f 2 | # 3 | # This Expect script was generated by autoexpect on Wed Feb 3 18:49:12 2016 4 | # Expect and autoexpect were both written by Don Libes, NIST. 5 | # 6 | # Note that autoexpect does not guarantee a working script. It 7 | # necessarily has to guess about certain things. Two reasons a script 8 | # might fail are: 9 | # 10 | # 1) timing - A surprising number of programs (rn, ksh, zsh, telnet, 11 | # etc.) and devices discard or ignore keystrokes that arrive "too 12 | # quickly" after prompts. If you find your new script hanging up at 13 | # one spot, try adding a short sleep just before the previous send. 14 | # Setting "force_conservative" to 1 (see below) makes Expect do this 15 | # automatically - pausing briefly before sending each character. This 16 | # pacifies every program I know of. The -c flag makes the script do 17 | # this in the first place. The -C flag allows you to define a 18 | # character to toggle this mode off and on. 19 | 20 | set force_conservative 1 ;# set to 1 to force conservative mode even if 21 | ;# script wasn't run conservatively originally 22 | if {$force_conservative} { 23 | set send_slow {1 .1} 24 | proc send {ignore arg} { 25 | sleep .1 26 | exp_send -s -- $arg 27 | } 28 | } 29 | 30 | # 31 | # 2) differing output - Some programs produce different output each time 32 | # they run. The "date" command is an obvious example. Another is 33 | # ftp, if it produces throughput statistics at the end of a file 34 | # transfer. If this causes a problem, delete these patterns or replace 35 | # them with wildcards. An alternative is to use the -p flag (for 36 | # "prompt") which makes Expect only look for the last line of output 37 | # (i.e., the prompt). The -P flag allows you to define a character to 38 | # toggle this mode off and on. 39 | # 40 | # Read the man page for more info. 41 | # 42 | # -Don 43 | 44 | 45 | set timeout -1 46 | # set env(https_proxy) 47 | spawn /usr/bin/python3 /usr/bin/ister_gui.py --exit-after 48 | match_max 100000 49 | expect -re ".*Clear Linux. OS Installer.*" 50 | send -- "\r" 51 | send -- "\t" 52 | expect -re ".*Network Requirements.*" 53 | send -- "\t" 54 | send -- "\t" 55 | send -- "\t" 56 | send -- "\t" 57 | send -- "\t" 58 | send -- "\t" 59 | send -- "\t" 60 | send -- "\t" 61 | send -- "\t" 62 | send -- "\t" 63 | send -- "\t" 64 | send -- "\t" 65 | send -- "\t" 66 | send -- "\r" 67 | expect -re ".*Choose Action.*" 68 | send -- "\r" 69 | # check < previous > button functionality 70 | expect -re ".*Stability Enhancement Program.*" 71 | send -- "\t" 72 | send -- "\t" 73 | send -- "\r" 74 | send -- "\r" 75 | expect -re ".*Stability Enhancement Program.*" 76 | send -- "\t" 77 | send -- "\r" 78 | send -- "\r" 79 | expect -re ".*Choose Installation Type.*" 80 | # tab to manual 81 | send -- "\t" 82 | send -- "\r" 83 | expect -re ".*Choose partitioning method.*" 84 | send -- "\t" 85 | send -- "\r" 86 | expect -re ".*Choose a drive to partition.*" 87 | send -- "\r" 88 | expect -re ".*Press any key to continue,*" 89 | send -- "\r" 90 | # New EFI partition 91 | expect -re ".*\[ New \],*" 92 | send -- "\r" 93 | send -- "\r" 94 | send -- "500M\r" 95 | send -- "ef00\r" 96 | send -- "EFI\r" 97 | send -- "\033\[B" 98 | send -- "\033\[B" 99 | # New swap partition 100 | expect -re ".*\[ New \],*" 101 | send -- "\r" 102 | send -- "\r" 103 | send -- "3G\r" 104 | send -- "8200\r" 105 | send -- "swap\r" 106 | send -- "\033\[B" 107 | # New root partition 108 | expect -re ".*\[ New \],*" 109 | send -- "\r" 110 | send -- "\r" 111 | send -- "\r" 112 | send -- "\r" 113 | send -- "Linux System\r" 114 | send -- "\033\[C" 115 | send -- "\033\[C" 116 | send -- "\r" 117 | send -- "yes\r" 118 | send -- "\033\[D" 119 | # quit partition manager 120 | send -- "\r" 121 | expect -re ".*Choose a drive to partition.*" 122 | send -- "\t" 123 | send -- "\t" 124 | send -- "\r" 125 | expect -re ".*Set mount points.*" 126 | send -- "\r" 127 | send -- "/boot\r" 128 | send -- "\r" 129 | send -- "\t" 130 | send -- "\r" 131 | expect -re ".*Set mount points.*" 132 | send -- "\t" 133 | send -- "\t" 134 | send -- "\r" 135 | send -- "/\r" 136 | send -- "\r" 137 | send -- "\t" 138 | send -- "\r" 139 | expect -re ".*Set mount points.*" 140 | send -- "\t" 141 | send -- "\t" 142 | send -- "\t" 143 | # root encryption checkbox 144 | send -- "\r" 145 | send -- "\t" 146 | send -- "\r" 147 | expect -re ".*Type passphrase.*" 148 | send -- "123" 149 | send -- "\t" 150 | send -- "\r" 151 | send -- "123" 152 | send -- "\t" 153 | send -- "\r" 154 | expect -re ".*Append to kernel cmdline.*" 155 | send -- "\t" 156 | send -- "\r" 157 | expect -re ".*Configuring Hostname.*" 158 | # accept default 159 | send -- "\r" 160 | # Next 161 | send -- "\r" 162 | expect -re ".*User configuration.*" 163 | # manually create a user 164 | send -- "\r" 165 | send -- "User\r" 166 | send -- "Name\r" 167 | # Username is now uname 168 | send -- "\t" 169 | send -- "UserPass\r" 170 | send -- "UserPass\r" 171 | # Add user to sudoers 172 | send -- "\r" 173 | send -- "\t" 174 | send -- "\r" 175 | expect -re ".*Bundle selector.*" 176 | # editors 177 | send -- "\r" 178 | # tab to Next 179 | send -- "\t" 180 | send -- "\t" 181 | send -- "\t" 182 | send -- "\t" 183 | send -- "\t" 184 | send -- "\t" 185 | # Next 186 | send -- "\r" 187 | expect -re ".*Network configuration.*" 188 | send -- "\t" 189 | # Static IP configuration 190 | send -- "\r" 191 | expect -re ".*Step 12 of 13.*" 192 | # tab through options, don't actually set anything 193 | send -- "\t" 194 | send -- "\t" 195 | send -- "\t" 196 | send -- "\t" 197 | send -- "\t" 198 | send -- "\t" 199 | send -- "\t" 200 | send -- "\t" 201 | # Previous 202 | send -- "\r" 203 | expect -re ".*Step 12 of 13.*" 204 | send -- "\t" 205 | send -- "\t" 206 | # Use DHCP 207 | send -- "\r" 208 | expect -re ".*Attention.*" 209 | send -- "\t" 210 | # Yes 211 | send -- "\r" 212 | expect -re ".*Ok.*" 213 | send -- "\r" 214 | expect -re ".*will be rebooted.*" 215 | send -- "\r" 216 | expect eof 217 | -------------------------------------------------------------------------------- /etc.conf: -------------------------------------------------------------------------------- 1 | template=file:///etc.json 2 | -------------------------------------------------------------------------------- /full-good.json: -------------------------------------------------------------------------------- 1 | { 2 | "DestinationType" : "virtual", 3 | "PartitionLayout" : [ { "disk" : "test.img", "partition" : 1, "size" : "512M", "type" : "EFI" }, 4 | { "disk" : "test.img", "partition" : 2, "size" : "1G", "type" : "linux" } ], 5 | "FilesystemTypes" : [ { "disk" : "test.img", "partition" : 1, "type" : "vfat" }, 6 | { "disk" : "test.img", "partition" : 2, "type" : "ext4" } ], 7 | "PartitionMountPoints" : [ { "disk" : "test.img", "partition" : 1, "mount" : "/boot" }, 8 | { "disk" : "test.img", "partition" : 2, "mount" : "/" } ], 9 | "Users" : [ { "username" : "test", "key" : "key.pub", "uid" : 1000, "sudo" : true } ], 10 | "Version": 930, 11 | "Bundles": ["kernel-kvm"] 12 | } 13 | -------------------------------------------------------------------------------- /good-ister.conf: -------------------------------------------------------------------------------- 1 | template=file:///tmp/template.json 2 | -------------------------------------------------------------------------------- /install-canary.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | 3 | function usage() 4 | { 5 | echo "Usage: $0 " 6 | exit 1 7 | } 8 | 9 | if [ $# -ne 1 ] 10 | then 11 | usage 12 | fi 13 | 14 | if [ $1 == "-h" ] 15 | then 16 | usage 17 | fi 18 | 19 | if [ ! -e $1 ] 20 | then 21 | echo $1: Does not exist 22 | exit 1 23 | fi 24 | 25 | mnt=$(/usr/bin/mktemp -d) 26 | next_dev=$(sudo losetup -f --show -P $1) 27 | sudo /usr/bin/mount ${next_dev}p3 $mnt 28 | 29 | if [ ! -e ${mnt}/usr/bin ] 30 | then 31 | echo "Install to installer-target.img failed" 32 | sudo /usr/bin/losetup -D 33 | exit 1 34 | else 35 | sudo /usr/bin/cp -f boot-canary.sh ${mnt}/usr/bin/boot-canary.sh 36 | sudo /usr/bin/cp -f boot-canary.service ${mnt}/usr/lib/systemd/system/boot-canary.service 37 | sudo /usr/bin/ln -f -s ../boot-canary.service ${mnt}/usr/lib/systemd/system/multi-user.target.wants 38 | sudo /usr/bin/umount $mnt 39 | sudo /usr/bin/losetup -D 40 | echo "Canary script and service installed to $1" 41 | fi 42 | -------------------------------------------------------------------------------- /installation-image-post-update-version.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import os 4 | import sys 5 | 6 | INSTALLER_VERSION = "latest" 7 | 8 | def create_installer_config(path): 9 | """Create a basicl installation configuration file""" 10 | config = u"template=file:///etc/ister.json\n" 11 | jconfig = u'{"DestinationType" : "physical", "PartitionLayout" : \ 12 | [{"disk" : "sda", "partition" : 1, "size" : "512M", "type" : "EFI"}, \ 13 | {"disk" : "sda", "partition" : 2, \ 14 | "size" : "512M", "type" : "swap"}, {"disk" : "sda", "partition" : 3, \ 15 | "size" : "rest", "type" : "linux"}], \ 16 | "FilesystemTypes" : \ 17 | [{"disk" : "sda", "partition" : 1, "type" : "vfat"}, \ 18 | {"disk" : "sda", "partition" : 2, "type" : "swap"}, \ 19 | {"disk" : "sda", "partition" : 3, "type" : "ext4"}], \ 20 | "PartitionMountPoints" : \ 21 | [{"disk" : "sda", "partition" : 1, "mount" : "/boot"}, \ 22 | {"disk" : "sda", "partition" : 3, "mount" : "/"}], \ 23 | "Version" : 0, "Bundles" : ["kernel-native", "telemetrics", "os-core", "os-core-update"]}\n' 24 | if not os.path.isdir("{}/etc".format(path)): 25 | os.mkdir("{}/etc".format(path)) 26 | with open("{}/etc/ister.conf".format(path), "w") as cfile: 27 | cfile.write(config) 28 | with open("{}/etc/ister.json".format(path), "w") as jfile: 29 | jfile.write(jconfig.replace('"Version" : 0', 30 | '"Version" : "{}"'.format(INSTALLER_VERSION))) 31 | 32 | 33 | def append_installer_rootwait(path): 34 | """Add a delay to the installer kernel commandline""" 35 | entry_path = path + "/boot/loader/entries/" 36 | entry_file = os.listdir(entry_path) 37 | if len(entry_file) != 1: 38 | raise Exception("Unable to find specific entry file in {0}, " 39 | "found {1} instead".format(entry_path, entry_file)) 40 | file_full_path = entry_path + entry_file[0] 41 | with open(file_full_path, "r") as entry: 42 | entry_content = entry.readlines() 43 | options_line = entry_content[-1] 44 | if not options_line.startswith("options "): 45 | raise Exception("Last line of entry file is not the kernel " 46 | "commandline options") 47 | # Account for newline at the end of the line 48 | options_line = options_line[:-1] + " rootwait\n" 49 | entry_content[-1] = options_line 50 | os.unlink(file_full_path) 51 | with open(file_full_path, "w") as entry: 52 | entry.writelines(entry_content) 53 | 54 | 55 | def append_installer_no_kms(path): 56 | """Disable KMS on the installer kernel commandline""" 57 | entry_path = path + "/boot/loader/entries/" 58 | entry_file = os.listdir(entry_path) 59 | if len(entry_file) != 1: 60 | raise Exception("Unable to find specific entry file in {0}, " 61 | "found {1} instead".format(entry_path, entry_file)) 62 | file_full_path = entry_path + entry_file[0] 63 | with open(file_full_path, "r") as entry: 64 | entry_content = entry.readlines() 65 | options_line = entry_content[-1] 66 | if not options_line.startswith("options "): 67 | raise Exception("Last line of entry file is not the kernel " 68 | "commandline options") 69 | # Account for newline at the end of the line 70 | options_line = options_line[:-1] + " nomodeset i915.modeset=0\n" 71 | entry_content[-1] = options_line 72 | os.unlink(file_full_path) 73 | with open(file_full_path, "w") as entry: 74 | entry.writelines(entry_content) 75 | 76 | 77 | def append_systemd_boot_timeout(path): 78 | """Add a systemd-boot timeout to loader.conf""" 79 | loader_path = path + "/boot/loader/loader.conf" 80 | if not os.path.exists(loader_path): 81 | raise Exception("Unable to find loader.conf in {}" 82 | .format(os.path.dirname(loader_path))) 83 | with open(loader_path, 'a') as loadconf: 84 | loadconf.write('timeout 5') 85 | 86 | 87 | def disable_tty1_getty(path): 88 | """Add a symlink masking the systemd tty1 generator""" 89 | os.makedirs(path + "/etc/systemd/system/getty.target.wants") 90 | os.symlink("/dev/null", path + "/etc/systemd/system/getty.target.wants/getty@tty1.service") 91 | 92 | 93 | def add_installer_service(path): 94 | os.symlink("{}/usr/lib/systemd/system/ister.service" 95 | .format(path), 96 | "{}/usr/lib/systemd/system/multi-user.target.wants/ister.service" 97 | .format(path)) 98 | 99 | 100 | if __name__ == '__main__': 101 | if len(sys.argv) != 2: 102 | sys.exit(-1) 103 | 104 | try: 105 | create_installer_config(sys.argv[1]) 106 | append_installer_rootwait(sys.argv[1]) 107 | append_installer_no_kms(sys.argv[1]) 108 | append_systemd_boot_timeout(sys.argv[1]) 109 | disable_tty1_getty(sys.argv[1]) 110 | add_installer_service(sys.argv[1]) 111 | except Exception as exep: 112 | print(exep) 113 | sys.exit(-1) 114 | sys.exit(0) 115 | -------------------------------------------------------------------------------- /installer-config-vm.json: -------------------------------------------------------------------------------- 1 | { 2 | "DestinationType" : "virtual", 3 | "PartitionLayout" : [ { "disk" : "installer-val.img", "partition" : 1, "size" : "64M", "type" : "EFI" }, 4 | { "disk" : "installer-val.img", "partition" : 2, "size" : "8G", "type" : "linux" } ], 5 | "FilesystemTypes" : [ { "disk" : "installer-val.img", "partition" : 1, "type" : "vfat" }, 6 | { "disk" : "installer-val.img", "partition" : 2, "type" : "ext4" } ], 7 | "PartitionMountPoints" : [ { "disk" : "installer-val.img", "partition" : 1, "mount" : "/boot" }, 8 | { "disk" : "installer-val.img", "partition" : 2, "mount" : "/" } ], 9 | "Version": "latest", 10 | "Bundles": ["kernel-native", "os-installer", "os-core-update", "os-core", "os-core-dev", "bootloader"], 11 | "PostNonChroot": ["./vm-installation-image-post-update-version.py"] 12 | } 13 | -------------------------------------------------------------------------------- /installer-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "DestinationType" : "virtual", 3 | "PartitionLayout" : [ { "disk" : "installer.img", "partition" : 1, "size" : "64M", "type" : "EFI" }, 4 | { "disk" : "installer.img", "partition" : 2, "size" : "2G", "type" : "linux" } ], 5 | "FilesystemTypes" : [ { "disk" : "installer.img", "partition" : 1, "type" : "vfat" }, 6 | { "disk" : "installer.img", "partition" : 2, "type" : "ext4" } ], 7 | "PartitionMountPoints" : [ { "disk" : "installer.img", "partition" : 1, "mount" : "/boot" }, 8 | { "disk" : "installer.img", "partition" : 2, "mount" : "/" } ], 9 | "Version": "latest", 10 | "Bundles": ["kernel-native", "os-installer", "os-core-update", "os-core", "bootloader"], 11 | "PostNonChroot": ["./installation-image-post-update-version.py"] 12 | } 13 | -------------------------------------------------------------------------------- /installer-design: -------------------------------------------------------------------------------- 1 | * ister 2 | ** Goals 3 | - Installer will run without user interaction 4 | - Will use a template to allow customization 5 | - Template may be a local or remote file 6 | *** Installer template system 7 | - Template will have the following structure (key: {} object, 8 | [] array, ! optional, || options): 9 | { 10 | DestinationType : |physical, virtual| 11 | PartitionLayout : [ { disk : 'sda', partition : 1, 12 | size : |rest, X|M, G, T|| type : |EFI, linux, swap| }, ... ], 13 | FilesystemTypes : [ { disk : 'sda', partition : 1, 14 | type : |vfat, ext4, btrfs, xfs, swap, ... | 15 | !options : |mkfs options| }, ... ], 16 | PartitionMountPoints : [ { disk : 'sda', partition : 1, 17 | mount : '/' }, ... ], 18 | !Users : [ { username : 'uname', !key : URI, !uid : 1000, 19 | !sudo : |True, False| }, ... ], 20 | Version : version-number, 21 | Bundles : [ 'bundle1', ... ], 22 | !PreSetupShell : [ "shellcmd --and args", ... ], 23 | !PostNonChroot : [ '/path/to/script', ... ], 24 | !PostNonChrootShell : [ "shellcmd --and args", ... ], 25 | !PostChroot : [ '/path/to/script', ... ], 26 | !PostChrootShell : [ 'shellcmd --and args', ... ], 27 | //Future 28 | !RaidSupport : |md lvm btrfs|, 29 | !RaidSetup : [ { raid : 'md-raid0', rdisk : 'md0', rpartitions : 30 | [ sda1, sda2, ... ] }, ... ], 31 | } 32 | - Use json as template format 33 | ** Installer image creation 34 | - Done via bootstrap script using ister 35 | ** Installer programs 36 | - One program that will be started via systemd 37 | - Can be configured to use a local or remote install template 38 | - Installer will parse and validate template, download and validate 39 | source file if needed, either use the template for partitioning 40 | and filesystem creation as well as mount point locations or 41 | identify the first non installer disk and use a default 42 | partition scheme and create filesystems on the disk, and then 43 | install the os with software update 44 | - partitions will be identified by UUID and used in gummiboot and 45 | fstab configuration files 46 | *** Installer dependencies 47 | - python3 (installer runtime) 48 | - parted (partition creation) 49 | - e2fsprogs (filesystem creation) 50 | - gummiboot (bootloader installation) 51 | - dosfstools (filesystem creation) 52 | - btrfs-progs (filesystem creation) 53 | - xfsprogs (filesystem creation) 54 | - util-linux (UUID verification, loop device management) 55 | - qemu efi bios (testing) 56 | - qemu-img (image file creation) 57 | - partprobe (detect partitions) 58 | - systemd (setting machine-id) 59 | - swupd (install os) 60 | - gptfdisk (set partition attributes) 61 | - shadow (user additions) 62 | -------------------------------------------------------------------------------- /ister-encryption.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=ister graphical installer 3 | 4 | [Service] 5 | Type=oneshot 6 | ExecStart=/usr/bin/expect /usr/bin/encryption.expect 7 | ExecStartPost=/usr/sbin/poweroff 8 | StandardInput=tty 9 | StandardOutput=tty 10 | StandardError=tty 11 | TTYPath=/dev/tty1 12 | 13 | [Install] 14 | WantedBy=multi-user.target 15 | -------------------------------------------------------------------------------- /ister-expect.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=ister graphical installer 3 | 4 | [Service] 5 | Type=oneshot 6 | ExecStart=/usr/bin/expect /usr/bin/autoinstall.expect 7 | ExecStartPost=/usr/sbin/poweroff 8 | StandardInput=tty 9 | StandardOutput=tty 10 | StandardError=tty 11 | TTYPath=/dev/tty1 12 | 13 | [Install] 14 | WantedBy=multi-user.target 15 | -------------------------------------------------------------------------------- /ister-manexpect.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=ister graphical installer 3 | 4 | [Service] 5 | Type=oneshot 6 | ExecStart=/usr/bin/expect /usr/bin/maninstall.expect 7 | ExecStartPost=/usr/sbin/poweroff 8 | StandardInput=tty 9 | StandardOutput=tty 10 | StandardError=tty 11 | TTYPath=/dev/tty1 12 | 13 | [Install] 14 | WantedBy=multi-user.target 15 | -------------------------------------------------------------------------------- /ister-provision.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=ister graphical installer 3 | 4 | [Service] 5 | Type=oneshot 6 | ExecStart=/bin/sh -c '/usr/bin/python3 /usr/bin/ister.py && /usr/sbin/reboot' 7 | 8 | [Install] 9 | WantedBy=multi-user.target 10 | -------------------------------------------------------------------------------- /ister-test.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=ister test 3 | After=network-online.target 4 | Requires=network-online.target 5 | 6 | [Service] 7 | Type=oneshot 8 | ExecStart=/usr/bin/python3 /root/ister_test.py 9 | ExecStartPost=/usr/sbin/poweroff 10 | 11 | [Install] 12 | WantedBy=multi-user.target 13 | -------------------------------------------------------------------------------- /ister.conf: -------------------------------------------------------------------------------- 1 | template=file:///usr/share/defaults/ister/ister.json 2 | -------------------------------------------------------------------------------- /ister.json: -------------------------------------------------------------------------------- 1 | { 2 | "DestinationType": "physical", 3 | "PartitionLayout": [{"disk": "sdb", "partition": 1, "size": "512M", "type": "EFI"}, 4 | {"disk": "sdb", "partition": 2, "size": "4G", "type": "swap"}, 5 | {"disk": "sdb", "partition": 3, "size": "rest", "type": "linux"}], 6 | "FilesystemTypes": [{"disk": "sdb", "partition": 1, "type": "vfat", "label" : "boot"}, 7 | {"disk": "sdb", "partition": 2, "type": "swap", "label" : "root"}, 8 | {"disk": "sdb", "partition": 3, "type": "ext4", "label" : "swap"}], 9 | "PartitionMountPoints": [{"disk": "sdb", "partition": 1, "mount": "/boot"}, 10 | {"disk": "sdb", "partition": 3, "mount": "/"}], 11 | "Version": 0, 12 | "Bundles": ["kernel-kvm"] 13 | } 14 | -------------------------------------------------------------------------------- /ister.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # vim: ts=4 sw=4 tw=80 et ai si 4 | """Linux installation template system""" 5 | 6 | # 7 | # This file is part of ister. 8 | # 9 | # Copyright (C) 2014 Intel Corporation 10 | # 11 | # ister is free software; you can redistribute it and/or modify 12 | # it under the terms of the GNU General Public License as published by the 13 | # Free Software Foundation; version 3 of the License, or (at your 14 | # option) any later version. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program in a file named COPYING; if not, write to the 18 | # Free Software Foundation, Inc., 19 | # 51 Franklin Street, Fifth Floor, 20 | # Boston, MA 02110-1301 USA 21 | # 22 | 23 | # We aren't splitting ister up just yet so ignore too many lines error 24 | # pylint: disable=too-many-lines 25 | # As much as it pains us, global for the LOG handler is reasonable here 26 | # pylint: disable=global-statement 27 | # If we see an exception it is always fatal so the broad exception 28 | # warning isn't helpful. 29 | # pylint: disable=broad-except 30 | # We aren't using classes for anything other than with handling so 31 | # a warning about too few methods being implemented isn't useful. 32 | # pylint: disable=too-few-public-methods 33 | # Too many branches is probably something we'd have hoped to avoid but this 34 | # logic for partition creation was born to be ugly, good spot for cleanup 35 | # though for the adventurous sort 36 | # pylint: disable=too-many-branches 37 | # We aren't worried too much about performance of ister itself here, so using 38 | # .format() for the logging functions (which always formats the string) is ok. 39 | # pylint: disable=logging-format-interpolation 40 | 41 | 42 | import argparse 43 | import ctypes 44 | import json 45 | import logging 46 | import os 47 | import pwd 48 | import re 49 | import shlex 50 | import shutil 51 | import socket 52 | import stat 53 | import subprocess 54 | import sys 55 | import tempfile 56 | import time 57 | import base64 58 | import binascii 59 | import codecs 60 | import errno 61 | import fcntl 62 | import queue 63 | import select 64 | import threading 65 | import traceback 66 | import urllib.request as request 67 | from urllib.error import URLError, HTTPError 68 | from urllib.parse import urlparse 69 | from contextlib import closing 70 | import netifaces 71 | import pycryptsetup 72 | 73 | LOG = None 74 | 75 | def extract_full_lines(text): 76 | """Extract full lines from string 'text'. Return a tuple containing 2 elements 77 | - list of full lines and a string containing the partial line. 78 | """ 79 | 80 | full, partial = [], "" 81 | for line_match in re.finditer("(.*)\n|(.+$)", text): 82 | if line_match.group(2): 83 | partial = line_match.group(2) 84 | break 85 | full.append(line_match.group(1)) 86 | return (full, partial) 87 | 88 | def stream_fetcher(info, streamid): 89 | """This function runs in a separate thread and fetches data from 'stream', 90 | which is a file associated with the stdout or stderr pipes of a process. 91 | """ 92 | 93 | partial = "" 94 | stream = info["streams"][streamid] 95 | try: 96 | # Set non-blocking mode for the stream. We are doing this because we 97 | # want to regularly supply the consumers with the output data and never 98 | # block for too long. 99 | fno = stream.fileno() 100 | fcntl.fcntl(fno, fcntl.F_SETFL, 101 | fcntl.fcntl(fno, fcntl.F_GETFL) | os.O_NONBLOCK) 102 | decoder = codecs.getincrementaldecoder('utf8')(errors="surrogateescape") 103 | while not info["die_now"]: 104 | # Wait for someting to appear in the stream. Wait for longest 1 105 | # second in order to ensure we exit on "die_now". 106 | if not select.select([stream], [], [], 1)[0]: 107 | continue 108 | data = None 109 | try: 110 | data = stream.read(4096) 111 | except OSError as err: 112 | if err.errno == errno.EAGAIN: 113 | continue 114 | raise 115 | if not data: 116 | break 117 | data = decoder.decode(data) 118 | if not data: 119 | continue 120 | data, partial = extract_full_lines(partial + data) 121 | for line in data: 122 | info["queue"].put((streamid, line)) 123 | except BaseException as err: 124 | LOG.error(err) 125 | 126 | if partial: 127 | info["queue"].put((streamid, partial)) 128 | # "End of data stream" marker. 129 | info["queue"].put((streamid, None)) 130 | 131 | def wait_for_process(proc, log_output, show_output): 132 | """Wait for process 'proc' to finish.""" 133 | 134 | info = {"streams" : (proc.stdout, proc.stderr), 135 | "die_now" : False, 136 | "queue" : queue.Queue()} 137 | 138 | # Start the stream fetcher threads. They will read the output of the process 139 | # and put to the queue. 140 | threads = [] 141 | for streamid in (0, 1): 142 | if info["streams"][streamid]: 143 | threads.append(threading.Thread(target=stream_fetcher, 144 | name='cmd-stream-fetcher', 145 | args=(info, streamid))) 146 | threads[-1].start() 147 | 148 | output = ([], []) 149 | try: 150 | while True: 151 | streamid, line = info["queue"].get() 152 | if line is not None: 153 | output[streamid].append(line) 154 | if show_output: 155 | LOG.info(line) 156 | elif log_output: 157 | LOG.debug(line) 158 | else: 159 | # 'None' means "no more output". 160 | threads[streamid].join() 161 | threads[streamid] = None 162 | if all(thread is None for thread in threads): 163 | break 164 | finally: 165 | # Make sure threads always exit. 166 | info["die_now"] = True 167 | 168 | # The process closed its stdout and stderr and we expect it to terminate 169 | # soon. This should happen right away in a normal situation. 170 | exitcode = proc.wait(timeout=60) 171 | return output[0], output[1], exitcode 172 | 173 | def run_command(cmd, raise_exception=True, log_output=True, environ=None, 174 | show_output=False, shell=False): 175 | """ 176 | Execute given command in a subprocess and return a (stdout, stderr, 177 | exitcode) tuple, where 'stdout' is the standard output of the command, 178 | 'stderr' is the standard error, and 'exitcode' is the exit status. 179 | 180 | This function will raise an Exception if the command fails unless 181 | raise_exception is False. 182 | """ 183 | 184 | result = ([], [], -1) 185 | try: 186 | LOG.debug("Running command {0}".format(cmd)) 187 | sys.stdout.flush() 188 | if shell: 189 | full_cmd = cmd 190 | else: 191 | full_cmd = shlex.split(cmd) 192 | proc = subprocess.Popen(full_cmd, stdout=subprocess.PIPE, 193 | stderr=subprocess.PIPE, env=environ, 194 | shell=shell) 195 | result = wait_for_process(proc, log_output, show_output) 196 | _, stderr, exitcode = result 197 | if exitcode and raise_exception: 198 | if stderr: 199 | LOG.debug("\n".join(stderr)) 200 | raise Exception("{0}".format(cmd)) 201 | except Exception as exep: 202 | if raise_exception: 203 | raise Exception("Error: {0} failed:\n{1}".format(cmd, exep)) 204 | return result 205 | 206 | def validate_network(url): 207 | """Validate there is network connection to swupd 208 | """ 209 | LOG.info("Verifying network connection") 210 | url = url if url else "https://update.clearlinux.org" 211 | try: 212 | _ = request.urlopen(url, timeout=3) 213 | except HTTPError as exep: 214 | if hasattr(exep, 'code'): 215 | LOG.info("SWUPD server error: {0}".format(exep.code)) 216 | raise exep 217 | except URLError as exep: 218 | if hasattr(exep, 'reason'): 219 | LOG.info("Network error: Cannot reach swupd server: {0}" 220 | .format(exep.reason)) 221 | raise exep 222 | 223 | 224 | def create_virtual_disk(template): 225 | """Create virtual disk file for install target 226 | """ 227 | LOG.info("Creating virtual disk") 228 | image_size = 0 229 | # number of kilobytes in each of the following 230 | match = {"M": 1024, "G": 1024 ** 2, "T": 1024 ** 3} 231 | for part in template["PartitionLayout"]: 232 | if part["size"] != "rest": 233 | image_size += int(part["size"][:-1]) * match[part["size"][-1]] 234 | 235 | # Add extra buffer, note disk sizes should be multiples of 4kb. 236 | # Increase buffer by 1MB to give parted wiggle room due to dd using 1K 237 | # sector sizes and parted is getting partition sizes specified in MiB. 238 | image_size += 1024 239 | command = "dd if=/dev/zero of={0} bs=1024 count=0 seek={1}".\ 240 | format(template["PartitionLayout"][0]["disk"], image_size) 241 | run_command(command) 242 | 243 | 244 | def create_partitions(template, sleep_time=1): 245 | """Create partitions according to template configuration 246 | """ 247 | LOG.info("Creating partitions") 248 | match = {"M": 1, "G": 1024, "T": 1024 * 1024} 249 | parted = "parted -sa" 250 | alignment = "optimal" 251 | units = "unit MiB" 252 | disks = set() 253 | cdisk = "" 254 | for disk in template["PartitionLayout"]: 255 | disks.add(disk["disk"]) 256 | # Setup GPT tables on disks 257 | for disk in sorted(disks): 258 | LOG.debug("Creating GPT label in {0}".format(disk)) 259 | if template.get("DestinationType") == "physical": 260 | command = "{0} {1} /dev/{2} {3} mklabel gpt".\ 261 | format(parted, alignment, disk, units) 262 | else: 263 | command = "{0} {1} {2} {3} mklabel gpt".\ 264 | format(parted, alignment, disk, units) 265 | run_command(command) 266 | time.sleep(sleep_time) 267 | # Create partitions 268 | for part in sorted(template["PartitionLayout"], key=lambda v: v["disk"] + 269 | str(v["partition"])): 270 | if part["disk"] != cdisk: 271 | start = 0 272 | if part["size"] == "rest": 273 | end = "-1M" 274 | else: 275 | mult = match[part["size"][-1]] 276 | end = int(part["size"][:-1]) * mult + start 277 | if part["type"] == "EFI": 278 | ptype = "fat32" 279 | elif part["type"] == "swap": 280 | ptype = "linux-swap" 281 | else: 282 | ptype = "ext2" 283 | if start == 0: 284 | # Using 0% on the first partition to get the first 1MB 285 | # border that is correctly aligned 286 | start = "0%" 287 | LOG.debug("Creating partition {0} in {1}".format(ptype, part["disk"])) 288 | if template.get("DestinationType") == "physical": 289 | command = "{0} {1} -- /dev/{2} {3} mkpart primary {4} {5} {6}"\ 290 | .format(parted, alignment, part["disk"], units, ptype, 291 | start, end) 292 | else: 293 | command = "{0} {1} -- {2} {3} mkpart primary {4} {5} {6}"\ 294 | .format(parted, alignment, part["disk"], units, ptype, 295 | start, end) 296 | run_command(command) 297 | time.sleep(sleep_time) 298 | if part["type"] == "EFI": 299 | if template.get("DestinationType") == "physical": 300 | command = "parted -s /dev/{0} set {1} boot on"\ 301 | .format(part["disk"], part["partition"]) 302 | else: 303 | command = "parted -s {0} set {1} boot on"\ 304 | .format(part["disk"], part["partition"]) 305 | run_command(command) 306 | time.sleep(sleep_time) 307 | start = end 308 | cdisk = part["disk"] 309 | 310 | 311 | def map_loop_device(template, sleep_time=1): 312 | """Setup a loop device for the image file 313 | 314 | This function will raise an Exception if the command fails. 315 | """ 316 | LOG.info("Mapping loop device") 317 | disk_image = template["PartitionLayout"][0]["disk"] 318 | command = "losetup --partscan --find --show {0}".format(disk_image) 319 | try: 320 | dev = subprocess.check_output(command.split(" ")).decode("utf-8")\ 321 | .splitlines() 322 | except Exception: 323 | raise Exception("losetup command failed: {0}: {1}" 324 | .format(command, sys.exc_info())) 325 | if len(dev) != 1: 326 | raise Exception("losetup failed to create loop device") 327 | time.sleep(sleep_time) 328 | run_command("partprobe {0}".format(dev[0])) 329 | time.sleep(sleep_time) 330 | 331 | template["dev"] = dev[0] 332 | 333 | 334 | def get_device_name(template, disk): 335 | """Return /dev/{loopXp, sdX} type device name 336 | """ 337 | # handle loop devices, disk can be None 338 | if template.get("dev"): 339 | return ("{}p".format(template["dev"]), "p") 340 | 341 | # if not a loop device, search for partition format in /dev 342 | devices = os.listdir("/dev") 343 | devgen = (name for name in devices if disk in name) 344 | for name in devgen: 345 | part = name.replace(disk, "") 346 | if part: 347 | prefix = "p" if part.startswith("p") else "" 348 | return ("/dev/{}{}".format(disk, prefix), prefix) 349 | 350 | # if we got this far, no partitions were found and nothing would be 351 | # returned, resulting in a failed install. 352 | raise Exception("No partitions found on /dev/{}".format(disk)) 353 | 354 | 355 | def create_filesystems(template): 356 | """Create filesystems according to template configuration 357 | """ 358 | 359 | # Filesystem-specific format tool options. 360 | fs_util = {"ext2": {"cmd" : "mkfs.ext2 -F", "label" : "-L"}, 361 | "ext3": {"cmd" : "mkfs.ext3 -F", "label" : "-L"}, 362 | "ext4": {"cmd" : "mkfs.ext4 -F", "label" : "-L"}, 363 | "btrfs": {"cmd" : "mkfs.btrfs -f", "label" : "-L"}, 364 | "vfat": {"cmd" : "mkfs.vfat", "label" : "-n"}, 365 | "swap": {"cmd" : "mkswap", "label" : "-L"}, 366 | "xfs": {"cmd" : "mkfs.xfs -f", "label" : "-L"}} 367 | 368 | LOG.info("Creating file systems") 369 | for fst in template["FilesystemTypes"]: 370 | (dev, prefix) = get_device_name(template, fst["disk"]) 371 | fsu = fs_util[fst["type"]] 372 | LOG.debug("Creating file system {0} in {1}{2}" 373 | .format(fst["type"], dev, fst["partition"])) 374 | 375 | opts = fst.get("options", "") 376 | if opts: 377 | opts = " " + opts 378 | if "label" in fst: 379 | opts += " {0} {1}".format(fsu["label"], fst["label"]) 380 | command = "{0}{1} {2}{3}".format(fsu["cmd"], opts, dev, 381 | fst["partition"]) 382 | if fst["type"] == "swap": 383 | if prefix: 384 | base_dev = dev[:-1] 385 | else: 386 | base_dev = dev 387 | run_command("sgdisk {0} --typecode={1}:\ 388 | 0657fd6d-a4ab-43c4-84e5-0933c84b4f4f" 389 | .format(base_dev, fst["partition"])) 390 | if "disable_format" not in fst: 391 | if "encryption" in fst: 392 | encr = fst["encryption"] 393 | c_dev = "{0}{1}".format(dev, fst["partition"]) 394 | crs = pycryptsetup.CryptSetup(device=c_dev) 395 | crs.luksFormat(cipher="aes", cipherMode="xts-plain64", 396 | keysize=512, hashMode="sha256") 397 | crs.addKeyByPassphrase(encr["passphrase"], encr["passphrase"]) 398 | crs.activate(name=encr["name"], passphrase=encr["passphrase"]) 399 | command = "{0}{1} /dev/mapper/{2}".format(fsu["cmd"], opts, 400 | encr["name"]) 401 | run_command(command) 402 | if fst["type"] == "swap": 403 | run_command("swapon {0}{1}".format(dev, fst["partition"]), 404 | raise_exception=False) 405 | 406 | 407 | def create_target_dir(args, template): 408 | """Create the target root directory 409 | """ 410 | 411 | if args.target_dir: 412 | target_dir = args.target_dir 413 | if not os.path.isdir(target_dir): 414 | raise Exception("Target directory {0} does not exist".format(target_dir)) 415 | else: 416 | try: 417 | prefix = "ister-" + str(template["Version"]) + "-" 418 | target_dir = tempfile.mkdtemp(prefix=prefix) 419 | except Exception: 420 | raise Exception("Failed to setup mounts for install") 421 | 422 | LOG.debug("Installation target directory: {0}".format(target_dir)) 423 | return target_dir 424 | 425 | def setup_mounts(target_dir, template): 426 | """Mount target folder 427 | 428 | Returns target folder name 429 | 430 | This function will raise an Exception on finding an error. 431 | """ 432 | def get_uuid(part_num, dev): 433 | """Get the uuid for a partition on a device""" 434 | result = run_command("sgdisk --info={0} {1}".format(part_num, dev)) 435 | return result[0][1].split()[-1].lower() 436 | 437 | def create_mount_unit(unit_dir, wants_dir, filename, uuid, mount, fs_type): 438 | """Create mount unit file for systemd 439 | """ 440 | LOG.debug("Creating mount unit for UUID: {0}".format(uuid)) 441 | unit = "[Unit]\nDescription = Mount for %s\n\n" % mount 442 | unit += "[Mount]\nWhat = /dev/disk/by-partuuid/{0}\nWhere = {1}\n" \ 443 | "Type = {2}\n\n".format(uuid, mount, fs_type) 444 | unit += "[Install]\nWantedBy = multi-user.target\n" 445 | unit_path = os.path.join(unit_dir, filename) 446 | symlink_path = os.path.join(wants_dir, filename) 447 | with open(unit_path, 'w') as unit_fobj: 448 | unit_fobj.write(unit) 449 | os.symlink(os.path.relpath(unit_path, wants_dir), symlink_path) 450 | 451 | LOG.info("Setting up mount points") 452 | 453 | units_dir = os.path.join(target_dir, "etc", "systemd", "system") 454 | wants_dir = os.path.join(units_dir, "local-fs.target.wants") 455 | 456 | parts = sorted(template["PartitionMountPoints"], key=lambda v: v["mount"]) 457 | has_boot = False 458 | for part in parts: 459 | if part["mount"] == "/boot": 460 | has_boot = True 461 | 462 | for part in parts: 463 | pnum = part["partition"] 464 | dev, prefix = get_device_name(template, part["disk"]) 465 | if prefix: 466 | base_dev = dev[:-1] 467 | else: 468 | base_dev = dev 469 | 470 | LOG.debug("Mounting {0}{1} in {2}".format(dev, pnum, part["mount"])) 471 | fs_type = [x["type"] for x in template["FilesystemTypes"] 472 | if x['disk'] == part['disk'] and x['partition'] == pnum][-1] 473 | 474 | if part["mount"] == "/": 475 | uuid = "4f68bce3-e8cd-4db1-96e7-fbcaf984b709" 476 | cmd = "sgdisk {0} --typecode={1}:{2}".format(base_dev, pnum, uuid) 477 | run_command(cmd) 478 | if not has_boot and template.get("LegacyBios"): 479 | cmd = "sgdisk {0} --attributes={1}:set:2".format(base_dev, pnum) 480 | run_command(cmd) 481 | if part["mount"] == "/boot" and not template.get("LegacyBios"): 482 | uuid = "c12a7328-f81f-11d2-ba4b-00a0c93ec93b" 483 | cmd = "sgdisk {0} --typecode={1}:{2}".format(base_dev, pnum, uuid) 484 | run_command(cmd) 485 | if part["mount"] == "/boot" and template.get("LegacyBios"): 486 | cmd = "sgdisk {0} --attributes={1}:set:2".format(base_dev, pnum) 487 | run_command(cmd) 488 | if part["mount"] == "/srv": 489 | uuid = "3B8F8425-20E0-4F3B-907F-1A25A76F98E8" 490 | cmd = "sgdisk {0} --typecode={1}:{2}".format(base_dev, pnum, uuid) 491 | run_command(cmd) 492 | if part["mount"] == "/home": 493 | uuid = "933AC7E1-2EB4-4F13-B844-0E14E2AEF915" 494 | cmd = "sgdisk {0} --typecode={1}:{2}".format(base_dev, pnum, uuid) 495 | run_command(cmd) 496 | if part["mount"] != "/": 497 | cmd = "mkdir -p {0}{1}".format(target_dir, part["mount"]) 498 | run_command(cmd) 499 | if "encryption" in part: 500 | cmd = "mount /dev/mapper/{0} {1}{2}" \ 501 | .format(part["encryption"]["name"], target_dir, part["mount"]) 502 | run_command(cmd) 503 | else: 504 | cmd = "mount {0}{1} {2}{3}".format(dev, pnum, target_dir, 505 | part["mount"]) 506 | run_command(cmd) 507 | 508 | # Create mount units for the partitions, except for those having standard 509 | # GPT type GUIDs, because the standard systemd 'systemd-gpt-auto-generator' 510 | # tool will generate the mount points. However, in some rare cases the 511 | # systemd tool may fail to generate a mount unit, in which case users 512 | # have a possibility to force ister creating it by specifying 'forcemu' 513 | # option. 514 | if not part.get("forcemu"): 515 | if part["mount"] in ["/", "/boot", "/srv", "/home", "/usr"]: 516 | continue 517 | if part["mount"].startswith("/usr/"): 518 | continue 519 | 520 | if not os.path.exists(wants_dir): 521 | os.makedirs(wants_dir) 522 | filename = part["mount"][1:].replace("/", "-") + ".mount" 523 | create_mount_unit(units_dir, wants_dir, filename, 524 | get_uuid(pnum, base_dev), part["mount"], fs_type) 525 | 526 | 527 | def add_bundles(template, target_dir): 528 | """Create bundle subscription file 529 | """ 530 | bundles_dir = "/usr/share/clear/bundles/" 531 | os.makedirs(target_dir + bundles_dir) 532 | for index, bundle in enumerate(template["Bundles"]): 533 | open(target_dir + bundles_dir + bundle, "w").close() 534 | 535 | # pylint: disable=undefined-loop-variable 536 | # since we never reach this point with an empty Bundles list 537 | LOG.info("Installing {} bundles (and dependencies)...".format(index + 1)) 538 | 539 | 540 | def copy_os(args, template, target_dir): 541 | """Wrapper for running install command 542 | """ 543 | package_manager = template["SoftwareManager"] 544 | LOG.info("Starting {0}. May take several minutes".format(package_manager)) 545 | if package_manager == "swupd": 546 | copy_os_swupd(args, template, target_dir) 547 | elif package_manager == "dnf": 548 | copy_os_dnf(args, template, target_dir) 549 | 550 | 551 | def copy_os_swupd(args, template, target_dir): 552 | """Wrapper for running install command with swupd 553 | """ 554 | add_bundles(template, target_dir) 555 | 556 | if args.fast_install: 557 | args.statedir = "{0}/tmp/swupd".format(target_dir) 558 | 559 | if template["DestinationType"] == "physical": 560 | os.makedirs(args.statedir, exist_ok=True) 561 | os.chmod(args.statedir, stat.S_IRWXU) 562 | os.makedirs("{0}/var/tmp".format(target_dir)) 563 | os.chmod("{0}/var/tmp".format(target_dir), stat.S_IRWXU) 564 | run_command("mount --bind {0}/var/tmp {1}" 565 | .format(target_dir, args.statedir)) 566 | 567 | cmd = "swupd verify --install" 568 | cmd += " --path={0}".format(target_dir) 569 | cmd += " --manifest={0}".format(template["Version"]) 570 | if args.contenturl: 571 | cmd += " --contenturl={0}".format(args.contenturl) 572 | if args.versionurl: 573 | cmd += " --versionurl={0}".format(args.versionurl) 574 | if args.format: 575 | cmd += " --format={0}".format(args.format) 576 | cmd += " --statedir={0}".format(args.statedir) 577 | if args.cert_file: 578 | cmd += " --certpath={0}".format(args.cert_file) 579 | if shutil.which("stdbuf"): 580 | cmd = "stdbuf -o 0 {0}".format(cmd) 581 | cmd_env = get_cmd_env(template) 582 | run_command(cmd, environ=cmd_env, show_output=True) 583 | 584 | if args.fast_install: 585 | run_command("rm -rf {0}".format(args.statedir)) 586 | 587 | 588 | def copy_os_dnf(args, template, target_dir): 589 | """Wrapper for running install command with dnf 590 | """ 591 | cmd = "dnf install --assumeyes" 592 | if args.dnf_config: 593 | cmd += " --config {0}".format(args.dnf_config) 594 | cmd += " --installroot {0}".format(target_dir) 595 | cmd += " {0}".format(" ".join(template["Bundles"])) 596 | if shutil.which("stdbuf"): 597 | cmd = "stdbuf -o 0 {0}".format(cmd) 598 | cmd_env = get_cmd_env(template) 599 | run_command(cmd, environ=cmd_env, show_output=True) 600 | 601 | 602 | def get_cmd_env(template): 603 | """Get the environment variables with which commands will execute 604 | """ 605 | cmd_env = os.environ 606 | if template.get("HTTPSProxy"): 607 | cmd_env["https_proxy"] = template["HTTPSProxy"] 608 | LOG.debug("https_proxy: {}".format(template["HTTPSProxy"])) 609 | return cmd_env 610 | 611 | 612 | class ChrootOpen(object): 613 | """Class encapsulating chroot setup and teardown 614 | """ 615 | def __init__(self, target_dir): 616 | """Stores the target directory for the chroot 617 | """ 618 | self.target_dir = target_dir 619 | self.old_root = -1 620 | 621 | def __enter__(self): 622 | """Using the target directory, setup the chroot 623 | 624 | This function will raise an Exception on finding an error. 625 | """ 626 | try: 627 | self.old_root = os.open("/", os.O_RDONLY) 628 | os.chroot(self.target_dir) 629 | os.chdir("/") 630 | except Exception: 631 | raise Exception("Unable to setup chroot to create users") 632 | 633 | return self.target_dir 634 | 635 | def __exit__(self, *args): 636 | """Using the old root, teardown the chroot 637 | 638 | This function will raise an Exception on finding an error. 639 | """ 640 | try: 641 | os.chdir(self.old_root) 642 | os.chroot(".") 643 | os.close(self.old_root) 644 | except Exception: 645 | raise Exception("Unable to restore real root after chroot") 646 | 647 | 648 | def get_user_homedir(username): 649 | """Returns user's home directory path.""" 650 | if username == "root": 651 | return os.path.join(os.sep, "root") 652 | return os.path.join(os.sep, "home", username) 653 | 654 | def create_account(user, target_dir): 655 | """Add user to the system 656 | 657 | Create a new account on the system with a home directory and one time 658 | passwordless login. Also add a new group with same name as the user 659 | """ 660 | 661 | opts = user["username"] 662 | if user.get("uid"): 663 | opts = "-u {0} ".format(user["uid"]) + opts 664 | if "password" in user: 665 | opts = "-p '{0}' ".format(user["password"]) + opts 666 | 667 | command = "useradd -U -m {0}".format(opts) 668 | 669 | with ChrootOpen(target_dir) as _: 670 | _, stderr, ret = run_command(command, raise_exception=False) 671 | if ret == 9: 672 | # '9' is a documented exit code for the "user already exists" case. 673 | # In this case just modify the existing user settings (if there is 674 | # something to modify). 675 | if opts != user["username"]: 676 | command = "usermod {0}".format(opts) 677 | run_command(command) 678 | elif ret != 0: 679 | if stderr: 680 | LOG.debug(stderr) 681 | raise Exception("failed to create user '{0}', 'useradd' returned " 682 | "exit status '{1}'".format(user["username"], ret)) 683 | 684 | def add_user_fullname(user, target_dir): 685 | """Add user's full name to /etc/passwd 686 | 687 | If the user's full name is set in the template, use chfn to set their full 688 | name in the GECOS field of the /etc/passwd file 689 | """ 690 | try: 691 | command = ["chfn", "-f", user["fullname"], user["username"]] 692 | 693 | with ChrootOpen(target_dir) as _: 694 | subprocess.call(command) 695 | except Exception as exep: 696 | print(exep) 697 | LOG.info("Unable to set user {} full name: {}".format(user["username"], 698 | exep)) 699 | 700 | 701 | def add_user_key(user, target_dir): 702 | """Append public key to user's ssh authorized_keys file 703 | 704 | This function will raise an Exception on finding an error. 705 | """ 706 | # Must run pwd.getpwnam outside of chroot to load installer shared 707 | # lib instead of target which prevents umount on cleanup 708 | pwd.getpwnam("root") 709 | 710 | sshdir = os.path.join(get_user_homedir(user["username"]), ".ssh") 711 | akey_path = os.path.join(sshdir, "authorized_keys") 712 | with ChrootOpen(target_dir) as _: 713 | try: 714 | os.makedirs(sshdir, mode=0o0700, exist_ok=True) 715 | pwinfo = pwd.getpwnam(user["username"]) 716 | uid = pwinfo[2] 717 | gid = pwinfo[3] 718 | os.chown(sshdir, uid, gid) 719 | with open(akey_path, "a") as akey_fobj: 720 | akey_fobj.write(user["key"]) 721 | os.chown(akey_path, uid, gid) 722 | except Exception as exep: 723 | raise Exception("Unable to add {0}'s ssh key to authorized " 724 | "keys: {1}".format(user["username"], exep)) 725 | 726 | 727 | def disable_root_login(target_dir): 728 | """Disables the login for root if there is a user with sudo active 729 | 730 | It reads the line of /etc/shadow for the user previously created and 731 | then it changes the username to root and the password to !. Finally, it 732 | writes the result at the end. 733 | """ 734 | line = '' 735 | with open("{0}/etc/shadow".format(target_dir)) as file: 736 | line = file.read().split('\n')[0] 737 | line = line.split(':') 738 | line[0] = 'root' 739 | line[1] = '!' 740 | line = ':'.join(line) 741 | with open("{0}/etc/shadow".format(target_dir), "a") as file: 742 | file.write(line) 743 | 744 | 745 | def setup_sudo(user, target_dir): 746 | """Add user to sudo (wheel) group 747 | 748 | This function will raise an Exception on finding an error. 749 | """ 750 | try: 751 | command = ["usermod", "-a", "-G", "wheel", user["username"]] 752 | 753 | with ChrootOpen(target_dir) as _: 754 | subprocess.call(command) 755 | except Exception: 756 | raise Exception("Unable to add sudo group for {}" 757 | .format(user["username"])) 758 | 759 | 760 | def add_users(template, target_dir): 761 | """Create user accounts with no password one time logins 762 | 763 | Will setup sudo and ssh key access if specified in template. 764 | """ 765 | users = template.get("Users") 766 | if not users: 767 | return 768 | 769 | LOG.info("Adding new user") 770 | for user in users: 771 | create_account(user, target_dir) 772 | if user.get("key"): 773 | add_user_key(user, target_dir) 774 | if user.get("sudo") and user["sudo"]: 775 | setup_sudo(user, target_dir) 776 | disable_root_login(target_dir) 777 | if user.get("fullname"): 778 | add_user_fullname(user, target_dir) 779 | 780 | 781 | def set_hostname(template, target_dir): 782 | """Writes the hostname to /etc/hostname 783 | """ 784 | 785 | hostname = template.get("Hostname") 786 | if not hostname: 787 | return 788 | LOG.info("Setting up hostname") 789 | path = '{0}/etc/'.format(target_dir) 790 | if not os.path.exists(path): 791 | os.makedirs(path) 792 | 793 | with open(path + "hostname", "w") as file: 794 | file.write(hostname) 795 | 796 | 797 | def set_mirror_url(template, target_dir): 798 | """Writes custom mirror url to /etc/swupd/mirror_contenturl 799 | """ 800 | target_mirror_url = template.get("MirrorURL") 801 | if not target_mirror_url: 802 | return 803 | 804 | LOG.info("Setting custom mirror url") 805 | path = '{0}/etc/swupd/'.format(target_dir) 806 | if not os.path.exists(path): 807 | os.makedirs(path) 808 | 809 | with open(path + "mirror_contenturl", "w") as file: 810 | file.write(target_mirror_url) 811 | 812 | 813 | def set_mirror_version_url(template, target_dir): 814 | """Writes custom mirror version url to /etc/swupd/mirror_versionurl 815 | """ 816 | target_mirror_version_url = template.get("VersionURL") 817 | if not target_mirror_version_url: 818 | return 819 | 820 | LOG.info("Setting custom mirror version url") 821 | path = '{0}/etc/swupd/'.format(target_dir) 822 | if not os.path.exists(path): 823 | os.makedirs(path) 824 | 825 | with open(path + "mirror_versionurl", "w") as file: 826 | file.write(target_mirror_version_url) 827 | 828 | 829 | def set_static_configuration(template, target_dir): 830 | """Writes the configuration on /etc/systemd/network/10-en-static.network 831 | """ 832 | 833 | static_conf = template.get("Static_IP") 834 | if not static_conf: 835 | return 836 | 837 | path = '{0}/etc/systemd/network/'.format(target_dir) 838 | if not os.path.exists(path): 839 | os.makedirs(path) 840 | 841 | with open(path + "10-en-static.network", "w") as file: 842 | file.write("[Match]\n") 843 | file.write("Name={}\n\n".format(static_conf["iface"])) 844 | file.write("[Network]\n") 845 | file.write("Address={0}\n".format(static_conf["address"])) 846 | file.write("Gateway={0}\n".format(static_conf["gateway"])) 847 | if "dns" in static_conf: 848 | file.write("DNS={0}\n".format(static_conf["dns"])) 849 | 850 | 851 | def set_kernel_cmdline_appends(template, target_dir): 852 | """Write template['cmdline'] to /etc/kernel/cmdline 853 | """ 854 | if not template.get("cmdline"): 855 | return 856 | 857 | cmdline_path = os.path.join(target_dir, "etc/kernel/") 858 | if not os.path.exists(cmdline_path): 859 | os.makedirs(cmdline_path) 860 | 861 | with open(os.path.join(cmdline_path, "cmdline"), "w") as cmdline_f: 862 | cmdline_f.write(template["cmdline"]) 863 | 864 | run_command("{0}/usr/bin/clr-boot-manager update --path {0}" 865 | .format(target_dir)) 866 | 867 | 868 | def pre_install_shell(template): 869 | """Run pre install commands 870 | """ 871 | if not template.get("PreInstallShell"): 872 | return 873 | LOG.info("Running pre install commands") 874 | for cmdl in template["PreInstallShell"]: 875 | run_command(cmdl, shell=True) 876 | 877 | 878 | def post_install_nonchroot(template, target_dir): 879 | """Run non chroot post install scripts 880 | 881 | All post scripts must be executable. 882 | 883 | The mount root for the install is passed as an argument to each script. 884 | """ 885 | if not template.get("PostNonChroot"): 886 | return 887 | LOG.info("Running post non-chroot scripts") 888 | for script in template["PostNonChroot"]: 889 | run_command(script + " {}".format(target_dir)) 890 | 891 | 892 | def post_install_nonchroot_shell(template, target_dir): 893 | """Run non chroot post install commands 894 | 895 | The mount root for the install is passed as the ISTER_CHROOT environment 896 | variable. 897 | """ 898 | if not template.get("PostNonChrootShell"): 899 | return 900 | LOG.info("Running post non-chroot commands") 901 | script_env = os.environ 902 | script_env["ISTER_CHROOT"] = target_dir 903 | for cmdl in template["PostNonChrootShell"]: 904 | run_command(cmdl, shell=True, environ=script_env) 905 | 906 | 907 | def post_install_chroot(template, target_dir): 908 | """Run chroot post install scripts 909 | 910 | All post scripts must be executable. 911 | """ 912 | if not template.get("PostChroot"): 913 | return 914 | LOG.info("Running post scripts") 915 | with ChrootOpen(target_dir) as _: 916 | for script in template["PostChroot"]: 917 | run_command(script) 918 | 919 | 920 | def post_install_chroot_shell(template, target_dir): 921 | """Run chroot post install commands 922 | """ 923 | if not template.get("PostChrootShell"): 924 | return 925 | LOG.info("Running post commands") 926 | with ChrootOpen(target_dir) as _: 927 | for cmdl in template["PostChrootShell"]: 928 | run_command(cmdl, shell=True) 929 | 930 | def cleanup(args, template, target_dir, raise_exception=True): 931 | """Unmount and remove temporary files 932 | """ 933 | if args.no_unmount: 934 | LOG.info("Skip unmounting target image at {0}".format(target_dir)) 935 | return 936 | 937 | LOG.info("Cleaning up") 938 | if target_dir: 939 | if os.path.isdir("{0}/var/tmp".format(target_dir)): 940 | run_command("umount {0}".format(args.statedir), 941 | raise_exception=raise_exception) 942 | run_command("rm -fr {0}/var/tmp".format(target_dir), 943 | raise_exception=raise_exception) 944 | try: 945 | run_command("umount -R {}".format(target_dir)) 946 | except Exception: 947 | run_command("lsof {}/boot".format(target_dir), 948 | raise_exception=raise_exception) 949 | 950 | if not args.target_dir: 951 | # --target-dir was not used. 952 | run_command("rm -fr {}".format(target_dir), 953 | raise_exception=raise_exception) 954 | 955 | # Turn off any swap devices we enabled 956 | for fst in template["FilesystemTypes"]: 957 | (dev, _) = get_device_name(template, fst["disk"]) 958 | if fst["type"] == "swap": 959 | run_command("swapoff {0}{1}".format(dev, fst["partition"]), 960 | raise_exception=raise_exception) 961 | 962 | if template.get("dev"): 963 | run_command("losetup --detach {0}".format(template["dev"]), 964 | raise_exception=raise_exception) 965 | for dev_entry in template['PartitionMountPoints']: 966 | if 'encryption' in dev_entry: 967 | crs = pycryptsetup.CryptSetup(name=dev_entry['encryption']['name']) 968 | crs.deactivate() 969 | 970 | 971 | def get_template_location(path): 972 | """Read the installer configuration file for the template location 973 | 974 | This function will raise an Exception on finding an error. 975 | """ 976 | with open(path, "r") as conf_file: 977 | contents = conf_file.readline().rstrip().split('=') 978 | 979 | if contents[0] != "template" or len(contents) != 2: 980 | # This does not look like a valid configuration file. Let's assume this 981 | # is the template. 982 | return "file://" + path 983 | return contents[1] 984 | 985 | 986 | def get_template(template_location): 987 | """Fetch JSON template file for installer 988 | """ 989 | json_file = request.urlopen(template_location) 990 | parsed_json = json.loads(json_file.read().decode("utf-8")) 991 | # Supply default SoftwareManager value if not defined for backwards compatibility 992 | if not parsed_json.get("SoftwareManager"): 993 | parsed_json["SoftwareManager"] = "swupd" 994 | return parsed_json 995 | 996 | 997 | def validate_layout(template): 998 | """Validate partition layout is sane 999 | 1000 | Returns mapping of layout to disk partitions. 1001 | 1002 | This function will raise an Exception on finding an error. 1003 | """ 1004 | disk_to_parts = {} 1005 | parts_to_size = {} 1006 | has_efi = False 1007 | accepted_ptypes = ["EFI", "linux", "swap"] 1008 | accepted_sizes = ["M", "G", "T"] 1009 | 1010 | for layout in template["PartitionLayout"]: 1011 | disk = layout.get("disk") 1012 | part = layout.get("partition") 1013 | size = layout.get("size") 1014 | ptype = layout.get("type") 1015 | 1016 | if not disk or not part or not size or not ptype: 1017 | raise Exception("Invalid PartitionLayout section: {}" 1018 | .format(layout)) 1019 | 1020 | if size[-1] not in accepted_sizes and size != "rest": 1021 | raise Exception("Invalid size specified in section {0}" 1022 | .format(layout)) 1023 | if size != "rest" and int(size[:-1]) <= 0: 1024 | raise Exception("Invalid size specified in section {0}" 1025 | .format(layout)) 1026 | 1027 | if ptype not in accepted_ptypes: 1028 | raise Exception("Invalid partiton type {0}, supported types \ 1029 | are: {1}".format(ptype, accepted_ptypes)) 1030 | 1031 | if ptype == "EFI" and has_efi: 1032 | raise Exception("Multiple EFI partitions defined") 1033 | 1034 | if ptype == "EFI": 1035 | has_efi = True 1036 | 1037 | disk_part = disk + str(part) 1038 | if disk_to_parts.get(disk): 1039 | if part in disk_to_parts[disk]: 1040 | raise Exception("Duplicate disk {0} and partition {1} entry \ 1041 | in PartitionLayout".format(disk, part)) 1042 | disk_to_parts[disk].append(part) 1043 | else: 1044 | disk_to_parts[disk] = [part] 1045 | parts_to_size[disk_part] = size 1046 | 1047 | for disk in disk_to_parts: 1048 | if len(disk_to_parts[disk]) > 128: 1049 | raise Exception("GPT disk with more than 128 partitions: {0}" 1050 | .format(disk)) 1051 | 1052 | if template["DestinationType"] == "virtual" and len(disk_to_parts) != 1: 1053 | raise Exception("Mulitple files for virtual disk \ 1054 | destination is unsupported") 1055 | if not has_efi and template["DestinationType"] != "virtual" and \ 1056 | template.get("LegacyBios") is not True: 1057 | raise Exception("No EFI partition defined") 1058 | 1059 | for key in disk_to_parts: 1060 | parts = sorted(disk_to_parts[key]) 1061 | for part in parts: 1062 | if parts_to_size[key + str(part)] == "rest" and part != parts[-1]: 1063 | raise Exception("Partition other than last uses rest of \ 1064 | disk {0} partition {1}".format(key, part)) 1065 | 1066 | return parts_to_size 1067 | 1068 | 1069 | def validate_fstypes(template, parts_to_size): 1070 | """Validate filesystem types are sane 1071 | 1072 | Returns a set of disk partitions with filesystem type information. 1073 | 1074 | This function will raise an Exception on finding an error. 1075 | """ 1076 | partition_fstypes = set() 1077 | accepted_fstypes = ["ext2", "ext3", "ext4", "vfat", "btrfs", "xfs", "swap"] 1078 | force_fmt = [(item.get("disk"), item.get("partition")) 1079 | for item in template.get("PartitionMountPoints", list()) 1080 | if item.get("mount", "") == "/"] 1081 | for fstype in template["FilesystemTypes"]: 1082 | disk = fstype.get("disk") 1083 | part = fstype.get("partition") 1084 | disable_fmt = fstype.get("disable_format") 1085 | fstype = fstype.get("type") 1086 | if not disk or not part or not fstype: 1087 | raise Exception("Invalid FilesystemTypes section: {}" 1088 | .format(fstype)) 1089 | 1090 | if fstype not in accepted_fstypes: 1091 | raise Exception("Invalid filesystem type {0}, supported types \ 1092 | are: {1}".format(fstype, accepted_fstypes)) 1093 | 1094 | disk_part = disk + str(part) 1095 | if disk_part in partition_fstypes: 1096 | raise Exception("Duplicate disk '{0}' and partition {1} entry in \ 1097 | FilesystemTypes".format(disk, part)) 1098 | if disk_part not in parts_to_size: 1099 | raise Exception("disk '{0}' partition {1} used in FilesystemTypes \ 1100 | not found in PartitionLayout".format(disk, part)) 1101 | if force_fmt and force_fmt[0][0] == disk \ 1102 | and force_fmt[0][1] == part and disable_fmt is not None: 1103 | raise Exception("/ does not apply to disable_format") 1104 | partition_fstypes.add(disk_part) 1105 | 1106 | return partition_fstypes 1107 | 1108 | 1109 | def validate_partition_mounts(template, partition_fstypes): 1110 | """Validate partition mount points are sane 1111 | 1112 | This function will raise an Exception on finding an error. 1113 | """ 1114 | has_rootfs = False 1115 | has_boot = False 1116 | disk_partitions = set() 1117 | partition_mounts = set() 1118 | for pmount in template["PartitionMountPoints"]: 1119 | disk = pmount.get("disk") 1120 | part = pmount.get("partition") 1121 | mount = pmount.get("mount") 1122 | if not disk or not part or not mount: 1123 | raise Exception("Invalid PartitionMountPoints section: {}" 1124 | .format(pmount)) 1125 | 1126 | if mount == "/": 1127 | has_rootfs = True 1128 | if mount == "/boot": 1129 | has_boot = True 1130 | disk_part = disk + str(part) 1131 | if mount in partition_mounts: 1132 | raise Exception("Duplicate mount points found") 1133 | if disk_part in disk_partitions: 1134 | raise Exception("Duplicate disk {0} and partition {1} entry in " 1135 | "PartitionMountPoints".format(disk, part)) 1136 | if disk_part not in partition_fstypes: 1137 | raise Exception("disk {0} partition {1} used in " 1138 | "PartitionMountPoints not found in FilesystemTypes" 1139 | .format(disk, part)) 1140 | if "forcemu" in pmount and not isinstance(pmount["forcemu"], bool): 1141 | raise Exception("'focecmu' of disk {0} partition {1} used in " 1142 | "PartitionMountPoints has incorrect type '{2}', " 1143 | "but it should be boolean" 1144 | .format(disk, part, type(pmount["forcemu"]))) 1145 | partition_mounts.add(mount) 1146 | disk_partitions.add(disk_part) 1147 | 1148 | if not has_rootfs: 1149 | raise Exception("Missing rootfs mount") 1150 | if not has_boot and template["DestinationType"] != "virtual" and \ 1151 | template.get("LegacyBios") is not True: 1152 | raise Exception("Missing boot mount") 1153 | 1154 | 1155 | def validate_type_template(template): 1156 | """Attempt to verify the type of install target is sane 1157 | 1158 | This function will raise an Exception on finding an error. 1159 | """ 1160 | dest_type = template["DestinationType"] 1161 | if dest_type not in ("physical", "virtual"): 1162 | raise Exception("Invalid destination type") 1163 | 1164 | 1165 | def validate_disk_template(template): 1166 | """Attempt to verify all disk layout related information is sane 1167 | """ 1168 | parts_to_size = validate_layout(template) 1169 | partition_fstypes = validate_fstypes(template, parts_to_size) 1170 | validate_partition_mounts(template, partition_fstypes) 1171 | 1172 | 1173 | def validate_version_template(template): 1174 | """Attempt to verify the version is sane 1175 | """ 1176 | version = template["Version"] 1177 | if isinstance(version, int) and version <= 0: 1178 | raise Exception("Invalid version number") 1179 | if isinstance(version, str) and version != "latest": 1180 | raise Exception("Invalid version string (must be 'latest')") 1181 | 1182 | 1183 | def validate_softmgr_template(template): 1184 | """Attempt to verify the package manager is sane 1185 | """ 1186 | package_manager = template["SoftwareManager"] 1187 | if package_manager != "dnf" and package_manager != "swupd": 1188 | raise Exception("Invalid package manager. Use either swupd or dnf") 1189 | 1190 | 1191 | def validate_user_template(users): 1192 | """Attempt to verify all user related information is sane 1193 | 1194 | Also cache the users public keys, so we fail early if the key isn't 1195 | found. 1196 | 1197 | This function will raise an Exception on finding an error. 1198 | """ 1199 | max_uid = ctypes.c_uint32(-1).value 1200 | uids = {} 1201 | unames = {} 1202 | for user in users: 1203 | name = user.get("username") 1204 | uid = user.get("uid") 1205 | sudo = user.get("sudo") 1206 | key = user.get("key") 1207 | password = user.get("password") 1208 | 1209 | if not name: 1210 | raise Exception("Missing username for user entry: {}".format(user)) 1211 | if unames.get(name): 1212 | raise Exception("Duplicate username: {}".format(name)) 1213 | unames[name] = name 1214 | 1215 | if uid: 1216 | iuid = int(uid) 1217 | if uid and uids.get(uid): 1218 | raise Exception("Duplicate UID: {}".format(uid)) 1219 | elif uid: 1220 | if iuid < 1 or iuid > max_uid: 1221 | raise Exception("Invalid UID: {}".format(uid)) 1222 | uids[uid] = uid 1223 | 1224 | if sudo is not None: 1225 | if not isinstance(sudo, bool): 1226 | raise Exception("Invalid sudo option") 1227 | if sudo and not key and (password is None or password == ""): 1228 | raise Exception("Missing password for user entry: {0}" 1229 | .format(user)) 1230 | 1231 | if key: 1232 | try: 1233 | with open(user["key"], "r") as key_file: 1234 | user["key"] = key_file.read() 1235 | except FileNotFoundError: 1236 | # Let's assume this is the public key value, not the file name 1237 | # containing the public key. 1238 | pass 1239 | except OSError as err: 1240 | raise Exception("failed to read public SSH key file for user " 1241 | "'{0}': {1}".format(name, err)) 1242 | # Basic key validation: check that it consists of 3 components 1243 | # (type, str, comment) and the str part is a base64-ecoded string. 1244 | key = user["key"].split() 1245 | if len(key) < 3: 1246 | raise Exception("Invalid public SSH for user '{}'".format(name)) 1247 | try: 1248 | base64.b64decode(key[1], validate=True) 1249 | except binascii.Error as err: 1250 | raise Exception("Invalid public SSH for user '{0}', base64 " 1251 | "decoding failed: {1}".format(name, err)) 1252 | 1253 | 1254 | def validate_hostname_template(hostname): 1255 | """Attemp to verify if the hostname has an accepted value 1256 | 1257 | This function will raise an Exception on finding an error. 1258 | """ 1259 | pattern = re.compile("^[a-zA-Z0-9][a-zA-Z0-9-]{0,63}$") 1260 | if not pattern.match(hostname): 1261 | raise Exception("Hostname can only contain letters, digits and dashes") 1262 | 1263 | 1264 | def validate_static_ip_template(static_conf): 1265 | """Attemp to verify if the static ip configuration is good 1266 | This function will raise an Exception on finding an error. 1267 | """ 1268 | # pylint: disable=W1401 1269 | # http://stackoverflow.com/questions/10006459/ 1270 | # regular-expression-for-ip-address-validation 1271 | pattern = re.compile("^(?:(?:2[0-4]\d|25[0-5]|1\d{2}|[1-9]?\d)\.){3}" 1272 | "(?:2[0-4]\d|25[0-5]|1\d{2}|[1-9]?\d)" 1273 | "(?:\:(?:\d|[1-9]\d{1,3}|[1-5]\d{4}|6[0-4]\d{3}" 1274 | "|65[0-4]\d{2}|655[0-2]\d|6553[0-5]))?$") 1275 | if "address" not in static_conf: 1276 | raise Exception("Missing address in {0}".format(static_conf)) 1277 | if "gateway" not in static_conf: 1278 | raise Exception("Missing gateway in {0}".format(static_conf)) 1279 | # tmp contains mask
/ 1280 | tmp = static_conf["address"].split('/') 1281 | if len(tmp) <= 1: 1282 | raise Exception("Missing mask prefix in {0}" 1283 | .format(static_conf["address"])) 1284 | address = tmp[0] 1285 | mask = tmp[1] 1286 | if not mask.isdigit(): 1287 | raise Exception("The mask should be an integer, found '{0}'" 1288 | .format(mask)) 1289 | ips = [address, static_conf["gateway"]] 1290 | if "dns" in static_conf: 1291 | ips.append(static_conf['dns']) 1292 | for item in ips: 1293 | if not pattern.match(item): 1294 | raise Exception("Invalid ip format for entry '{0}'".format(item)) 1295 | if ips[0] == ips[1]: 1296 | raise Exception("Gateway has equal value to address '{0}'" 1297 | .format(static_conf)) 1298 | 1299 | 1300 | def validate_postnonchroot_template(scripts): 1301 | """Attempt to verify all post non-chroot scripts exist 1302 | 1303 | This function will raise an Exception on finding an error. 1304 | """ 1305 | for script in scripts: 1306 | if not os.path.isfile(script): 1307 | raise Exception("Missing post nonchroot script {}" 1308 | .format(script)) 1309 | 1310 | 1311 | def validate_legacybios_template(legacy): 1312 | """Attempt to verify legacy bios setting is valid 1313 | 1314 | This function will raise an Exception on finding an error. 1315 | """ 1316 | if not isinstance(legacy, bool): 1317 | raise Exception("Invalid type for LegacyBios, must be True or False") 1318 | 1319 | 1320 | def validate_proxy_url_template(proxy): 1321 | """Attempt to verify the proxy setting is valid 1322 | 1323 | This function will raise an Exception on finding an error. 1324 | """ 1325 | url = urlparse(proxy) 1326 | if not (url.scheme and url.netloc): 1327 | raise Exception("Invalid proxy url: {}".format(proxy)) 1328 | 1329 | 1330 | def validate_mirror_url_template(mirror): 1331 | """Attempt to verify the mirror setting is valid 1332 | 1333 | This function will raise an Exception on finding an error. 1334 | """ 1335 | url = urlparse(mirror) 1336 | if not (url.scheme and url.netloc): 1337 | raise Exception("Invalid mirror url: {}".format(mirror)) 1338 | 1339 | 1340 | def validate_mirror_version_url_template(version): # pylint:disable=invalid-name 1341 | """Attempt to verify the version setting is valid 1342 | 1343 | This function will raise an Exception on finding an error. 1344 | """ 1345 | url = urlparse(version) 1346 | if not (url.scheme and url.netloc): 1347 | raise Exception("Invalid version url: {}".format(version)) 1348 | 1349 | 1350 | def validate_cmdline_template(cmdline): 1351 | """Attempt to verify the cmdline configuration 1352 | 1353 | This function will raise an Exception on finding an error. 1354 | """ 1355 | if not isinstance(cmdline, str): 1356 | raise Exception("cmdline must be stored as a string") 1357 | 1358 | 1359 | def validate_template(template): 1360 | """Attempt to verify template is sane 1361 | 1362 | This function will raise an Exception on finding an error. 1363 | """ 1364 | LOG.info("Validating configuration") 1365 | if not template.get("DestinationType"): 1366 | raise Exception("Missing DestinationType field") 1367 | if not template.get("PartitionLayout"): 1368 | raise Exception("Missing PartitionLayout field") 1369 | if not template.get("FilesystemTypes"): 1370 | raise Exception("Missing FilesystemTypes field") 1371 | if not template.get("PartitionMountPoints"): 1372 | raise Exception("Missing PartitionMountPoints field") 1373 | if not template.get("Version"): 1374 | raise Exception("Missing Version field") 1375 | if not template.get("Bundles"): 1376 | raise Exception("Missing Bundles field") 1377 | validate_type_template(template) 1378 | validate_disk_template(template) 1379 | validate_version_template(template) 1380 | validate_softmgr_template(template) 1381 | if template.get("Users"): 1382 | validate_user_template(template["Users"]) 1383 | if template.get("Hostname") is not None: 1384 | validate_hostname_template(template["Hostname"]) 1385 | if template.get("Static_IP") is not None: 1386 | validate_static_ip_template(template['Static_IP']) 1387 | if template.get("PostNonChroot"): 1388 | validate_postnonchroot_template(template["PostNonChroot"]) 1389 | if template.get("LegacyBios"): 1390 | validate_legacybios_template(template["LegacyBios"]) 1391 | if template.get("HTTPSProxy"): 1392 | validate_proxy_url_template(template["HTTPSProxy"]) 1393 | if template.get("HTTPProxy"): 1394 | validate_proxy_url_template(template["HTTPProxy"]) 1395 | if template.get("MirrorURL"): 1396 | validate_mirror_url_template(template["MirrorURL"]) 1397 | if template.get("VersionURL"): 1398 | validate_mirror_version_url_template(template["VersionURL"]) 1399 | if template.get("cmdline"): 1400 | validate_cmdline_template(template["cmdline"]) 1401 | LOG.debug("Configuration is valid:") 1402 | LOG.debug(template) 1403 | 1404 | 1405 | def download_ister_conf(uri, timeout=15): 1406 | """Download the ister.conf/ister.json file from 'uri' to a local temporary 1407 | file and return the temporary file path. The timeout argument specifies for 1408 | how long to try downloading the file.""" 1409 | 1410 | tmpfd, abs_path = tempfile.mkstemp() 1411 | LOG.debug("ister_conf tmp file = {0}".format(abs_path)) 1412 | 1413 | start_time = time.time() 1414 | while True: 1415 | try: 1416 | with request.urlopen(uri) as response: 1417 | with closing(os.fdopen(tmpfd, "wb")) as out_file: 1418 | shutil.copyfileobj(response, out_file) 1419 | return abs_path 1420 | except Exception as err: 1421 | # In a PXE environment it's possible systemd launched us before the 1422 | # network is up. Therefore, keep trying for 'timeout' seconds. 1423 | if time.time() - start_time > timeout: 1424 | raise Exception("failed to download ister.conf from '{0}': {1}"\ 1425 | .format(uri, err)) 1426 | 1427 | 1428 | def enable_root_ssh_login(): 1429 | """Configure current environment to passwordless root SSH log in. 1430 | NOTE: this is not about configuring the final system, this is about 1431 | configuring the currently running installer system. 1432 | 1433 | The idea here is to open a possibility for the user to reconfigure the 1434 | installer system/environment or follow the installation log during the 1435 | installation process from an outside script. 1436 | """ 1437 | 1438 | LOG.debug("Enable paswordless root SSH login") 1439 | # Set empty root password. 1440 | run_command("usermod -p '' root") 1441 | # Create a minimal SSH server config file allowing for root login and SFTP. 1442 | sshdir = os.path.join("etc", "ssh") 1443 | os.makedirs(sshdir, mode=0o0755, exist_ok=True) 1444 | with open(os.path.join(sshdir, "sshd_config"), "a") as fobj: 1445 | os.fchmod(fobj.fileno(), 0o600) 1446 | fobj.write("# Added by ister due to 'ister.root_ssh'.\n" 1447 | "PermitRootLogin yes\n" 1448 | "PermitEmptyPasswords yes\n" 1449 | "Subsystem sftp /usr/libexec/sftp-server\n") 1450 | run_command("systemctl enable --now sshd.socket") 1451 | 1452 | 1453 | def process_kernel_cmdline(f_kcmdline): 1454 | """Some ister options can be passed via carnel configuration file. For 1455 | example, 'isterconf=' can be used for passing ister configuration file 1456 | (AKA 'ister.conf') or ister template file (AKA 'ister.json'). This function 1457 | processes ister kernel command line options and returns the results. 1458 | 1459 | If ister.conf/ister.json file was specified, this function downloads it and 1460 | returns path to a local copy of the file. Otherwise returns 'None'. 1461 | """ 1462 | LOG.debug("Inspecting kernel command line for ister.conf location") 1463 | LOG.debug("kernel command line file: {0}".format(f_kcmdline)) 1464 | kernel_args = list() 1465 | ister_conf_uri = None 1466 | with open(f_kcmdline, "r") as file: 1467 | kernel_args = set(file.read().split()) 1468 | 1469 | if "ister.root_ssh" in kernel_args: 1470 | # Handle the "root_ssh" option first enable root login ASAP. 1471 | enable_root_ssh_login() 1472 | if "ister.exit" in kernel_args: 1473 | LOG.info("exiting with status 13 due to 'ister.exit' found in kernel " 1474 | "command line") 1475 | raise SystemExit(13) 1476 | for opt in kernel_args: 1477 | if opt.startswith("isterconf="): 1478 | ister_conf_uri = opt.split("=")[1] 1479 | 1480 | LOG.debug("ister_conf_uri = {0}".format(ister_conf_uri)) 1481 | if ister_conf_uri: 1482 | return download_ister_conf(ister_conf_uri) 1483 | return None 1484 | 1485 | 1486 | def get_host_from_url(url): 1487 | """ Given url, return the host:port portion 1488 | Try to be protocol agnostic 1489 | """ 1490 | LOG.debug("Extracting host component of cloud-init-svc url") 1491 | parsed = urlparse(url) 1492 | LOG.debug("URL parsed") 1493 | return parsed.hostname or None 1494 | 1495 | 1496 | def get_iface_for_host(host): 1497 | """ Get interface being used to reach host 1498 | """ 1499 | LOG.debug("Finding interface used to reach {0}".format(host)) 1500 | ip_addr = socket.gethostbyname(host) 1501 | cmd = "ip route show to match {0}".format(ip_addr) 1502 | iface = None 1503 | 1504 | output, _, ret = run_command(cmd) 1505 | LOG.debug("Output from ip route show...") 1506 | LOG.debug(output) 1507 | if ret == 0: 1508 | match = re.match(r'.*dev (\w+)', output[-1]) 1509 | iface = match.group(1) 1510 | # Maybe make sure this really exists? 1511 | 1512 | return iface 1513 | 1514 | 1515 | def get_mac_for_iface(iface): 1516 | """ Get the MAC address for iface 1517 | """ 1518 | # pylint: disable=E1101 1519 | LOG.debug("Determining MAC address for iface {0}".format(iface)) 1520 | try: 1521 | addrs = netifaces.ifaddresses(iface) 1522 | except Exception: 1523 | return None 1524 | macs = addrs[netifaces.AF_LINK] 1525 | mac = macs[0].get('addr') 1526 | LOG.debug("FOUND MAC address {0}".format(mac)) 1527 | return mac 1528 | 1529 | 1530 | def fetch_cloud_init_configs(src_url, mac): 1531 | """ Fetch the json configs from ister-cloud-init-svc for mac 1532 | """ 1533 | src_url += 'get_config/{0}'.format(mac) 1534 | LOG.debug("Fetching cloud init configs from:\n" 1535 | "\t{0}".format(src_url)) 1536 | try: 1537 | json_file = request.urlopen(src_url) 1538 | except Exception: 1539 | json_file = None 1540 | 1541 | if json_file is not None: 1542 | return json.loads(json_file.read().decode("utf-8")) 1543 | 1544 | return dict() 1545 | 1546 | 1547 | def get_cloud_init_configs(icis_source): 1548 | """ Fetch configs from ister-cloud-init-svc 1549 | """ 1550 | 1551 | # TODO: Iterate over all interfaces in the future? 1552 | 1553 | # extract hostname/ip from url 1554 | host = get_host_from_url(icis_source) 1555 | if not host: 1556 | LOG.debug("Could not extract hostname for ister cloud " 1557 | "init service from url: {0}".format(icis_source)) 1558 | return None 1559 | 1560 | # get interface being used to communicate 1561 | iface = get_iface_for_host(host) 1562 | if not iface: 1563 | LOG.debug("No route to ister-cloud-init-svc host?" 1564 | " Failed to find interface for route") 1565 | return None 1566 | 1567 | mac = get_mac_for_iface(iface) 1568 | if not mac: 1569 | LOG.debug("Could not find MAC for iface: {0}".format(iface)) 1570 | return None 1571 | 1572 | # query icis service for confs 1573 | icis_confs = fetch_cloud_init_configs(icis_source, mac) 1574 | 1575 | # return confs 1576 | return icis_confs 1577 | 1578 | 1579 | def fetch_cloud_init_role(icis_source, role, target_dir): 1580 | """ Get role from icis_source - install into target 1581 | """ 1582 | icis_role_url = icis_source + "get_role/" + role 1583 | out_file = target_dir + "/etc/cloud-init-user-data" 1584 | LOG.debug("Fetching role file from {0}".format(icis_role_url)) 1585 | 1586 | with request.urlopen(icis_role_url) as response: 1587 | with closing(open(out_file, 'wb')) as out_file: 1588 | shutil.copyfileobj(response, out_file) 1589 | 1590 | 1591 | def modify_cloud_init_service_file(target_dir): 1592 | """ Modify cloud-init service file to use userdata file 1593 | that was just installed. 1594 | """ 1595 | LOG.debug("Updating cloud-init.service to user role file for user-data") 1596 | cloud_init_file = target_dir + "/usr/lib/systemd/system/ucd.service" 1597 | 1598 | with open(cloud_init_file, "r") as service_file: 1599 | lines = service_file.readlines() 1600 | with open(cloud_init_file, "w") as service_file: 1601 | for line in lines: 1602 | service_file.write(re.sub("(ExecStart.*) --metadata " 1603 | "--user-data-once", 1604 | r"\1 --user-data-file " 1605 | r"/etc/cloud-init-user-data", line)) 1606 | 1607 | 1608 | def cloud_init_configs(template, target_dir): 1609 | """ fetch configs from ister-cloud-init-svc and set appropriate 1610 | template entries. Configs from ister-cloud-init-svc trump 1611 | anything already in the template. 1612 | """ 1613 | 1614 | icis_source = template.get("IsterCloudInitSvc") 1615 | 1616 | if icis_source: 1617 | icis_confs = get_cloud_init_configs(icis_source) 1618 | 1619 | icis_role = icis_confs.get('role') 1620 | 1621 | if icis_role: 1622 | fetch_cloud_init_role(icis_source, icis_role, target_dir) 1623 | modify_cloud_init_service_file(target_dir) 1624 | 1625 | 1626 | def parse_config(args): 1627 | """Setup configuration dict holding ister settings 1628 | 1629 | This function will raise an Exception on finding an error. 1630 | """ 1631 | LOG.info("Reading configuration") 1632 | config = {} 1633 | 1634 | kconf_file = process_kernel_cmdline(args.kcmdline) 1635 | 1636 | if kconf_file: 1637 | config["template"] = get_template_location(kconf_file) 1638 | elif args.config_file: 1639 | config["template"] = get_template_location(args.config_file) 1640 | elif os.path.isfile("/etc/ister.conf"): 1641 | config["template"] = get_template_location("/etc/ister.conf") 1642 | elif os.path.isfile("/usr/share/defaults/ister/ister.conf"): 1643 | config["template"] = get_template_location( 1644 | "/usr/share/defaults/ister/ister.conf" 1645 | ) 1646 | elif args.template_file: 1647 | pass 1648 | else: 1649 | raise Exception("Couldn't find configuration file") 1650 | 1651 | if args.template_file: 1652 | if args.template_file[0] == "/": 1653 | config["template"] = "file://" + args.template_file 1654 | else: 1655 | config["template"] = "file://" + os.path.\ 1656 | abspath(args.template_file) 1657 | LOG.debug("File found: {0}".format(config["template"])) 1658 | return config 1659 | 1660 | 1661 | def install_os(args, template): 1662 | """Install the OS 1663 | 1664 | Start out parsing the configuration file for URI of the template. 1665 | After the template file is located, download the template and validate it. 1666 | If the template is valid, run the installation procedure. 1667 | 1668 | This function will raise an Exception on finding an error. 1669 | """ 1670 | target_dir = None 1671 | 1672 | validate_template(template) 1673 | try: 1674 | # Disabling this until implementation replaced with pycurl 1675 | # validate_network(args.url) 1676 | pre_install_shell(template) 1677 | if template["DestinationType"] == "virtual": 1678 | create_virtual_disk(template) 1679 | if not template.get("DisabledNewPartitions", False): 1680 | create_partitions(template) 1681 | if template["DestinationType"] == "virtual": 1682 | map_loop_device(template) 1683 | create_filesystems(template) 1684 | target_dir = create_target_dir(args, template) 1685 | setup_mounts(target_dir, template) 1686 | copy_os(args, template, target_dir) 1687 | add_users(template, target_dir) 1688 | set_hostname(template, target_dir) 1689 | set_mirror_url(template, target_dir) 1690 | set_mirror_version_url(template, target_dir) 1691 | set_static_configuration(template, target_dir) 1692 | set_kernel_cmdline_appends(template, target_dir) 1693 | if template.get("IsterCloudInitSvc"): 1694 | LOG.debug("Detected IsterCloudInitSvc directive") 1695 | cloud_init_configs(template, target_dir) 1696 | post_install_nonchroot(template, target_dir) 1697 | post_install_nonchroot_shell(template, target_dir) 1698 | post_install_chroot(template, target_dir) 1699 | post_install_chroot_shell(template, target_dir) 1700 | except Exception as excep: 1701 | LOG.error("Couldn't install ClearLinux") 1702 | raise excep 1703 | finally: 1704 | cleanup(args, template, target_dir, False) 1705 | 1706 | 1707 | def handle_logging(level, logfile, shandler=logging.StreamHandler(sys.stdout)): 1708 | """Setup log levels and direct logs to a file""" 1709 | # Apparently the LOG object's level trumps level of handler? 1710 | LOG.setLevel(logging.DEBUG) 1711 | 1712 | shandler.setLevel(logging.INFO) 1713 | if level == 'debug': 1714 | shandler.setLevel(logging.DEBUG) 1715 | elif level == 'error': 1716 | shandler.setLevel(logging.ERROR) 1717 | LOG.addHandler(shandler) 1718 | 1719 | if logfile: 1720 | open(logfile, 'w').close() 1721 | fhandler = logging.FileHandler(logfile) 1722 | fhandler.setLevel(logging.DEBUG) 1723 | formatter = logging.Formatter( 1724 | '%(asctime)s-%(levelname)s: %(message)s') 1725 | fhandler.setFormatter(formatter) 1726 | LOG.addHandler(fhandler) 1727 | 1728 | 1729 | def handle_options(sys_args): 1730 | """Setup option parsing 1731 | """ 1732 | parser = argparse.ArgumentParser(prog='ister') 1733 | parser.add_argument("-c", "--config-file", action="store", 1734 | default=None, 1735 | help="Path to configuration file to use") 1736 | parser.add_argument("-s", "--cert-file", action="store", 1737 | default=None, 1738 | help="Path to certificate file used by swupd") 1739 | parser.add_argument("-t", "--template-file", action="store", 1740 | default=None, 1741 | help="Path to template file to use") 1742 | parser.add_argument("-V", "--versionurl", action="store", 1743 | default=None, 1744 | help="URL to use for looking for update versions") 1745 | parser.add_argument("-C", "--contenturl", action="store", 1746 | default=None, 1747 | help="URL to use for looking for update content") 1748 | parser.add_argument("-f", "--format", action="store", 1749 | default=None, 1750 | help="format to use for looking for update content") 1751 | parser.add_argument("-v", "--verbose", action="store_true", 1752 | help="Output logging to console stream") 1753 | parser.add_argument("-L", "--loglevel", action="store", 1754 | default="info", 1755 | help="loglevel: debug, info, error. default=info") 1756 | parser.add_argument("-l", "--logfile", action="store", 1757 | default="/var/log/ister.log", 1758 | help="Output debug logging to a file") 1759 | parser.add_argument("-k", "--kcmdline", action="store", 1760 | default="/proc/cmdline", 1761 | help="File to inspect for kernel cmdline opts") 1762 | group = parser.add_mutually_exclusive_group(required=False) 1763 | group.add_argument("-S", "--statedir", action="store", 1764 | default="/var/lib/swupd", 1765 | help="Path to swupd state dir") 1766 | group.add_argument("-F", "--fast-install", action="store_true", 1767 | help="Move swupd state dir inside image for a faster install") 1768 | parser.add_argument("-D", "--target-dir", action="store", 1769 | default=None, 1770 | help="Target root directory path, 'mktemp' by default") 1771 | parser.add_argument("-m", "--no-unmount", action="store_true", 1772 | help="Do not unmount the target file-systems when done") 1773 | parser.add_argument("-d", "--dnf-config", action="store", 1774 | default=None, 1775 | help="DNF configuration file for installing packages") 1776 | args = parser.parse_args(sys_args) 1777 | return args 1778 | 1779 | 1780 | def main(): 1781 | """Start the installer 1782 | """ 1783 | global LOG 1784 | args = handle_options(sys.argv[1:]) 1785 | 1786 | LOG = logging.getLogger(__name__) 1787 | handle_logging(args.loglevel, args.logfile) 1788 | 1789 | try: 1790 | configuration = parse_config(args) 1791 | template = get_template(configuration["template"]) 1792 | install_os(args, template) 1793 | except Exception as exep: 1794 | if args.loglevel == "debug": 1795 | traceback.print_exc() 1796 | LOG.error("Failed: {}".format(repr(exep))) 1797 | sys.exit(-1) 1798 | LOG.info("Successful installation") 1799 | sys.exit(0) 1800 | 1801 | 1802 | if __name__ == '__main__': 1803 | main() 1804 | -------------------------------------------------------------------------------- /ister.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=ister graphical installer 3 | 4 | [Service] 5 | Type=oneshot 6 | ExecStart=/bin/sh -c '/usr/bin/python3 /usr/bin/ister_gui.py && /usr/sbin/reboot' 7 | StandardInput=tty 8 | StandardOutput=tty 9 | StandardError=tty 10 | TTYPath=/dev/tty1 11 | 12 | [Install] 13 | WantedBy=multi-user.target 14 | -------------------------------------------------------------------------------- /key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpQIBAAKCAQEAxYu5w4wZjzmtuOHEwcoeLbVeFLjHed8CLEszME7RLqEZS59U 3 | ThHu/bacU9VJEp4qRDvXhTuQ6f68d3ZFrZIbHUzjCiAfuxBfbL90A4o2C4FsOxi1 4 | 2ODySzcoGCyCt3Yea8auBpaO+Uq5z3tWm7iH2MBNnLFutgFXgv/+jOuPw+XAv6rU 5 | ZP7W/JIamxKfhTrwwhDxQXDv9mT9YtKD5DJv1CIT9Y7zlNRjZSsTys1R0WDvbssk 6 | EJlUc+pijNUOLSl3Isu6U5noT1haASn6OfWzKWSm9Zvf7/r/nj8Wd38pYs5eu/lB 7 | +7EVPKGkZ0lGn8EfHyhYcLSVfMoLY+03iPOhkQIDAQABAoIBAQCp8PDm65BVaT4s 8 | rXRxbeoWUk9ULj3UdufMqQipRzSnE4nKR4/j9YOOmdjUOci6Dny35G8cu8iHtE/3 9 | yTRaBDX1N96dKFODvqsIx48LOIwTy8wK7tAJekKWOCXy3d+56hBzkSDGpCDtDr7Y 10 | Yfd40P90lMJvySg/xNm+5XDbVA6Ca5BiXWrZ53/q36qJiMoTxFj7taxYWFVEH1iN 11 | YBlQzeA56W9yQw8G39VkBdxyhDEL5yim1Qykr0pc14oO2NndNcqtVDEuGM9N7kMi 12 | hCbgY5e8a/5+cFcFJRvvfBjto3eXB8xzhjnPu5Vvayp5hinP4Xwlbmdb0pS4wUw/ 13 | 8WpZizFNAoGBAPgaXqO2OSYUpmcUwEZ7MWbsjVQqNEOMRmnmPijheEQd5BYcEPUb 14 | CA3zOPLbtCRk/BPLmwnKYQwUTK76QFXp2HDD8JGCRR0GlxZxyZp/3t2JcDrZmcLx 15 | MeDJX5lfQgJgeUTRjEz2UWVjTSKeQSHoBfYCeadqZbOzDFCa4vks1VDvAoGBAMvV 16 | Zevbnn3z/LtHf+gJcMSMrtFwvXsyImfIn8ktcNH3SN6Wl35IeSR3Glft375/jzA4 17 | u4uZAr0T8hvjiZZZJAJIigYhR4K+Qayuyacb1//uMTWBa8OeVgk84c/F3UGf2h0/ 18 | Q/ZQthPUn4rgjeSHuuNV5Ibncye3wod0x6FWkTV/AoGBAK61swtJ2LiONhfErxly 19 | yvkVfvhTt/YRI8yTDBaxn4UoH2PKY86WOHfKXMH4IMS4MCKocAbW8rwU12MoaoGV 20 | aIsQD6oHuC+WYnK1sigP/5q1m8h1TyfNvTfz1lQkllEiKoNhpJDVq7/fy4OjOW5s 21 | +zWfzJct/2wpm3RvfYHGJnkVAoGAahLWZGQ42YD0H524wU7QYWh4vUN3R7oyT2IH 22 | TREZqhqO0E777VrXuBNHIUUH78HACS8s4huxYiYUE1FY02X2KD4JneEJrs9FrBCV 23 | niIOSQBymU6NfxJR4aLOPGrSlokSX7ABtRgReMZodEQhczDzH8UeFNozghLN5+Hs 24 | 1VgQXw8CgYEA2S2W0XqRhv+rKdZEW8kuvzkkEdlehyy9E0f8J7L7tBqlqeqWNg+J 25 | //FCHRjWXYMm4mcr4xfY1DvL4di+9gO+5At/6zmHTaW0TP0uodWPNryHE8gKr9RK 26 | DLoTcd/1z5wJUv7KuRtT5Y1EuC2FSglPb5BoNIF5GOroXq6P8EPSNpI= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /key.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDFi7nDjBmPOa244cTByh4ttV4UuMd53wIsSzMwTtEuoRlLn1ROEe79tpxT1UkSnipEO9eFO5Dp/rx3dkWtkhsdTOMKIB+7EF9sv3QDijYLgWw7GLXY4PJLNygYLIK3dh5rxq4Glo75SrnPe1abuIfYwE2csW62AVeC//6M64/D5cC/qtRk/tb8khqbEp+FOvDCEPFBcO/2ZP1i0oPkMm/UIhP1jvOU1GNlKxPKzVHRYO9uyyQQmVRz6mKM1Q4tKXciy7pTmehPWFoBKfo59bMpZKb1m9/v+v+ePxZ3fylizl67+UH7sRU8oaRnSUafwR8fKFhwtJV8ygtj7TeI86GR !!!ister-test-key 2 | -------------------------------------------------------------------------------- /live-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "DestinationType" : "virtual", 3 | "PartitionLayout" : [ { "disk" : "live.img", 4 | "partition" : 1, 5 | "size" : "64M", 6 | "type" : "EFI" }, 7 | { "disk" : "live.img", 8 | "partition" : 2, 9 | "size" : "5G", 10 | "type" : "linux" } ], 11 | "FilesystemTypes" : [ { "disk" : "live.img", 12 | "partition" : 1, 13 | "type" : "vfat" }, 14 | { "disk" : "live.img", 15 | "partition" : 2, 16 | "type" : "ext4" } ], 17 | "PartitionMountPoints" : [ { "disk" : "live.img", 18 | "partition" : 1, 19 | "mount" : "/boot" }, 20 | { "disk" : "live.img", 21 | "partition" : 2, 22 | "mount" : "/" } ], 23 | "Version": 6000, 24 | "Bundles": ["kernel-native", 25 | "os-core-update", 26 | "os-core", 27 | "os-utils", 28 | "bootloader"], 29 | "PostNonChroot": ["./live-image-post-update-version.py"] 30 | 31 | } 32 | -------------------------------------------------------------------------------- /live-image-post-update-version.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import os 4 | import sys 5 | 6 | INSTALLER_VERSION = "6000" 7 | 8 | def append_installer_rootwait(path): 9 | """Add a delay to the installer kernel commandline""" 10 | entry_path = path + "/boot/loader/entries/" 11 | entry_file = os.listdir(entry_path) 12 | if len(entry_file) != 1: 13 | raise Exception("Unable to find specific entry file in {0}, " 14 | "found {1} instead".format(entry_path, entry_file)) 15 | file_full_path = entry_path + entry_file[0] 16 | with open(file_full_path, "r") as entry: 17 | entry_content = entry.readlines() 18 | options_line = entry_content[-1] 19 | if not options_line.startswith("options "): 20 | raise Exception("Last line of entry file is not the kernel " 21 | "commandline options") 22 | # Account for newline at the end of the line 23 | options_line = options_line[:-1] + " rootwait\n" 24 | entry_content[-1] = options_line 25 | os.unlink(file_full_path) 26 | with open(file_full_path, "w") as entry: 27 | entry.writelines(entry_content) 28 | 29 | 30 | if __name__ == '__main__': 31 | if len(sys.argv) != 2: 32 | sys.exit(-1) 33 | 34 | try: 35 | append_installer_rootwait(sys.argv[1]) 36 | except Exception as exep: 37 | print(exep) 38 | sys.exit(-1) 39 | sys.exit(0) 40 | -------------------------------------------------------------------------------- /maninstall.expect: -------------------------------------------------------------------------------- 1 | #!/usr/bin/expect -f 2 | # 3 | # This Expect script was generated by autoexpect on Wed Feb 3 18:49:12 2016 4 | # Expect and autoexpect were both written by Don Libes, NIST. 5 | # 6 | # Note that autoexpect does not guarantee a working script. It 7 | # necessarily has to guess about certain things. Two reasons a script 8 | # might fail are: 9 | # 10 | # 1) timing - A surprising number of programs (rn, ksh, zsh, telnet, 11 | # etc.) and devices discard or ignore keystrokes that arrive "too 12 | # quickly" after prompts. If you find your new script hanging up at 13 | # one spot, try adding a short sleep just before the previous send. 14 | # Setting "force_conservative" to 1 (see below) makes Expect do this 15 | # automatically - pausing briefly before sending each character. This 16 | # pacifies every program I know of. The -c flag makes the script do 17 | # this in the first place. The -C flag allows you to define a 18 | # character to toggle this mode off and on. 19 | 20 | set force_conservative 1 ;# set to 1 to force conservative mode even if 21 | ;# script wasn't run conservatively originally 22 | if {$force_conservative} { 23 | set send_slow {1 .1} 24 | proc send {ignore arg} { 25 | sleep .1 26 | exp_send -s -- $arg 27 | } 28 | } 29 | 30 | # 31 | # 2) differing output - Some programs produce different output each time 32 | # they run. The "date" command is an obvious example. Another is 33 | # ftp, if it produces throughput statistics at the end of a file 34 | # transfer. If this causes a problem, delete these patterns or replace 35 | # them with wildcards. An alternative is to use the -p flag (for 36 | # "prompt") which makes Expect only look for the last line of output 37 | # (i.e., the prompt). The -P flag allows you to define a character to 38 | # toggle this mode off and on. 39 | # 40 | # Read the man page for more info. 41 | # 42 | # -Don 43 | 44 | 45 | set timeout -1 46 | # set env(https_proxy) 47 | spawn /usr/bin/python3 /usr/bin/ister_gui.py --exit-after 48 | match_max 100000 49 | expect -re ".*Clear Linux. OS Installer.*" 50 | send -- "\r" 51 | send -- "\t" 52 | expect -re ".*Network Requirements.*" 53 | send -- "\t" 54 | send -- "\t" 55 | send -- "\t" 56 | send -- "\t" 57 | send -- "\t" 58 | send -- "\t" 59 | send -- "\t" 60 | send -- "\t" 61 | send -- "\t" 62 | send -- "\t" 63 | send -- "\t" 64 | send -- "\t" 65 | send -- "\t" 66 | send -- "\r" 67 | expect -re ".*Choose Action.*" 68 | send -- "\r" 69 | # check < previous > button functionality 70 | expect -re ".*Stability Enhancement Program.*" 71 | send -- "\t" 72 | send -- "\t" 73 | send -- "\r" 74 | send -- "\r" 75 | expect -re ".*Stability Enhancement Program.*" 76 | send -- "\t" 77 | send -- "\r" 78 | send -- "\r" 79 | expect -re ".*Choose Installation Type.*" 80 | # tab to manual 81 | send -- "\t" 82 | send -- "\r" 83 | expect -re ".*Choose partitioning method.*" 84 | # use default - not doing cgdisk testing with expect 85 | send -- "\r" 86 | expect -re ".*Choose target device.*" 87 | send -- "\r" 88 | expect -re ".*Warning.*" 89 | # tab to Yes 90 | send -- "\t" 91 | send -- "\r" 92 | expect -re ".*Append to kernel cmdline.*" 93 | send -- "\t" 94 | send -- "\r" 95 | expect -re ".*Configuring Hostname.*" 96 | # accept default 97 | send -- "\r" 98 | # Next 99 | send -- "\r" 100 | expect -re ".*User configuration.*" 101 | # manually create a user 102 | send -- "\r" 103 | send -- "User\r" 104 | send -- "Name\r" 105 | # Username is now uname 106 | send -- "\t" 107 | send -- "UserPass\r" 108 | send -- "UserPass\r" 109 | # Add user to sudoers 110 | send -- "\r" 111 | send -- "\t" 112 | send -- "\r" 113 | expect -re ".*Bundle selector.*" 114 | # editors 115 | send -- "\r" 116 | # tab to Next 117 | send -- "\t" 118 | send -- "\t" 119 | send -- "\t" 120 | send -- "\t" 121 | send -- "\t" 122 | send -- "\t" 123 | # Next 124 | send -- "\r" 125 | expect -re ".*Network configuration.*" 126 | send -- "\t" 127 | # Static IP configuration 128 | send -- "\r" 129 | expect -re ".*Step 12 of 13.*" 130 | # tab through options, don't actually set anything 131 | send -- "\t" 132 | send -- "\t" 133 | send -- "\t" 134 | send -- "\t" 135 | send -- "\t" 136 | send -- "\t" 137 | send -- "\t" 138 | send -- "\t" 139 | # Previous 140 | send -- "\r" 141 | expect -re ".*Step 12 of 13.*" 142 | send -- "\t" 143 | send -- "\t" 144 | # Use DHCP 145 | send -- "\r" 146 | expect -re ".*Attention.*" 147 | send -- "\t" 148 | # Yes 149 | send -- "\r" 150 | expect -re ".*Ok.*" 151 | send -- "\r" 152 | expect -re ".*will be rebooted.*" 153 | send -- "\r" 154 | expect eof 155 | -------------------------------------------------------------------------------- /mbr.json: -------------------------------------------------------------------------------- 1 | { 2 | "DestinationType" : "virtual", 3 | "PartitionLayout" : [ { "disk" : "mbr.img", "partition" : 1, "size" : "20G", "type" : "linux" } ], 4 | "FilesystemTypes" : [ { "disk" : "mbr.img", "partition" : 1, "type" : "ext4" } ], 5 | "PartitionMountPoints" : [ { "disk" : "mbr.img", "partition" : 1, "mount" : "/" } ], 6 | "Version": "latest", 7 | "Bundles": ["kernel-native", "os-core", "os-core-update"], 8 | "LegacyBios": true 9 | } 10 | -------------------------------------------------------------------------------- /min-good.json: -------------------------------------------------------------------------------- 1 | { 2 | "DestinationType": "virtual", 3 | "PartitionLayout": [{"disk": "test.img", "partition": 1, "size": "512M", "type": "EFI"}, 4 | {"disk": "test.img", "partition": 2, "size": "4G", "type": "swap"}, 5 | {"disk": "test.img", "partition": 3, "size": "rest", "type": "linux"}], 6 | "FilesystemTypes": [{"disk": "test.img", "partition": 1, "type": "vfat"}, 7 | {"disk": "test.img", "partition": 2, "type": "swap"}, 8 | {"disk": "test.img", "partition": 3, "type": "ext4"}], 9 | "PartitionMountPoints": [{"disk": "test.img", "partition": 1, "mount": "/boot"}, 10 | {"disk": "test.img", "partition": 3, "mount": "/"}], 11 | "Version": 930, 12 | "Bundles": ["kernel-kvm"] 13 | } 14 | -------------------------------------------------------------------------------- /post-chroot.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | touch /post-chroot 4 | -------------------------------------------------------------------------------- /post-encryption.expect: -------------------------------------------------------------------------------- 1 | #!/usr/bin/expect -f 2 | set ovmf [lindex $argv 0] 3 | spawn sudo qemu-system-x86_64 -enable-kvm -nographic -m 1024 -cpu host -drive file=installer-target.img,if=virtio,aio=threads -net nic,model=virtio -net user,hostfwd=tcp::2233-:22 -smp 2 -bios $ovmf 4 | expect -re ".*Please enter passphrase.*" 5 | send -- "123\r" 6 | expect -re ".*login:.*" 7 | send -- "uname\r" 8 | send -- "UserPass\r" 9 | expect -re "\$$" 10 | send -- "sudo poweroff\r" 11 | expect -re ".*assword:.*" 12 | send -- "UserPass\r" 13 | -------------------------------------------------------------------------------- /post-non-chroot.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cp post-chroot.sh $1/ 4 | -------------------------------------------------------------------------------- /pre-post.json: -------------------------------------------------------------------------------- 1 | { 2 | "DestinationType" : "virtual", 3 | "PartitionLayout" : [ { "disk" : "test.img", "partition" : 1, "size" : "64M", "type" : "EFI" }, 4 | { "disk" : "test.img", "partition" : 2, "size" : "2G", "type" : "linux" } ], 5 | "FilesystemTypes" : [ { "disk" : "test.img", "partition" : 1, "type" : "vfat" }, 6 | { "disk" : "test.img", "partition" : 2, "type" : "ext4" } ], 7 | "PartitionMountPoints" : [ { "disk" : "test.img", "partition" : 1, "mount" : "/boot" }, 8 | { "disk" : "test.img", "partition" : 2, "mount" : "/" } ], 9 | "Version": "latest", 10 | "Bundles": ["os-core"], 11 | "PreInstallShell": ["echo preinstall shell command"], 12 | "PostNonChroot": ["./post-non-chroot.sh"], 13 | "PostNonChrootShell": ["touch $ISTER_CHROOT/post-non-chroot-shell"], 14 | "PostChroot": ["/post-chroot.sh"], 15 | "PostChrootShell": ["touch /post-chroot-shell"] 16 | } 17 | -------------------------------------------------------------------------------- /provision-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "DestinationType" : "virtual", 3 | "PartitionLayout" : [ { "disk" : "provision.img", "partition" : 1, "size" : "64M", "type" : "EFI" }, 4 | { "disk" : "provision.img", "partition" : 2, "size" : "2G", "type" : "linux" } ], 5 | "FilesystemTypes" : [ { "disk" : "provision.img", "partition" : 1, "type" : "vfat" }, 6 | { "disk" : "provision.img", "partition" : 2, "type" : "ext4" } ], 7 | "PartitionMountPoints" : [ { "disk" : "provision.img", "partition" : 1, "mount" : "/boot" }, 8 | { "disk" : "provision.img", "partition" : 2, "mount" : "/" } ], 9 | "Version": 6900, 10 | "Bundles": ["kernel-native", "os-installer", "os-core-update", "os-core", "bootloader"], 11 | "PostNonChroot": ["./provision-image-post-update-version.py"] 12 | } 13 | -------------------------------------------------------------------------------- /provision-image-post-update-version.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import os 4 | import sys 5 | 6 | INSTALLER_VERSION = "6900" 7 | 8 | def create_provision_config(path): 9 | """Create a basicl installation configuration file""" 10 | config = u"template=file:///etc/ister.json\n" 11 | jconfig = u'{"DestinationType" : "physical", "PartitionLayout" : \ 12 | [{"disk" : "sda", "partition" : 1, "size" : "512M", "type" : "EFI"}, \ 13 | {"disk" : "sda", "partition" : 2, \ 14 | "size" : "512M", "type" : "swap"}, {"disk" : "sda", "partition" : 3, \ 15 | "size" : "rest", "type" : "linux"}], \ 16 | "FilesystemTypes" : \ 17 | [{"disk" : "sda", "partition" : 1, "type" : "vfat"}, \ 18 | {"disk" : "sda", "partition" : 2, "type" : "swap"}, \ 19 | {"disk" : "sda", "partition" : 3, "type" : "ext4"}], \ 20 | "PartitionMountPoints" : \ 21 | [{"disk" : "sda", "partition" : 1, "mount" : "/boot"}, \ 22 | {"disk" : "sda", "partition" : 3, "mount" : "/"}], \ 23 | "Version" : 0, "Bundles" : ["kernel-native", "telemetrics", "os-core", "os-core-update"]}\n' 24 | if not os.path.isdir("{}/etc".format(path)): 25 | os.mkdir("{}/etc".format(path)) 26 | with open("{}/etc/ister.conf".format(path), "w") as cfile: 27 | cfile.write(config) 28 | with open("{}/etc/ister.json".format(path), "w") as jfile: 29 | jfile.write(jconfig.replace('"Version" : 0', 30 | '"Version" : ' + INSTALLER_VERSION)) 31 | 32 | 33 | def add_provision_symlink(path): 34 | os.symlink("{}/usr/lib/systemd/system/ister-provision.service" 35 | .format(path), 36 | "{}/usr/lib/systemd/system/multi-user.target.wants/ister-provision.service" 37 | .format(path)) 38 | 39 | 40 | if __name__ == '__main__': 41 | if len(sys.argv) != 2: 42 | sys.exit(-1) 43 | 44 | try: 45 | create_provision_config(sys.argv[1]) 46 | add_provision_symlink(sys.argv[1]) 47 | except Exception as exep: 48 | print(exep) 49 | sys.exit(-1) 50 | sys.exit(0) 51 | -------------------------------------------------------------------------------- /release-image-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "DestinationType" : "virtual", 3 | "PartitionLayout" : [ { "disk" : "release.img", "partition" : 1, "size" : "32M", "type" : "EFI" }, 4 | { "disk" : "release.img", "partition" : 2, "size" : "16M", "type" : "swap" }, 5 | { "disk" : "release.img", "partition" : 3, "size" : "10G", "type" : "linux" } ], 6 | "FilesystemTypes" : [ { "disk" : "release.img", "partition" : 1, "type" : "vfat" }, 7 | { "disk" : "release.img", "partition" : 2, "type" : "swap" }, 8 | { "disk" : "release.img", "partition" : 3, "type" : "ext4" } ], 9 | "PartitionMountPoints" : [ { "disk" : "release.img", "partition" : 1, "mount" : "/boot" }, 10 | { "disk" : "release.img", "partition" : 3, "mount" : "/" } ], 11 | "Version": "latest", 12 | "Bundles": ["kernel-native", "os-core", "os-core-update"] 13 | } 14 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | urwid 2 | netifaces 3 | pycurl 4 | -------------------------------------------------------------------------------- /script.exp: -------------------------------------------------------------------------------- 1 | #!/usr/bin/expect -f 2 | # 3 | # This Expect script was generated by autoexpect on Wed Feb 3 18:49:12 2016 4 | # Expect and autoexpect were both written by Don Libes, NIST. 5 | # 6 | # Note that autoexpect does not guarantee a working script. It 7 | # necessarily has to guess about certain things. Two reasons a script 8 | # might fail are: 9 | # 10 | # 1) timing - A surprising number of programs (rn, ksh, zsh, telnet, 11 | # etc.) and devices discard or ignore keystrokes that arrive "too 12 | # quickly" after prompts. If you find your new script hanging up at 13 | # one spot, try adding a short sleep just before the previous send. 14 | # Setting "force_conservative" to 1 (see below) makes Expect do this 15 | # automatically - pausing briefly before sending each character. This 16 | # pacifies every program I know of. The -c flag makes the script do 17 | # this in the first place. The -C flag allows you to define a 18 | # character to toggle this mode off and on. 19 | 20 | set force_conservative 1 ;# set to 1 to force conservative mode even if 21 | ;# script wasn't run conservatively originally 22 | if {$force_conservative} { 23 | set send_slow {1 .1} 24 | proc send {ignore arg} { 25 | sleep .1 26 | exp_send -s -- $arg 27 | } 28 | } 29 | 30 | # 31 | # 2) differing output - Some programs produce different output each time 32 | # they run. The "date" command is an obvious example. Another is 33 | # ftp, if it produces throughput statistics at the end of a file 34 | # transfer. If this causes a problem, delete these patterns or replace 35 | # them with wildcards. An alternative is to use the -p flag (for 36 | # "prompt") which makes Expect only look for the last line of output 37 | # (i.e., the prompt). The -P flag allows you to define a character to 38 | # toggle this mode off and on. 39 | # 40 | # Read the man page for more info. 41 | # 42 | # -Don 43 | 44 | set timeout -1 45 | spawn python3 ister_gui.py 46 | match_max 100000 47 | expect { 48 | -re ".*Choose Installation Type.*" { 49 | send -- "\[B" 50 | send -- "\r" 51 | exp_continue 52 | } 53 | -re ".*Do you want to handle the partitions.*" { 54 | send -- "\[C" 55 | send -- "\r" 56 | exp_continue 57 | } 58 | -re ".*Configuring Hostname.*" { 59 | send -- "\[B" 60 | send -- "\[C" 61 | send -- "\r" 62 | exp_continue 63 | } 64 | -re ".*Bundle selector.*" { 65 | send -- "\[B" 66 | send -- "\[B" 67 | send -- "\[B" 68 | send -- "\[B" 69 | send -- "\[B" 70 | send -- "\[B" 71 | send -- "\[C" 72 | send -- "\r" 73 | exp_continue 74 | } 75 | -re ".*Do you want to configure a new user?.*" { 76 | send -- "\[C" 77 | send -- "\r" 78 | exp_continue 79 | } 80 | -re ".*Do you want to use DHCP?.*" { 81 | send -- "\[C" 82 | send -- "\[C" 83 | send -- "\r" 84 | exp_continue 85 | } 86 | -re ".*< Ok >.*" { 87 | send -- "\r" 88 | expect { 89 | -re ".*< Ok >.*" { 90 | send -- "\r" 91 | log_file expect.log 92 | send_log -- "Log installation failed" 93 | } 94 | -re ".*Successful installation.*" { 95 | send -- "\r" 96 | log_file expect.log 97 | send_log -- "Log installation successful" 98 | } 99 | } 100 | } 101 | } 102 | expect eof 103 | -------------------------------------------------------------------------------- /spinup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | x(){ 6 | echo -- "$@" >&2 7 | "$@" 8 | } 9 | 10 | runinst(){ 11 | qemu-system-x86_64 -enable-kvm -m 1024 -vnc 0.0.0.0:0 -cpu host \ 12 | -drive file=installer.img,if=virtio,aio=threads -net nic,model=virtio \ 13 | -drive file=installer-target.img,if=virtio,aio=threads \ 14 | -net user,hostfwd=tcp::$1-:22 -smp 2 -bios ./OVMF.fd & 15 | } 16 | 17 | newtarget(){ 18 | x rm -f installer-target.img 19 | x qemu-img create installer-target.img 10G 20 | } 21 | 22 | if [[ -z $1 ]]; then 23 | port="2233" 24 | else 25 | port=$1 26 | fi 27 | 28 | if [[ ! -f ./installer-target.img ]]; then 29 | echo "Creating new installer target" 30 | newtarget 31 | fi 32 | echo Using port $port 33 | runinst $port 34 | bg_pid=$! 35 | sleep 1 36 | vncviewer 0.0.0.0 37 | sudo kill $bg_pid 38 | 39 | -------------------------------------------------------------------------------- /test.json: -------------------------------------------------------------------------------- 1 | {"test": 1} 2 | -------------------------------------------------------------------------------- /update_gui_expect.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | # usage: update_gui_expect.sh 3 | 4 | function usage() 5 | { 6 | echo "Usage: $0 " 7 | exit 1 8 | } 9 | 10 | if [ $# -ne 3 ] 11 | then 12 | usage 13 | fi 14 | 15 | if [ $1 == "-h" ] 16 | then 17 | usage 18 | fi 19 | 20 | if [ ! -e $1 ] 21 | then 22 | echo $1: Does not exist 23 | exit 1 24 | fi 25 | 26 | if [ ! -e $2 ] 27 | then 28 | echo $2: Does not exist 29 | exit 1 30 | fi 31 | 32 | if [ ! -e $3 ] 33 | then 34 | echo $3: Does not exist 35 | exit 1 36 | fi 37 | 38 | mnt=$(/usr/bin/mktemp -d) 39 | next_dev=$(sudo losetup -f --show -P $1) 40 | sudo /usr/bin/mount ${next_dev}p2 $mnt 41 | sudo cp ister_gui.py ${mnt}/usr/bin/ister_gui.py 42 | sudo cp ister.py ${mnt}/usr/bin/ister.py 43 | sudo cp $2 ${mnt}/usr/bin 44 | sudo cp $3 ${mnt}/usr/lib/systemd/system/ister.service 45 | sudo /usr/bin/umount $mnt 46 | sudo /usr/bin/losetup -D 47 | echo "$1 set to use expect to drive install" 48 | -------------------------------------------------------------------------------- /update_installer.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | # usage: update_installer.sh 3 | 4 | function usage() 5 | { 6 | echo "Usage: $0 " 7 | exit 1 8 | } 9 | 10 | if [ $# -ne 1 ] 11 | then 12 | usage 13 | fi 14 | 15 | if [ $1 == "-h" ] 16 | then 17 | usage 18 | fi 19 | 20 | if [ ! -e $1 ] 21 | then 22 | echo $1: Does not exist 23 | exit 1 24 | fi 25 | 26 | mnt=$(/usr/bin/mktemp -d) 27 | next_dev=$(sudo losetup -f --show -P $1) 28 | sudo /usr/bin/mount ${next_dev}p3 $mnt 29 | sudo cp ister_gui.py ${mnt}/usr/bin/ister_gui.py 30 | sudo cp ister.py ${mnt}/usr/bin/ister.py 31 | sync 32 | #read -p "Enter to umount" 33 | sudo /usr/bin/umount $mnt 34 | sudo /usr/bin/losetup -D 35 | echo "$1 up to date with latest gui and installer" 36 | -------------------------------------------------------------------------------- /update_usb.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | # usage: update_installer.sh 3 | 4 | function usage() 5 | { 6 | echo "Usage: $0 " 7 | exit 1 8 | } 9 | 10 | function run_cmd() 11 | { 12 | ${@} 13 | status=$? 14 | if [ ${status} -ne 0 ] 15 | then 16 | echo "Error ${status}: ${@}" 17 | exit ${status} 18 | fi 19 | } 20 | 21 | 22 | if [ $# -ne 1 ] 23 | then 24 | usage 25 | fi 26 | 27 | if [ $1 == "-h" ] 28 | then 29 | usage 30 | fi 31 | 32 | if [ ! -e $1 ] 33 | then 34 | echo $1: Does not exist 35 | exit 1 36 | fi 37 | 38 | mnt=$(/usr/bin/mktemp -d) 39 | run_cmd "sudo /usr/bin/mount ${1}3 $mnt" 40 | run_cmd "sudo cp ister_gui.py ${mnt}/usr/bin/ister_gui.py" 41 | run_cmd "sudo cp ister.py ${mnt}/usr/bin/ister.py" 42 | run_cmd "sync" 43 | #read -p "Enter to umount" 44 | run_cmd "sudo /usr/bin/umount $mnt" 45 | run_cmd "sudo eject $1" 46 | echo "$1 up to date with latest gui and installer" 47 | -------------------------------------------------------------------------------- /usr.conf: -------------------------------------------------------------------------------- 1 | template=file:///usr.json 2 | -------------------------------------------------------------------------------- /validate_release.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import argparse 4 | import ctypes 5 | import json 6 | import os 7 | import pwd 8 | import subprocess 9 | import sys 10 | import tempfile 11 | import time 12 | import urllib.request as request 13 | 14 | def handle_options(): 15 | """Setup option parsing 16 | """ 17 | parser = argparse.ArgumentParser() 18 | parser.add_argument('-v', '--verbose', action='store_true', default=None, 19 | help='More verbose output.') 20 | parser.add_argument('-b' , '--bios', action='store', default=None, 21 | help='Use specified bios file for qemu') 22 | parser.add_argument('-V' , '--vnc', action='store', default=0, 23 | help='Use specified vnc port number') 24 | args = parser.parse_args() 25 | 26 | if args.bios is None: 27 | print("Error: -b|--bios require") 28 | # print("{0}").format(parser.usage) 29 | sys.exit(1) 30 | 31 | return args 32 | 33 | 34 | def spin_up_installer(): 35 | print(">>> Spinning new installer into ./installer-val.img") 36 | if os.path.isfile('./installer-val.img'): 37 | os.remove('./installer-val.img') 38 | cp = subprocess.run(['qemu-img', 'create', 'installer-val.img', '2G']) 39 | cp = subprocess.run(['sudo', 'python3', 'ister.py', '-t', 40 | 'installer-config-vm.json']) 41 | cp.check_returncode() 42 | 43 | 44 | def validate_installer(args, expectfile, unitfile, alternative_check=None): 45 | print(">>> Configuring installer-val.img to be driven by expect using new ister and gui") 46 | cp = subprocess.run(['sudo', './update_gui_expect.sh', 'installer-val.img', 47 | expectfile, unitfile]) 48 | cp.check_returncode() 49 | 50 | print(">>> Create target image for install") 51 | if os.path.isfile('installer-target.img'): 52 | os.remove('./installer-target.img') 53 | cp = subprocess.run(['qemu-img', 'create', 'installer-target.img', '10G']) 54 | cp.check_returncode() 55 | 56 | print(">>> Booting installer-val.img against installer-target.img") 57 | cp = subprocess.run(['sudo', 'qemu-system-x86_64', '-enable-kvm', '-m', 58 | '1024', '-vnc', '0.0.0.0:{}'.format(args.vnc), '-cpu', 'host', '-drive', 59 | 'file=installer-target.img,if=virtio,aio=threads', 60 | '-drive', 'file=installer-val.img,if=virtio,aio=threads', 61 | '-net', 'nic,model=virtio', '-net', 62 | 'user,hostfwd=tcp::2233-:22', '-smp', '2', 63 | '-bios', args.bios]) 64 | cp.check_returncode() 65 | 66 | if not alternative_check: 67 | print(">>> Installing boot canary into installer-target.mg") 68 | cp = subprocess.run(['sudo', './install-canary.sh', 'installer-target.img']) 69 | cp.check_returncode() 70 | 71 | print(">>> Booting installer-target.img") 72 | cp = subprocess.run(['sudo', 'qemu-system-x86_64', '-enable-kvm', '-m', 73 | '1024', '-vnc', '0.0.0.0:{}'.format(args.vnc), '-cpu', 'host', '-drive', 74 | 'file=installer-target.img,if=virtio,aio=threads', 75 | '-net', 'nic,model=virtio', '-net', 76 | 'user,hostfwd=tcp::2233-:22', '-smp', '2', 77 | '-bios', args.bios]) 78 | cp.check_returncode() 79 | 80 | # Check for boot canary 81 | cp = subprocess.run(['sudo', './check-canary.sh', 'installer-target.img']) 82 | 83 | status_code = cp.returncode 84 | else: 85 | status_code = alternative_check(args) 86 | 87 | if status_code == 0: 88 | status = ">>> SUCCESS! Boot Canary detected!" 89 | else: 90 | status = ">>> Failure: installer-target.img failed to boot" 91 | 92 | print(status) 93 | return status 94 | 95 | def encryption_validation(args): 96 | cp = subprocess.run(['sudo', './post-encryption.expect', args.bios]) 97 | cp.check_returncode() 98 | return cp.returncode 99 | 100 | def main(): 101 | """Start the installer 102 | """ 103 | args = handle_options() 104 | try: 105 | spin_up_installer() 106 | autostatus = validate_installer(args, 'autoinstall.expect', 'ister-expect.service') 107 | manstatus = validate_installer(args, 'maninstall.expect', 'ister-manexpect.service') 108 | encryptstatus = validate_installer(args, 'encryption.expect', 'ister-encryption.service', encryption_validation) 109 | print('\nAutomatic Installation', autostatus) 110 | print('Manual Installation ', manstatus) 111 | print('Manual Installation root Encrypted ', encryptstatus) 112 | except Exception as exep: 113 | print("Failed: {}".format(exep)) 114 | sys.exit(-1) 115 | 116 | sys.exit(0) 117 | 118 | if __name__ == '__main__': 119 | main() 120 | 121 | -------------------------------------------------------------------------------- /vm-installation-image-post-update-version.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import os 4 | import sys 5 | 6 | INSTALLER_VERSION = '"latest"' 7 | 8 | def create_installer_config(path): 9 | """Create a basicl installation configuration file""" 10 | config = u"template=file:///etc/ister.json\n" 11 | jconfig = u'{"DestinationType" : "physical", "PartitionLayout" : \ 12 | [{"disk" : "vda", "partition" : 1, "size" : "512M", "type" : "EFI"}, \ 13 | {"disk" : "vda", "partition" : 2, \ 14 | "size" : "512M", "type" : "swap"}, {"disk" : "vda", "partition" : 3, \ 15 | "size" : "rest", "type" : "linux"}], \ 16 | "FilesystemTypes" : \ 17 | [{"disk" : "vda", "partition" : 1, "type" : "vfat"}, \ 18 | {"disk" : "vda", "partition" : 2, "type" : "swap"}, \ 19 | {"disk" : "vda", "partition" : 3, "type" : "ext4"}], \ 20 | "PartitionMountPoints" : \ 21 | [{"disk" : "vda", "partition" : 1, "mount" : "/boot"}, \ 22 | {"disk" : "vda", "partition" : 3, "mount" : "/"}], \ 23 | "Version" : 0, "Bundles" : ["kernel-native", "telemetrics", "os-core", "os-core-update"]}\n' 24 | if not os.path.isdir("{}/etc".format(path)): 25 | os.mkdir("{}/etc".format(path)) 26 | with open("{}/etc/ister.conf".format(path), "w") as cfile: 27 | cfile.write(config) 28 | with open("{}/etc/ister.json".format(path), "w") as jfile: 29 | jfile.write(jconfig.replace('"Version" : 0', 30 | '"Version" : ' + INSTALLER_VERSION)) 31 | 32 | 33 | def append_installer_rootwait(path): 34 | """Add a delay to the installer kernel commandline""" 35 | entry_path = path + "/boot/loader/entries/" 36 | entry_file = os.listdir(entry_path) 37 | if len(entry_file) != 1: 38 | raise Exception("Unable to find specific entry file in {0}, " 39 | "found {1} instead".format(entry_path, entry_file)) 40 | file_full_path = entry_path + entry_file[0] 41 | with open(file_full_path, "r") as entry: 42 | entry_content = entry.readlines() 43 | options_line = entry_content[-1] 44 | if not options_line.startswith("options "): 45 | raise Exception("Last line of entry file is not the kernel " 46 | "commandline options") 47 | # Account for newline at the end of the line 48 | options_line = options_line[:-1] + " rootwait\n" 49 | entry_content[-1] = options_line 50 | os.unlink(file_full_path) 51 | with open(file_full_path, "w") as entry: 52 | entry.writelines(entry_content) 53 | 54 | 55 | def disable_tty1_getty(path): 56 | """Add a symlink masking the systemd tty1 generator""" 57 | os.makedirs(path + "/etc/systemd/system/getty.target.wants") 58 | os.symlink("/dev/null", path + "/etc/systemd/system/getty.target.wants/getty@tty1.service") 59 | 60 | 61 | def add_installer_service(path): 62 | os.symlink("{}/usr/lib/systemd/system/ister.service" 63 | .format(path), 64 | "{}/usr/lib/systemd/system/multi-user.target.wants/ister.service" 65 | .format(path)) 66 | 67 | 68 | if __name__ == '__main__': 69 | if len(sys.argv) != 2: 70 | sys.exit(-1) 71 | 72 | try: 73 | create_installer_config(sys.argv[1]) 74 | append_installer_rootwait(sys.argv[1]) 75 | disable_tty1_getty(sys.argv[1]) 76 | add_installer_service(sys.argv[1]) 77 | except Exception as exep: 78 | print(exep) 79 | sys.exit(-1) 80 | sys.exit(0) 81 | --------------------------------------------------------------------------------