├── .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 |
--------------------------------------------------------------------------------