]> git.ipfire.org Git - people/ms/bricklayer.git/blob - src/python/__init__.py
2dcceb201b6d3bd81d75cf1f0a5322222fa80e75
[people/ms/bricklayer.git] / src / python / __init__.py
1 ###############################################################################
2 # #
3 # Bricklayer - An Installer for IPFire #
4 # Copyright (C) 2021 IPFire Development Team #
5 # #
6 # This program is free software; you can redistribute it and/or #
7 # modify it under the terms of the GNU General Public License #
8 # as published by the Free Software Foundation; either version 2 #
9 # of the License, or (at your option) any later version. #
10 # #
11 # This program is distributed in the hope that it will be useful, #
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of #
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
14 # GNU General Public License for more details. #
15 # #
16 # You should have received a copy of the GNU General Public License #
17 # along with this program. If not, see <http://www.gnu.org/licenses/>. #
18 # #
19 ###############################################################################
20
21 import logging
22 import subprocess
23 import sys
24 import tempfile
25 import traceback
26
27 import pakfire
28
29 from . import bootloaders
30 from . import disk
31 from . import i18n
32 from . import logger
33 from . import packages
34 from . import step
35 from . import timezones
36 from . import tui
37 from . import util
38 from .i18n import _
39 from .errors import *
40
41 # Setup logging
42 log = logging.getLogger("bricklayer")
43
44 class Bricklayer(object):
45 """
46 Bricklayer's base class
47 """
48 def __init__(self, arch, pakfire_conf=None, first_install=False, debug=False,
49 unattended=False, disks=[]):
50 self.arch = arch
51 self.pakfire_conf = pakfire_conf
52 self.first_install = first_install
53 self.debug = debug
54 self.unattended = unattended
55
56 # Enable debug logging
57 if debug:
58 log.setLevel(logging.DEBUG)
59
60 # Settings
61 self.settings = {
62 "language" : i18n.default_language,
63
64 "packages" : [
65 # Install the Base group
66 "@Base",
67 ],
68
69 # Set the default swap size to 1 GiB
70 "swap-size": 1024 ** 3,
71
72 # Default timezone
73 "timezone" : "UTC",
74 }
75
76 # Read OS information
77 self.os = self._read_os_release()
78
79 # Select a bootloaders
80 self.bootloaders = self._select_bootloaders()
81
82 # Hardware
83 self.disks = disk.Disks(self)
84 for path in disks:
85 self.disks.add_disk(path)
86
87 # Initialise the text user interface
88 self.tui = tui.Tui(self)
89
90 # Create root directory
91 self.root = tempfile.mkdtemp(prefix="bricklayer-", suffix="-root")
92
93 # Log when we are ready
94 log.info("Bricklayer initialized")
95
96 # An ordered list of all available steps
97 steps = (
98 step.UnattendedWarning,
99 step.Welcome,
100 timezones.SelectTimezone,
101 disk.SelectDisk,
102 disk.CalculatePartitionLayout,
103 step.RootPassword,
104
105 # Go!
106 disk.CreatePartitionLayout,
107 disk.CreateFilesystems,
108 disk.MountFilesystems,
109 packages.InstallPackages,
110 bootloaders.InstallBootloader,
111
112 # Done!
113 step.ToggleFirstInstallStatus,
114 disk.UmountFilesystems,
115 step.Congratulations,
116 )
117
118 def __call__(self):
119 with self.tui:
120 try:
121 # Walk through all steps
122 for step in self.steps:
123 try:
124 self._run_step(step)
125
126 # End installer if aborted
127 except InstallAbortedError:
128 return 1
129
130 # The user requested to cancel the installation process
131 except UserCanceledError:
132 if self.tui.confirm(
133 _("Cancel Installation?"),
134 _("Are you sure that you want to cancel the installation process?"),
135 ):
136 return 0
137
138 # Catch any failed commands
139 except subprocess.CalledProcessError as e:
140 args = {
141 "command" : " ".join(e.cmd),
142 "output" : e.output.decode(),
143 "returncode" : e.returncode,
144 }
145
146 # Log the error
147 log.error("Command \"%(command)s\" failed with error code "
148 "%(returncode)s:\n%(output)s" % args)
149
150 # Format the error message
151 error = _("Command \"%(command)s\" failed with error code "
152 "%(returncode)s:\n\n%(output)s" % args)
153
154 # Show it
155 self.tui.error(_("An Unexpected Error Occured"), error,
156 buttons=[_("Exit")], width=78)
157
158 # Exit
159 return e.returncode
160
161 # Catch all other exceptions and show an error
162 except:
163 type, value, tb = sys.exc_info()
164
165 # Log the error
166 log.error("An unexpected error occured:", exc_info=True)
167
168 # Format the exception
169 error = _("The installation cannot be continued due to an error:"
170 "\n\n%s") % "".join(traceback.format_exception(type, value, tb))
171
172 # Show an error message
173 self.tui.error(_("An Unexpected Error Occured"),
174 "".join(error), buttons=[_("Exit")], width=78)
175
176 # Exit
177 return 1
178
179 # Cleanup when we leave
180 finally:
181 self.disks.tear_down()
182
183 def _run_step(self, stepcls):
184 """
185 Runs a single step
186 """
187 # Initialize the step
188 step = stepcls(self, tui=self.tui)
189
190 # Skip this step if it isn't enabled in first install mode
191 if self.first_install and not step.first_install:
192 return
193
194 # Skip this step if it isn't enabled
195 if not step.enabled:
196 return
197
198 # Run it
199 return step.run()
200
201 def _read_os_release(self):
202 """
203 Read /etc/os-release
204 """
205 with open("/etc/os-release") as f:
206 return util.config_read(f)
207
208 def command(self, command, error_ok=False, interactive=False):
209 """
210 Runs a command in a shell environment
211 """
212 log.debug("Running command: %s" % " ".join(command))
213
214 args = {}
215
216 if not interactive:
217 args.update({
218 "stdout" : subprocess.PIPE,
219 "stderr" : subprocess.STDOUT,
220 })
221
222 # Execute the command
223 p = subprocess.run(command, **args)
224
225 # Check the return code (raises CalledProcessError on non-zero)
226 if not error_ok:
227 p.check_returncode()
228
229 # There is no output in interactive mode
230 if interactive:
231 return
232
233 # Decode output
234 output = p.stdout.decode()
235
236 # Log output
237 if output:
238 log.debug(output)
239
240 return output
241
242 def setup_pakfire(self, **kwargs):
243 """
244 Calls Pakfire and has it load its configuration
245 """
246 kwargs |= {
247 "confirm_callback" : self.tui._confirm,
248 }
249
250 return pakfire.Pakfire(self.root, arch=self.arch,
251 conf=self.pakfire_conf, **kwargs)
252
253 def _select_bootloaders(self):
254 """
255 Select all bootloaders for this installation process
256 """
257 _bootloaders = (
258 bootloaders.Grub(self),
259 bootloaders.GrubEFI(self),
260 )
261
262 # Return all supported bootloaders
263 return [bl for bl in _bootloaders if bl.supported]