Browse code

installer: reorganize entry point

Installer engine consist of two pieces (stages):
1) installer.configure(install_config, ui_config)
- install_config is a KS input config as well as 'installer'
portion of image builder config
if 'install_config' is None then configure will trigger
interactive ncurses configurator
- ui_config is a config for interactive ncurses configurator.
Can be used to control what screens to show.
No actions allowed toward target system (disk or image) during
configure stage.

2) installer.execute(install_config)
- takes install_config and "execute" it. All actions on target
system are performed here.

Installer can be run as an app, like ./isoInstaller

In this case control flow looks like:
isoInstaller
<detect KS from kernel cmdline> - no luck
installer.configure(None, ui_config)
ui_config - run ncurses interractive installer to
construct 'install_config'
<perform sanity checks of install_config>
installer.execute(install_config)

When isoInstaller detects KS file - control flow is like:
isoInstaller
<detect KS from kernel cmdline> - success - create 'install_config'
installer.configure(install_config, None)
<perform sanity checks of install_config>
installer.execute(install_config)

When installer called from image builder it is similar to KS case:
image builder
<parses its config file> - creates install_config out of
'installer' section
installer.configure(install_config, None)
<perform sanity checks of install_config>
installer.execute(install_config)
<performs post actions>
<convert raw disk to some output format>

Changed images config file to move installer specific
changes to 'installer' subsection

Deprecate install_config fields: iso_installer, ui_install,
additional-files.

Unified password related logic in installer and removed it
from image builder.

Added ks_config.txt file - documentation for ks file format.

NOTE: this commit can break ostree, as it was not tested.

Change-Id: I3f718b6793c7f0befeb63df4d7e99072cf4d7693
Reviewed-on: http://photon-jenkins.eng.vmware.com:8082/8127
Tested-by: gerrit-photon <photon-checkins@vmware.com>
Reviewed-by: Alexey Makhalov <amakhalov@vmware.com>

Alexey Makhalov authored on 2019/10/01 12:02:11
Showing 19 changed files
... ...
@@ -163,6 +163,7 @@ packages: check-docker-py check-tools photon-stage $(PHOTON_PUBLISH_XRPMS) $(PHO
163 163
 		$(PUBLISH_BUILD_DEPENDENCIES) \
164 164
 		$(PACKAGE_WEIGHTS) \
165 165
 		--threads ${THREADS}
166
+	$(PHOTON_REPO_TOOL) $(PHOTON_RPMS_DIR)
166 167
 
167 168
 packages-docker: check-docker-py check-docker-service check-tools photon-stage $(PHOTON_PUBLISH_XRPMS) $(PHOTON_PUBLISH_RPMS) $(PHOTON_SOURCES) $(CONTAIN) generate-dep-lists
168 169
 	@echo "Building all RPMS..."
... ...
@@ -438,6 +439,7 @@ packages-cached:
438 438
 	$(RM) -f $(PHOTON_RPMS_DIR_ARCH)/* && \
439 439
 	$(CP) -f $(PHOTON_CACHE_PATH)/RPMS/noarch/* $(PHOTON_RPMS_DIR_NOARCH)/ && \
440 440
 	$(CP) -f $(PHOTON_CACHE_PATH)/RPMS/$(ARCH)/* $(PHOTON_RPMS_DIR_ARCH)/
441
+	$(PHOTON_REPO_TOOL) $(PHOTON_RPMS_DIR)
441 442
 
442 443
 sources:
443 444
 	@$(MKDIR) -p $(PHOTON_SRCS_DIR)
... ...
@@ -1,6 +1,9 @@
1 1
 # pylint: disable=invalid-name,missing-docstring
2 2
 import subprocess
3 3
 import os
4
+import crypt
5
+import string
6
+import random
4 7
 
5 8
 class CommandUtils(object):
6 9
     def __init__(self, logger):
... ...
@@ -16,3 +19,21 @@ class CommandUtils(object):
16 16
             self.logger.debug(err.decode())
17 17
         return retval
18 18
 
19
+    @staticmethod
20
+    def is_vmware_virtualization():
21
+        """Detect vmware vm"""
22
+        process = subprocess.Popen(['systemd-detect-virt'], stdout=subprocess.PIPE)
23
+        out, err = process.communicate()
24
+        if err is not None and err != 0:
25
+            return False
26
+        return out.decode() == 'vmware\n'
27
+
28
+    @staticmethod
29
+    def generate_password_hash(password):
30
+        """Generate hash for the password"""
31
+        shadow_password = crypt.crypt(
32
+            password, "$6$" + "".join(
33
+                [random.choice(
34
+                    string.ascii_letters + string.digits) for _ in range(16)]))
35
+        return shadow_password
36
+
... ...
@@ -12,6 +12,7 @@ import sys
12 12
 import glob
13 13
 import re
14 14
 import modules.commons
15
+import random
15 16
 from logger import Logger
16 17
 from commandutils import CommandUtils
17 18
 from jsonwrapper import JsonWrapper
... ...
@@ -23,34 +24,51 @@ class Installer(object):
23 23
     """
24 24
     Photon installer
25 25
     """
26
+
27
+    # List of allowed keys in kickstart config file.
28
+    # Please keep ks_config.txt file updated.
29
+    known_keys = {
30
+        'additional_packages',
31
+        'autopartition',
32
+        'bootmode',
33
+        'disk',
34
+        'eject_cdrom',
35
+        'hostname',
36
+        'install_linux_esx',
37
+        'packages',
38
+        'packagelist_file',
39
+        'partition_type',
40
+        'partitions',
41
+        'password',
42
+        'postinstall',
43
+        'public_key',
44
+        'setup_grub_script',
45
+        'shadow_password',
46
+        'type'
47
+    }
48
+
26 49
     mount_command = os.path.dirname(__file__)+"/mk-mount-disk.sh"
50
+    prepare_command = os.path.dirname(__file__)+"/mk-prepare-system.sh"
27 51
     finalize_command = "./mk-finalize-system.sh"
28 52
     chroot_command = os.path.dirname(__file__)+"/mk-run-chroot.sh"
29 53
     unmount_disk_command = os.path.dirname(__file__)+"/mk-unmount-disk.sh"
30 54
     default_partitions = [{"mountpoint": "/", "size": 0, "filesystem": "ext4"}]
31 55
 
32
-    def __init__(self, install_config, maxy=0, maxx=0, iso_installer=False,
56
+    def __init__(self, working_directory="/mnt/photon-root", maxy=0, maxx=0, iso_installer=False,
33 57
                  rpm_path=os.path.dirname(__file__)+"/../stage/RPMS", log_path=os.path.dirname(__file__)+"/../stage/LOGS", log_level="info"):
34 58
         self.exiting = False
35
-        self.install_config = install_config
36
-        self.install_config['iso_installer'] = iso_installer
59
+        self.interactive = False
60
+        self.install_config = None
61
+        self.iso_installer = iso_installer
37 62
         self.rpm_path = rpm_path
38 63
         self.logger = Logger.get_logger(log_path, log_level, not iso_installer)
39 64
         self.cmd = CommandUtils(self.logger)
65
+        self.working_directory = working_directory
40 66
 
41
-        if 'working_directory' in self.install_config:
42
-            self.working_directory = self.install_config['working_directory']
43
-        else:
44
-            self.working_directory = "/mnt/photon-root"
45
-
46
-        if os.path.exists(self.working_directory) and os.path.isdir(self.working_directory):
67
+        if os.path.exists(self.working_directory) and os.path.isdir(self.working_directory) and working_directory == '/mnt/photon-root':
47 68
             shutil.rmtree(self.working_directory)
48
-        os.mkdir(self.working_directory)
49
-
50
-        if 'prepare_script' in self.install_config:
51
-            self.prepare_command = self.install_config['prepare_script']
52
-        else:
53
-            self.prepare_command = os.path.dirname(__file__)+"/mk-prepare-system.sh"
69
+        if not os.path.exists(self.working_directory):
70
+            os.mkdir(self.working_directory)
54 71
 
55 72
         self.photon_root = self.working_directory + "/photon-chroot"
56 73
         self.installer_path = os.path.dirname(os.path.abspath(__file__))
... ...
@@ -60,13 +78,13 @@ class Installer(object):
60 60
         # used by tdnf.conf as cachedir=, tdnf will append the rest
61 61
         self.rpm_cache_dir_short = self.photon_root + '/cache/tdnf'
62 62
 
63
-        if 'setup_grub_script' in self.install_config:
64
-            self.setup_grub_command = self.install_config['setup_grub_script']
65
-        else:
66
-            self.setup_grub_command = os.path.dirname(__file__)+"/mk-setup-grub.sh"
63
+        self.setup_grub_command = os.path.dirname(__file__)+"/mk-setup-grub.sh"
64
+
67 65
         self.rpms_tobeinstalled = None
68 66
 
69
-        if self.install_config['iso_installer']:
67
+        if iso_installer:
68
+            self.maxy = maxy
69
+            self.maxx = maxx
70 70
             #initializing windows
71 71
             height = 10
72 72
             width = 75
... ...
@@ -83,11 +101,102 @@ class Installer(object):
83 83
 
84 84
         signal.signal(signal.SIGINT, self.exit_gracefully)
85 85
 
86
-    def install(self):
86
+    """
87
+    create, append and validate configuration date - install_config
88
+    """
89
+    def configure(self, install_config, ui_config = None):
90
+        # run UI configurator iff install_config param is None
91
+        if not install_config and ui_config:
92
+            from iso_config import IsoConfig
93
+            self.interactive = True
94
+            config = IsoConfig(self.maxy, self.maxx)
95
+            install_config = config.configure(ui_config)
96
+
97
+        self._add_defaults(install_config)
98
+
99
+        issue = self._check_install_config(install_config)
100
+        if issue:
101
+            self.logger.error(issue)
102
+            raise Exception(issue)
103
+
104
+        self.install_config = install_config
105
+
106
+
107
+    def execute(self):
108
+        if 'setup_grub_script' in self.install_config:
109
+            self.setup_grub_command = self.install_config['setup_grub_script']
110
+
111
+        if self.install_config.get('type', '') == "ostree_host":
112
+            ostree = OstreeInstaller(self.install_config, self.maxy, self.maxx, self.interactive,
113
+                              self.iso_installer, self.rpm_path, self.log_path, self.log_level)
114
+            return ostree.install()
115
+
116
+        return self._install()
117
+
118
+    def _add_defaults(self, install_config):
119
+        """
120
+        Add default install_config settings if not specified
121
+        """
122
+        # extend 'packages' by 'packagelist_file' and 'additional_packages'
123
+        packages = []
124
+        if 'packagelist_file' in install_config:
125
+            plf = install_config['packagelist_file']
126
+            if not plf.startswith('/'):
127
+                plf = os.path.join(os.path.dirname(__file__), plf)
128
+            json_wrapper_package_list = JsonWrapper(plf)
129
+            package_list_json = json_wrapper_package_list.read()
130
+            packages.extend(package_list_json["packages"])
131
+
132
+        if 'additional_packages' in install_config:
133
+            packages.extend(install_config['additional_packages'])
134
+
135
+        if 'packages' in install_config:
136
+            install_config['packages'] = list(set(packages + install_config['packages']))
137
+        else:
138
+            install_config['packages'] = packages
139
+
140
+        # 'bootmode' mode
141
+        if 'bootmode' not in install_config:
142
+            arch = subprocess.check_output(['uname', '-m'], universal_newlines=True)
143
+            if "x86_64" in arch:
144
+                install_config['bootmode'] = 'dualboot'
145
+            else:
146
+                install_config['bootmode'] = 'efi'
147
+
148
+        # define 'hostname' as 'photon-<RANDOM STRING>'
149
+        if "hostname" not in install_config or install_config['hostname'] == "":
150
+            install_config['hostname'] = 'photon-%12x' % random.randrange(16**12)
151
+
152
+        # Set crypted password if needed
153
+        if 'shadow_password' not in install_config:
154
+            if 'password' in install_config:
155
+                if install_config['password']['crypted']:
156
+                    install_config['shadow_password'] = install_config['password']['text']
157
+                else:
158
+                    install_config['shadow_password'] = CommandUtils.generate_password_hash(install_config['password']['text'])
159
+            else:
160
+                install_config['shadow_password'] = '*'
161
+
162
+    def _check_install_config(self, install_config):
163
+        """
164
+        Sanity check of install_config before its execution.
165
+        Return error string or None
166
+        """
167
+
168
+        unknown_keys = install_config.keys() - Installer.known_keys
169
+        if len(unknown_keys) > 0:
170
+            return "Unknown install_config keys: " + ", ".join(unknown_keys)
171
+
172
+        if not 'disk' in install_config:
173
+            return "No disk configured"
174
+
175
+        return None
176
+
177
+    def _install(self):
87 178
         """
88 179
         Install photon system and handle exception
89 180
         """
90
-        if self.install_config['iso_installer']:
181
+        if self.iso_installer:
91 182
             self.window.show_window()
92 183
             self.progress_bar.initialize('Initializing installation...')
93 184
             self.progress_bar.show()
... ...
@@ -97,7 +206,7 @@ class Installer(object):
97 97
         except Exception as inst:
98 98
             self.logger.exception(repr(inst))
99 99
             self.exit_gracefully()
100
-            if not self.install_config['iso_installer']:
100
+            if not self.iso_installer:
101 101
                 raise
102 102
 
103 103
     def _unsafe_install(self):
... ...
@@ -127,7 +236,7 @@ class Installer(object):
127 127
         del frame1
128 128
         if not self.exiting:
129 129
             self.exiting = True
130
-            if self.install_config['iso_installer']:
130
+            if self.iso_installer:
131 131
                 self.progress_bar.hide()
132 132
                 self.window.addstr(0, 0, 'Oops, Installer got interrupted.\n\n' +
133 133
                                    'Press any key to get to the bash...')
... ...
@@ -147,6 +256,8 @@ class Installer(object):
147 147
         if retval != 0:
148 148
             self.logger.error("Failed to unmount disks")
149 149
 
150
+        shutil.rmtree(self.photon_root)
151
+
150 152
         # Uninitialize device paritions mapping
151 153
         disk = self.install_config['disk']
152 154
         if 'loop' in disk:
... ...
@@ -156,12 +267,12 @@ class Installer(object):
156 156
                 return None
157 157
 
158 158
         # Congratulation screen
159
-        if self.install_config['iso_installer'] and not self.exiting:
159
+        if self.iso_installer and not self.exiting:
160 160
             self.progress_bar.hide()
161 161
             self.window.addstr(0, 0, 'Congratulations, Photon has been installed in {0} secs.\n\n'
162 162
                                'Press any key to continue to boot...'
163 163
                                .format(self.progress_bar.time_elapsed))
164
-            if 'ui_install' in self.install_config:
164
+            if self.interactive:
165 165
                 self.window.content_window().getch()
166 166
             self._eject_cdrom()
167 167
 
... ...
@@ -275,11 +386,11 @@ class Installer(object):
275 275
             self.logger.info("Failed to setup the disk for installation")
276 276
             self.exit_gracefully()
277 277
 
278
-        if self.install_config['iso_installer']:
278
+        if self.iso_installer:
279 279
             self.progress_bar.update_message('Initializing system...')
280 280
         self._bind_installer()
281 281
         self._bind_repo_dir()
282
-        retval = self.cmd.run([self.prepare_command, '-w', self.photon_root,
282
+        retval = self.cmd.run([Installer.prepare_command, '-w', self.photon_root,
283 283
                                self.working_directory, self.rpm_cache_dir])
284 284
         if retval != 0:
285 285
             self.logger.info("Failed to bind the installer and repo needed by tdnf")
... ...
@@ -289,7 +400,7 @@ class Installer(object):
289 289
         """
290 290
         Finalize the system after the installation
291 291
         """
292
-        if self.install_config['iso_installer']:
292
+        if self.iso_installer:
293 293
             self.progress_bar.show_loading('Finalizing installation')
294 294
         #Setup the disk
295 295
         retval = self.cmd.run([Installer.chroot_command, '-w', self.photon_root,
... ...
@@ -304,6 +415,8 @@ class Installer(object):
304 304
         retval = self.cmd.run(['rm', '-rf', os.path.join(self.photon_root, "cache")])
305 305
         if retval != 0:
306 306
             self.logger.error("Fail to remove the cache")
307
+        os.remove(self.tdnf_conf_path)
308
+        os.remove(self.tdnf_repo_path)
307 309
 
308 310
     def _post_install(self):
309 311
         # install grub
... ...
@@ -413,7 +526,7 @@ class Installer(object):
413 413
         # run in shell to do not throw exception if tdnf not found
414 414
         process = subprocess.Popen(tdnf_cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
415 415
 
416
-        if self.install_config['iso_installer']:
416
+        if self.iso_installer:
417 417
             while True:
418 418
                 output = process.stdout.readline().decode()
419 419
                 if output == '':
... ...
@@ -475,10 +588,7 @@ class Installer(object):
475 475
         """
476 476
         Eject the cdrom on request
477 477
         """
478
-        eject_cdrom = True
479
-        if 'eject_cdrom' in self.install_config and not self.install_config['eject_cdrom']:
480
-            eject_cdrom = False
481
-        if eject_cdrom:
478
+        if self.install_config.get('eject_cdrom', True):
482 479
             self.cmd.run(['eject', '-r'])
483 480
 
484 481
     def _enable_network_in_chroot(self):
... ...
@@ -519,7 +629,7 @@ class Installer(object):
519 519
         Partition the disk
520 520
         """
521 521
 
522
-        if self.install_config['iso_installer']:
522
+        if self.iso_installer:
523 523
             self.progress_bar.update_message('Partitioning...')
524 524
 
525 525
         if 'partitions' not in self.install_config:
526 526
deleted file mode 100755
... ...
@@ -1,31 +0,0 @@
1
-#
2
-#
3
-#    Author: Touseef Liaqat <tliaqat@vmware.com>
4
-
5
-from installer import Installer
6
-from ostreeinstaller import OstreeInstaller
7
-
8
-class InstallerContainer(object):
9
-    def __init__(self, install_config, maxy=0, maxx=0,
10
-                 iso_installer=False, rpm_path="../stage/RPMS",
11
-                 log_path="../stage/LOGS", log_level="info"):
12
-
13
-        self.install_config = install_config
14
-        self.maxy = maxy
15
-        self.maxx = maxx
16
-        self.iso_installer = iso_installer
17
-        self.rpm_path = rpm_path
18
-        self.log_path = log_path
19
-        self.log_level = log_level
20
-
21
-    def install(self):
22
-        installer = None
23
-
24
-        if self.install_config.get('type', '') == "ostree_host":
25
-            installer = OstreeInstaller(self.install_config, self.maxy, self.maxx,
26
-                              self.iso_installer, self.rpm_path, self.log_path, self.log_level)
27
-        else:
28
-            installer = Installer(self.install_config, self.maxy, self.maxx,
29
-                              self.iso_installer, self.rpm_path, self.log_path, self.log_level)
30
-
31
-        return installer.install()
... ...
@@ -3,10 +3,11 @@
3 3
 #
4 4
 #    Author: Mahmoud Bassiouny <mbassiouny@vmware.com>
5 5
 
6
-from argparse import ArgumentParser
6
+import shlex
7 7
 import curses
8
-from installercontainer import InstallerContainer
9
-from iso_config import IsoConfig
8
+from argparse import ArgumentParser
9
+from installer import Installer
10
+from commandutils import CommandUtils
10 11
 
11 12
 class IsoInstaller(object):
12 13
     def __init__(self, stdscreen, options_file, rpms_path):
... ...
@@ -23,19 +24,124 @@ class IsoInstaller(object):
23 23
         self.maxy, self.maxx = self.screen.getmaxyx()
24 24
         self.screen.addstr(self.maxy - 1, 0, '  Arrow keys make selections; <Enter> activates.')
25 25
         curses.curs_set(0)
26
-        config = IsoConfig()
27
-        rpm_path, install_config = config.configure(options_file, rpms_path, self.maxy, self.maxx, log_path="/var/log")
28 26
 
29
-        self.screen.erase()
30
-        installer = InstallerContainer(
31
-            install_config,
32
-            self.maxy, self.maxx,
33
-            True,
34
-            rpm_path=rpm_path,
27
+        install_config=None
28
+        self.cd_mount_path = None
29
+        ks_path = None
30
+        cd_search = None
31
+
32
+        with open('/proc/cmdline', 'r') as f:
33
+            kernel_params = shlex.split(f.read().replace('\n', ''))
34
+
35
+        for arg in kernel_params:
36
+            if arg.startswith("ks="):
37
+                ks_path = arg[len("ks="):]
38
+            elif arg.startswith("repo="):
39
+                rpms_path = arg[len("repo="):]
40
+            elif arg.startswith("photon.media="):
41
+                cd_search = arg[len("photon.media="):]
42
+
43
+        if cd_search is not None:
44
+            self.mount_cd(cd_search)
45
+
46
+        if ks_path is not None:
47
+            install_config=self.get_ks_config(ks_path)
48
+
49
+        # Run installer
50
+        installer = Installer(
51
+            maxy=self.maxy,
52
+            maxx=self.maxx,
53
+            iso_installer=True,
54
+            rpm_path=rpms_path,
35 55
             log_path="/var/log",
36 56
             log_level="debug")
37 57
 
38
-        installer.install()
58
+        ui_config={}
59
+        ui_config['options_file'] = options_file
60
+
61
+        installer.configure(install_config, ui_config)
62
+        self.screen.erase()
63
+        installer.execute()
64
+
65
+    def _load_ks_config(self, path):
66
+        """kick start configuration"""
67
+        if path.startswith("http://"):
68
+            # Do 5 trials to get the kick start
69
+            # TODO: make sure the installer run after network is up
70
+            ks_file_error = "Failed to get the kickstart file at {0}".format(path)
71
+            wait = 1
72
+            for _ in range(0, 5):
73
+                err_msg = ""
74
+                try:
75
+                    response = requests.get(path, timeout=3)
76
+                    if response.ok:
77
+                        return json.loads(response.text)
78
+                    err_msg = response.text
79
+                except Exception as e:
80
+                    err_msg = e
81
+
82
+                self.logger.warning(ks_file_error)
83
+                self.logger.warning("error msg: {0}".format(err_msg))
84
+                self.logger.warning("retry in a second")
85
+                time.sleep(wait)
86
+                wait = wait * 2
87
+
88
+            # Something went wrong
89
+            self.logger.error(ks_file_error)
90
+            raise Exception(err_msg)
91
+        else:
92
+            if path.startswith("cdrom:/"):
93
+                if self.cd_mount_path is None:
94
+                    raise Exception("cannot read ks config from cdrom, no cdrom specified")
95
+                path = os.path.join(self.cd_mount_path, path.replace("cdrom:/", "", 1))
96
+            return (JsonWrapper(path)).read()
97
+
98
+    def get_ks_config(self, ks_path):
99
+        """Load configuration from kick start file"""
100
+        install_config = self._load_ks_config(ks_path)
101
+
102
+        if 'install_linux_esx' not in install_config and CommandUtils.is_vmware_virtualization():
103
+            install_config['install_linux_esx'] = True
104
+
105
+        return install_config
106
+
107
+    def mount_cd(self, cd_search):
108
+        """Mount the cd with RPMS"""
109
+        # check if the cd is already mounted
110
+        if self.cd_mount_path:
111
+            return
112
+        mount_path = "/mnt/cdrom"
113
+
114
+        # Mount the cd to get the RPMS
115
+        os.makedirs(mount_path, exist_ok=True)
116
+
117
+        # Construct mount cmdline
118
+        cmdline = ['mount']
119
+        if cd_search.startswith("UUID="):
120
+            cmdline.extend(['-U', cd_search[len("UUID="):] ]);
121
+        elif cd_search.startswith("LABEL="):
122
+            cmdline.extend(['-L', cd_search[len("LABEL="):] ]);
123
+        elif cd_search == "cdrom":
124
+            cmdline.append('/dev/cdrom')
125
+        else:
126
+            self.logger.error("Unsupported installer media, check photon.media in kernel cmdline")
127
+            raise Exception("Can not mount the cd")
128
+
129
+        cmdline.extend(['-o', 'ro', mount_path])
130
+
131
+        # Retry mount the CD
132
+        for _ in range(0, 3):
133
+            process = subprocess.Popen(cmdline)
134
+            retval = process.wait()
135
+            if retval == 0:
136
+                self.cd_mount_path = mount_path
137
+                return
138
+            self.logger.error("Failed to mount the cd, retry in a second")
139
+            time.sleep(1)
140
+        self.logger.error("Failed to mount the cd, exiting the installer")
141
+        self.logger.error("check the logs for more details")
142
+        raise Exception("Can not mount the cd")
143
+
39 144
 
40 145
 if __name__ == '__main__':
41 146
     usage = "Usage: %prog [options]"
... ...
@@ -1,13 +1,6 @@
1 1
 import os
2 2
 import sys
3
-import subprocess
4
-import shlex
5 3
 import re
6
-import json
7
-import time
8
-import crypt
9
-import string
10
-from urllib.request import urlopen
11 4
 import random
12 5
 import requests
13 6
 import cracklib
... ...
@@ -16,19 +9,18 @@ from custompartition import CustomPartition
16 16
 from packageselector import PackageSelector
17 17
 from windowstringreader import WindowStringReader
18 18
 from confirmwindow import ConfirmWindow
19
-from jsonwrapper import JsonWrapper
20 19
 from selectdisk import SelectDisk
21 20
 from license import License
22 21
 from linuxselector import LinuxSelector
23 22
 from ostreeserverselector import OSTreeServerSelector
24 23
 from ostreewindowstringreader import OSTreeWindowStringReader
24
+from commandutils import CommandUtils
25 25
 
26 26
 class IsoConfig(object):
27 27
     g_ostree_repo_url = None
28 28
     """This class handles iso installer configuration."""
29
-    def __init__(self):
29
+    def __init__(self, maxy, maxx):
30 30
         self.logger = None
31
-        self.cd_mount_path = None
32 31
         self.alpha_chars = list(range(65, 91))
33 32
         self.alpha_chars.extend(range(97, 123))
34 33
         self.hostname_accepted_chars = self.alpha_chars
... ...
@@ -39,160 +31,10 @@ class IsoConfig(object):
39 39
         self.random_id = '%12x' % random.randrange(16**12)
40 40
         self.random_hostname = "photon-" + self.random_id.strip()
41 41
 
42
-    def configure(self, options_file, rpms_path, maxy, maxx, log_path="../stage/LOGS"):
43
-        self.logger = Logger.get_logger(log_path)
44
-        ks_path = None
45
-        rpm_path = rpms_path
46
-        ks_config = None
47
-        cd_search = None
42
+        self.maxy = maxy
43
+        self.maxx = maxx
44
+        self.logger = Logger.get_logger()
48 45
 
49
-        with open('/proc/cmdline', 'r') as f:
50
-            kernel_params = shlex.split(f.read().replace('\n', ''))
51
-
52
-        for arg in kernel_params:
53
-            if arg.startswith("ks="):
54
-                ks_path = arg[len("ks="):]
55
-            elif arg.startswith("repo="):
56
-                rpm_path = arg[len("repo="):]
57
-            elif arg.startswith("photon.media="):
58
-                cd_search = arg[len("photon.media="):]
59
-
60
-        if cd_search is not None:
61
-            self.mount_cd(cd_search)
62
-
63
-        if ks_path is not None:
64
-            ks_config = self.get_config(ks_path)
65
-
66
-        if rpm_path is None:
67
-            # the rpms should be in the cd
68
-            if self.cd_mount_path is None:
69
-                self.logger.error("Please specify RPM repo location, as no cdrom is specified. (PXE?)")
70
-                raise Exception("RPM repo not found")
71
-            rpm_path = os.path.join(self.cd_mount_path, "RPMS")
72
-
73
-        if ks_config:
74
-            install_config = self.ks_config(options_file, ks_config)
75
-        else:
76
-            install_config = self.ui_config(options_file, maxy, maxx)
77
-
78
-        self._add_default(install_config)
79
-
80
-        issue = self._check_install_config(install_config)
81
-        if issue:
82
-            self.logger.error(issue)
83
-            raise Exception(issue)
84
-
85
-        return rpm_path, install_config
86
-
87
-    @staticmethod
88
-    def is_vmware_virtualization():
89
-        """Detect vmware vm"""
90
-        process = subprocess.Popen(['systemd-detect-virt'], stdout=subprocess.PIPE)
91
-        out, err = process.communicate()
92
-        if err is not None and err != 0:
93
-            return False
94
-        return out.decode() == 'vmware\n'
95
-
96
-    def get_config(self, path):
97
-        """kick start configuration"""
98
-        if path.startswith("http://"):
99
-            # Do 5 trials to get the kick start
100
-            # TODO: make sure the installer run after network is up
101
-            ks_file_error = "Failed to get the kickstart file at {0}".format(path)
102
-            wait = 1
103
-            for _ in range(0, 5):
104
-                err_msg = ""
105
-                try:
106
-                    response = requests.get(path, timeout=3)
107
-                    if response.ok:
108
-                        return json.loads(response.text)
109
-                    err_msg = response.text
110
-                except Exception as e:
111
-                    err_msg = e
112
-
113
-                self.logger.warning(ks_file_error)
114
-                self.logger.warning("error msg: {0}".format(err_msg))
115
-                self.logger.warning("retry in a second")
116
-                time.sleep(wait)
117
-                wait = wait * 2
118
-
119
-            # Something went wrong
120
-            self.logger.error(ks_file_error)
121
-            raise Exception(err_msg)
122
-        else:
123
-            if path.startswith("cdrom:/"):
124
-                if self.cd_mount_path is None:
125
-                    raise Exception("cannot read ks config from cdrom, no cdrom specified")
126
-                path = os.path.join(self.cd_mount_path, path.replace("cdrom:/", "", 1))
127
-            return (JsonWrapper(path)).read()
128
-
129
-    def mount_cd(self, cd_search):
130
-        """Mount the cd with RPMS"""
131
-        # check if the cd is already mounted
132
-        if self.cd_mount_path:
133
-            return
134
-        mount_path = "/mnt/cdrom"
135
-
136
-        # Mount the cd to get the RPMS
137
-        os.makedirs(mount_path, exist_ok=True)
138
-
139
-        # Construct mount cmdline
140
-        cmdline = ['mount']
141
-        if cd_search.startswith("UUID="):
142
-            cmdline.extend(['-U', cd_search[len("UUID="):] ]);
143
-        elif cd_search.startswith("LABEL="):
144
-            cmdline.extend(['-L', cd_search[len("LABEL="):] ]);
145
-        elif cd_search == "cdrom":
146
-            cmdline.append('/dev/cdrom')
147
-        else:
148
-            self.logger.error("Unsupported installer media, check photon.media in kernel cmdline")
149
-            raise Exception("Can not mount the cd")
150
-
151
-        cmdline.extend(['-o', 'ro', mount_path])
152
-
153
-        # Retry mount the CD
154
-        for _ in range(0, 3):
155
-            process = subprocess.Popen(cmdline)
156
-            retval = process.wait()
157
-            if retval == 0:
158
-                self.cd_mount_path = mount_path
159
-                return
160
-            self.logger.error("Failed to mount the cd, retry in a second")
161
-            time.sleep(1)
162
-        self.logger.error("Failed to mount the cd, exiting the installer")
163
-        self.logger.error("check the logs for more details")
164
-        raise Exception("Can not mount the cd")
165
-
166
-    def ks_config(self, options_file, ks_config):
167
-        """Load configuration from file"""
168
-        del options_file
169
-        install_config = ks_config
170
-        if self.is_vmware_virtualization() and 'install_linux_esx' not in install_config:
171
-            install_config['install_linux_esx'] = True
172
-
173
-        base_path = os.path.dirname("build_install_options_all.json")
174
-        package_list = []
175
-        if 'packagelist_file' in install_config:
176
-            package_list = PackageSelector.get_packages_to_install(install_config['packagelist_file'],
177
-                                                               base_path)
178
-
179
-        if 'additional_packages' in install_config:
180
-            package_list.extend(install_config['additional_packages'])
181
-        install_config['packages'] = package_list
182
-
183
-        if "hostname" in install_config:
184
-            evalhostname = os.popen('printf ' + install_config["hostname"].strip(" ")).readlines()
185
-            install_config['hostname'] = evalhostname[0]
186
-
187
-        # crypt the password if needed
188
-        if install_config['password']['crypted']:
189
-            install_config['password'] = install_config['password']['text']
190
-        else:
191
-            install_config['password'] = crypt.crypt(
192
-                install_config['password']['text'],
193
-                "$6$" + "".join([random.choice(
194
-                    string.ascii_letters + string.digits) for _ in range(16)]))
195
-        return install_config
196 46
 
197 47
     @staticmethod
198 48
     def validate_hostname(hostname):
... ...
@@ -290,23 +132,10 @@ class IsoConfig(object):
290 290
             password = str(message)
291 291
         return password == text, "Error: " + password
292 292
 
293
-    @staticmethod
294
-    def generate_password_hash(password):
295
-        """Generate hash for the password"""
296
-        shadow_password = crypt.crypt(
297
-            password, "$6$" + "".join(
298
-                [random.choice(
299
-                    string.ascii_letters + string.digits) for _ in range(16)]))
300
-        return shadow_password
301
-
302
-    def ui_config(self, options_file, maxy, maxx):
293
+    def configure(self, ui_config):
303 294
         """Configuration through UI"""
304
-        # This represents the installer screen, the bool indicated if
305
-        # I can go back to this window or not
306 295
         install_config = {}
307
-        install_config['ui_install'] = True
308
-        items = self.add_ui_pages(options_file, maxy, maxx,
309
-                                                      install_config)
296
+        items = self.add_ui_pages(install_config, ui_config, self.maxy, self.maxx)
310 297
         index = 0
311 298
         while True:
312 299
             result = items[index][0]()
... ...
@@ -325,12 +154,13 @@ class IsoConfig(object):
325 325
                 if index < 0:
326 326
                     index = 0
327 327
         return install_config
328
-    def add_ui_pages(self, options_file, maxy, maxx, install_config):
328
+
329
+    def add_ui_pages(self, install_config, ui_config, maxy, maxx):
329 330
         items = []
330 331
         license_agreement = License(maxy, maxx)
331 332
         select_disk = SelectDisk(maxy, maxx, install_config)
332 333
         custom_partition = CustomPartition(maxy, maxx, install_config)
333
-        package_selector = PackageSelector(maxy, maxx, install_config, options_file)
334
+        package_selector = PackageSelector(maxy, maxx, install_config, ui_config['options_file'])
334 335
         hostname_reader = WindowStringReader(
335 336
             maxy, maxx, 10, 70,
336 337
             'hostname',
... ...
@@ -344,7 +174,7 @@ class IsoConfig(object):
344 344
             True)
345 345
         root_password_reader = WindowStringReader(
346 346
             maxy, maxx, 10, 70,
347
-            'password',
347
+            'shadow_password',
348 348
             None, # confirmation error msg if it's a confirmation text
349 349
             '*', # echo char
350 350
             None, # set of accepted chars
... ...
@@ -353,13 +183,13 @@ class IsoConfig(object):
353 353
             'Set up root password', 'Root password:', 2, install_config)
354 354
         confirm_password_reader = WindowStringReader(
355 355
             maxy, maxx, 10, 70,
356
-            'password',
356
+            'shadow_password',
357 357
             # confirmation error msg if it's a confirmation text
358 358
             "Passwords don't match, please try again.",
359 359
             '*', # echo char
360 360
             None, # set of accepted chars
361 361
             None, # validation function of the input
362
-            IsoConfig.generate_password_hash, # post processing of the input field
362
+            CommandUtils.generate_password_hash, # post processing of the input field
363 363
             'Confirm root password', 'Confirm Root password:', 2, install_config)
364 364
 
365 365
         ostree_server_selector = OSTreeServerSelector(maxy, maxx, install_config)
... ...
@@ -388,11 +218,13 @@ class IsoConfig(object):
388 388
                                       'Start installation? All data on the selected disk will be lost.\n\n'
389 389
                                       'Press <Yes> to confirm, or <No> to quit')
390 390
 
391
+        # This represents the installer screens, the bool indicated if
392
+        # I can go back to this window or not
391 393
         items.append((license_agreement.display, False))
392 394
         items.append((select_disk.display, True))
393 395
         items.append((custom_partition.display, False))
394 396
         items.append((package_selector.display, True))
395
-        if self.is_vmware_virtualization():
397
+        if CommandUtils.is_vmware_virtualization():
396 398
             linux_selector = LinuxSelector(maxy, maxx, install_config)
397 399
             items.append((linux_selector.display, True))
398 400
         items.append((hostname_reader.get_user_string, True))
... ...
@@ -405,29 +237,3 @@ class IsoConfig(object):
405 405
 
406 406
         return items
407 407
 
408
-    def _add_default(self, install_config):
409
-        """
410
-        Add default install_config settings if not specified
411
-        """
412
-        # 'bootmode' mode
413
-        if 'bootmode' not in install_config:
414
-            arch = subprocess.check_output(['uname', '-m'], universal_newlines=True)
415
-            if "x86_64" in arch:
416
-                install_config['bootmode'] = 'dualboot'
417
-            else:
418
-                install_config['bootmode'] = 'efi'
419
-
420
-        # define 'hostname' as 'photon-<RANDOM STRING>'
421
-        if "hostname" not in install_config or install_config['hostname'] == "":
422
-            install_config['hostname'] = "photon-" + self.random_id.strip()
423
-
424
-    def _check_install_config(self, install_config):
425
-        """
426
-        Sanity check of install_config before its execution.
427
-        Return error string or None
428
-        """
429
-        if not 'disk' in install_config:
430
-            return "No disk configured"
431
-
432
-        return None
433
-
434 408
new file mode 100644
... ...
@@ -0,0 +1,97 @@
0
+Kickstart config file is a json format with following possible parameters:
1
+
2
+'additional_packages' same as 'packages'
3
+
4
+'bootmode' (optional)
5
+	Sets the boot type to suppot: EFI, BIOS or both.
6
+	Acceptible values are: "bios", "efi", "dualboot"
7
+	"bios" will add special partition (very first) for first stage grub.
8
+	"efi" will add ESP (Efi Special Partition), format is as FAT and copy
9
+	there EFI binaries including grub.efi
10
+	"dualboot" will add two extra partitions for "bios" and "efi" modes.
11
+	This target will support both modes that can be switched in bios
12
+	settings without extra actions in the OS.
13
+	Default value: 'dualboot' for x86_64 and 'efi' for aarch64
14
+	Example: { 'bootmode': 'bios' }
15
+
16
+'disk' (required)
17
+	Target's disk device file path to install into, such as '/dev/sda'.
18
+	Loop device is also supported.
19
+	Example: { 'disk': '/dev/sdb' }
20
+
21
+'eject_cdrom' (optional)
22
+	Eject or not cdrom after installation completed.
23
+	Boolean: True or False
24
+	Default value: True
25
+	Example: { 'eject_cdrom': False }
26
+
27
+'hostname' (optional)
28
+	Set target host name.
29
+	Default value: 'photon-<randomized string>'
30
+	Example: { 'hostname': 'photon-machine' }
31
+
32
+'packagelist_file' (optional if 'packages' set)
33
+	Contains file name which has list of packages to install.
34
+	Example: { 'packagelist_file': 'packages_minimal.json' }
35
+
36
+'packages' (optional if 'packagelist_file' set)
37
+	Contains list of packages to install.
38
+	Example: { 'packages': ['minimal', 'linux', 'initramfs'] }
39
+
40
+'partition_type' (optional)
41
+	Set partition table type. Supported values are: "gpt", "msdos".
42
+	Default value: 'gpt'
43
+	Example: { 'partition_type': 'msdos' }
44
+
45
+'partitions' (optional)
46
+	Contains list of partitions to create.
47
+	Each partition is a dictionary of the following items:
48
+	'filesystem' (required)
49
+		Filesystem type. Supported values are: 'swap', 'ext4', 'fat'.
50
+	'mountpoint' (required for non 'swap' partitions)
51
+		Mount point for the partition.
52
+	'size' (required)
53
+		Size of the partition in MB. If 0 then partition is considered
54
+		as expansible to fill rest of the disk. Only one expansible
55
+		partition is allowed.
56
+	'fs_options' (optional)
57
+		Additional parameters for the mkfs command as a string
58
+	Default value: [{"mountpoint": "/", "size": 0, "filesystem": "ext4"}]
59
+	Example: { 'partitions' : [
60
+			{"mountpoint": "/", "size": 0, "filesystem": "ext4"},
61
+			{
62
+				"mountpoint": "/boot/esp",
63
+				"size": 12,
64
+				"filesystem": "fat",
65
+				"fs_options": "-n EFI"
66
+			},
67
+			{"size": 128, "filesystem": "swap"} ] }
68
+
69
+'password' (optional)
70
+	Set root password. It is dictionary of two items:
71
+	'text' (required) password plain text ('crypted' : false)
72
+			  of encrypted ('crypted': true)
73
+	'crypted' (required) how to interpret 'text' content
74
+	Default value: { "crypted": true, "text": "*"} }
75
+	    which means root is not allowed to login.
76
+	Example: { "password": {
77
+			"crypted": false,
78
+			"text": "changeme"} }
79
+
80
+'postinstall' (optional)
81
+	Contains list of lines to be executed as a single script on
82
+	the target after installation.
83
+	Example: { "postinstall": [
84
+			"#!/bin/sh",
85
+			"echo \"Hello World\" > /etc/postinstall" ] }
86
+
87
+'public_key' (optional)
88
+	To inject entry to authorized_keys as a string
89
+
90
+'shadow_password' (optional)
91
+	Contains encrypted root password <encrypted password here>.
92
+	Short form of: { "password": {
93
+				"crypted": true,
94
+				"text": "<encrypted password here>"} }
95
+
96
+For reference, look at 'sample_ks.cfg' file
... ...
@@ -8,7 +8,7 @@ install_phase = commons.POST_INSTALL
8 8
 enabled = True
9 9
 
10 10
 def execute(installer):
11
-    shadow_password = installer.install_config['password']
11
+    shadow_password = installer.install_config['shadow_password']
12 12
     installer.logger.info("Set root password")
13 13
 
14 14
     passwd_filename = os.path.join(installer.photon_root, 'etc/passwd')
... ...
@@ -14,8 +14,9 @@ from actionresult import ActionResult
14 14
 
15 15
 class OstreeInstaller(Installer):
16 16
 
17
-    def __init__(self, install_config, maxy = 0, maxx = 0, iso_installer = False, rpm_path = "../stage/RPMS", log_path = "../stage/LOGS", log_level = "info"):
17
+    def __init__(self, install_config, maxy = 0, maxx = 0, iso_installer = False, interactive = False, rpm_path = "../stage/RPMS", log_path = "../stage/LOGS", log_level = "info"):
18 18
         Installer.__init__(self, install_config, maxy, maxx, iso_installer, rpm_path, log_path, log_level)
19
+        self.interactive = interactive
19 20
         self.repo_config = {}
20 21
         self.repo_read_conf()
21 22
 
... ...
@@ -192,7 +193,7 @@ class OstreeInstaller(Installer):
192 192
         self.progress_bar.update_loading_message("Ready to restart")
193 193
         self.progress_bar.hide()
194 194
         self.window.addstr(0, 0, 'Congratulations, Photon RPM-OSTree Host has been installed in {0} secs.\n\nPress any key to continue to boot...'.format(self.progress_bar.time_elapsed))
195
-        if 'ui_install' in self.install_config:
195
+        if self.interactive:
196 196
             self.window.content_window().getch()
197 197
         return ActionResult(True, None)
198 198
 
... ...
@@ -39,13 +39,6 @@ class PackageSelector(object):
39 39
         else:
40 40
             raise Exception("Install option '" + option['title'] + "' must have 'packagelist_file' or 'packages' property")
41 41
 
42
-    @staticmethod
43
-    def get_additional_files_to_copy_in_iso(install_option, base_path):
44
-        additional_files = []
45
-        if "additional-files" in install_option[1]:
46
-            additional_files = install_option[1]["additional-files"]
47
-        return additional_files
48
-
49 42
     def load_package_list(self, options_file):
50 43
         json_wrapper_option_list = JsonWrapper(options_file)
51 44
         option_list_json = json_wrapper_option_list.read()
... ...
@@ -61,12 +54,9 @@ class PackageSelector(object):
61 61
             if install_option[1]["visible"] == True:
62 62
                 package_list = PackageSelector.get_packages_to_install(install_option[1],
63 63
                                                                        base_path)
64
-                additional_files = PackageSelector.get_additional_files_to_copy_in_iso(
65
-                    install_option, base_path)
66 64
                 self.package_menu_items.append((install_option[1]["title"],
67 65
                                                 self.exit_function,
68
-                                                [install_option[0],
69
-                                                 package_list, additional_files]))
66
+                                                [install_option[0], package_list]))
70 67
                 if install_option[0] == 'minimal':
71 68
                     default_selected = visible_options_cnt
72 69
                 visible_options_cnt = visible_options_cnt + 1
... ...
@@ -78,7 +68,6 @@ class PackageSelector(object):
78 78
     def exit_function(self, selected_item_params):
79 79
         self.install_config['type'] = selected_item_params[0]
80 80
         self.install_config['packages'] = selected_item_params[1]
81
-        self.install_config['additional-files'] = selected_item_params[2]
82 81
         return ActionResult(True, {'custom': False})
83 82
 
84 83
     def custom_packages(self):
... ...
@@ -1,14 +1,10 @@
1 1
 {
2
+    "installer": {
3
+        "hostname": "photon-machine",
4
+        "packagelist_file": "packages_ami.json"
5
+    },
2 6
     "image_type": "ami",
3
-    "hostname": "photon-machine",
4
-    "password": 
5
-        {
6
-            "crypted": false,
7
-            "text": "PASSWORD"
8
-        },
9
-    "packagelist_file": "packages_ami.json",
10 7
     "size": 8192,
11
-    "public_key":"<ssh-key-here>",
12 8
     "postinstallscripts": [ "ami-patch.sh", "../password-expiry.sh" ],
13 9
     "additionalfiles": [
14 10
                             {"cloud-photon.cfg": "/etc/cloud/cloud.cfg"}
... ...
@@ -1,14 +1,10 @@
1 1
 {
2
+    "installer": {
3
+        "hostname": "photon-machine",
4
+        "packagelist_file": "packages_azure.json"
5
+    },
2 6
     "image_type": "azure",
3
-    "hostname": "photon-machine",
4
-    "password": 
5
-        {
6
-            "crypted": false,
7
-            "text": "PASSWORD"
8
-        },
9
-    "packagelist_file": "packages_azure.json",
10 7
     "size": 16384,
11
-    "public_key":"<ssh-key-here>",
12 8
     "postinstallscripts": [ "azure-patch.sh", "../password-expiry.sh" ],
13 9
     "additionalfiles": [
14 10
                             {"cloud-photon.cfg": "/etc/cloud/cloud.cfg"}
... ...
@@ -1,15 +1,11 @@
1 1
 {
2
+    "installer": {
3
+        "bootmode":"bios",
4
+        "hostname": "photon-machine",
5
+        "packagelist_file": "packages_gce.json"
6
+    },
2 7
     "image_type": "gce",
3
-    "hostname": "photon-machine",
4
-    "password": 
5
-        {
6
-            "crypted": false,
7
-            "text": "PASSWORD"
8
-        },
9
-    "packagelist_file": "packages_gce.json",
10 8
     "size": 16284,
11
-    "bootmode":"bios",
12
-    "public_key":"<ssh-key-here>",
13 9
     "postinstallscripts": [ "gce-patch.sh", "../password-expiry.sh" ],
14 10
     "additionalfiles": [
15 11
                             {"cloud-photon.cfg": "/etc/cloud/cloud.cfg"},
... ...
@@ -12,30 +12,18 @@ import subprocess
12 12
 from argparse import ArgumentParser
13 13
 import imagegenerator
14 14
 
15
-def runInstaller(options, config):
15
+def runInstaller(options, install_config, working_directory):
16 16
     try:
17 17
         sys.path.insert(0, options.installer_path)
18 18
         from installer import Installer
19
-        from packageselector import PackageSelector
20 19
     except:
21 20
         raise ImportError('Installer path incorrect!')
22 21
 
23
-    # Check the installation type
24
-    option_list_json = Utils.jsonread(options.package_list_file)
25
-    options_sorted = option_list_json.items()
26
-
27
-    packages = []
28
-    if 'packagelist_file' in config:
29
-        packages = PackageSelector.get_packages_to_install(config,
30
-                                                           options.generated_data_path)
31
-    if 'additional_packages' in config:
32
-        packages = packages.extend(config['additional_packages'])
33
-
34
-    config['packages'] = packages
35 22
     # Run the installer
36
-    package_installer = Installer(config, rpm_path=options.rpm_path,
37
-                                  log_path=options.log_path, log_level=options.log_level)
38
-    return package_installer.install()
23
+    installer = Installer(working_directory = working_directory, rpm_path=options.rpm_path,
24
+                          log_path=options.log_path, log_level=options.log_level)
25
+    installer.configure(install_config)
26
+    return installer.execute()
39 27
 
40 28
 def get_file_name_with_last_folder(filename):
41 29
     basename = os.path.basename(filename)
... ...
@@ -159,20 +147,6 @@ def createIso(options):
159 159
     if os.path.exists(working_directory) and os.path.isdir(working_directory):
160 160
         shutil.rmtree(working_directory)
161 161
 
162
-def cryptPassword(config, passwordtext):
163
-    config['passwordtext'] = passwordtext
164
-    crypted = config['password']['crypted']
165
-    if config['password']['text'] == 'PASSWORD':
166
-        config['password'] = "".join([random.SystemRandom().choice(
167
-                string.ascii_letters + string.digits) for _ in range(16)])
168
-        if crypted:
169
-            config['password'] = crypt.crypt(
170
-                config['password'],
171
-                "$6$" + "".join([random.SystemRandom().choice(
172
-                    string.ascii_letters + string.digits) for _ in range(16)]))
173
-    else:
174
-        config['password'] = crypt.crypt(passwordtext, '$6$saltsalt$')
175
-
176 162
 def replaceScript(script_dir, img, script_name, parent_script_dir=None):
177 163
     if not parent_script_dir:
178 164
         parent_script_dir = script_dir
... ...
@@ -222,17 +196,17 @@ def createImage(options):
222 222
     if 'ova' in config['artifacttype'] and shutil.which("ovftool") is None:
223 223
         raise Exception("ovftool is not available")
224 224
 
225
+    install_config = config['installer']
226
+
225 227
     image_type = config['image_type']
226 228
     workingDir = os.path.abspath(options.stage_path + "/" + image_type)
227 229
     if os.path.exists(workingDir) and os.path.isdir(workingDir):
228 230
         shutil.rmtree(workingDir)
229 231
     os.mkdir(workingDir)
230
-    if 'password' in config:
231
-        cryptPassword(config, config['password']['text'])
232 232
     script_dir = os.path.dirname(os.path.realpath(__file__))
233 233
 
234 234
     grub_script = replaceScript(script_dir, image_type, "mk-setup-grub.sh", options.installer_path)
235
-    config['setup_grub_script'] = grub_script
235
+    install_config['setup_grub_script'] = grub_script
236 236
 
237 237
     if options.additional_rpms_path:
238 238
         os.mkdir(options.rpm_path + '/additional')
... ...
@@ -241,6 +215,13 @@ def createImage(options):
241 241
             d = os.path.join(options.rpm_path + '/additional', item)
242 242
             shutil.copy2(s, d)
243 243
 
244
+    # Set absolute path for 'packagelist_file'
245
+    if 'packagelist_file' in install_config:
246
+        plf = install_config['packagelist_file']
247
+        if not plf.startswith('/'):
248
+            plf = os.path.join(options.generated_data_path, plf)
249
+        install_config['packagelist_file'] = plf
250
+
244 251
     os.chdir(workingDir)
245 252
     image_file = workingDir + "/photon-" + image_type + ".raw"
246 253
 
... ...
@@ -251,15 +232,15 @@ def createImage(options):
251 251
         "chmod 755 {}".format(image_file))
252 252
 
253 253
     # Associating loopdevice to raw disk and save the name as a target's 'disk'
254
-    config['disk'] = (Utils.runshellcommand(
254
+    install_config['disk'] = (Utils.runshellcommand(
255 255
         "losetup --show -f {}".format(image_file))).rstrip('\n')
256 256
 
257
-    result = runInstaller(options, config)
257
+    result = runInstaller(options, install_config, workingDir)
258 258
     if not result:
259 259
         raise Exception("Installation process failed")
260 260
 
261 261
     # Detaching loop device from vmdk
262
-    Utils.runshellcommand("losetup -d {}".format(config['disk']))
262
+    Utils.runshellcommand("losetup -d {}".format(install_config['disk']))
263 263
 
264 264
     os.chdir(script_dir)
265 265
     imagegenerator.generateImage(
... ...
@@ -176,8 +176,9 @@ def generateImage(raw_image_path, additional_rpms_path, tools_bin_path, src_root
176 176
     working_directory = os.path.dirname(raw_image_path)
177 177
     mount_path = os.path.splitext(raw_image_path)[0]
178 178
     build_scripts_path = os.path.dirname(os.path.abspath(__file__))
179
+    # TODO: remove 'bootmode' -> partition_no hack
179 180
     root_partition_no = 2
180
-    if 'bootmode' in config and config['bootmode'] == 'dualboot':
181
+    if config['installer'].get('bootmode', '') == 'dualboot':
181 182
         root_partition_no = 3
182 183
 
183 184
     if os.path.exists(mount_path) and os.path.isdir(mount_path):
... ...
@@ -196,10 +197,6 @@ def generateImage(raw_image_path, additional_rpms_path, tools_bin_path, src_root
196 196
         (uuidval, partuuidval) = generateUuid(loop_device_path)
197 197
         # Prep the loop device
198 198
         prepLoopDevice(loop_device_path, mount_path)
199
-        # Clear the root password if not set explicitly from the config file
200
-        if config['passwordtext'] == 'PASSWORD':
201
-            Utils.replaceinfile(mount_path + "/etc/shadow",
202
-                                'root:.*?:', 'root:*:')
203 199
         # Clear machine-id so it gets regenerated on boot
204 200
         open(mount_path + "/etc/machine-id", "w").close()
205 201
         # Write fstab
... ...
@@ -1,20 +1,16 @@
1 1
 {
2
-	"image_type": "ls1012afrwy",
3
-	"hostname": "photon-machine",
4
-	"password":
5
-		{
6
-			"crypted": false,
7
-			"text": "PASSWORD"
8
-		},
9
-    "partition_type": "msdos",
10
-    "partitions": [
11
-                        {"mountpoint": "/boot/esp", "size": 12, "filesystem": "fat", "fs_options": "-n EFI"},
12
-                        {"mountpoint": "/", "size": 0, "filesystem": "ext4", "fs_options": "-F -O ^huge_file -b 4096 -L rootfs"}
13
-                    ],
14
-    "packagelist_file": "packages_ls1012afrwy.json",
2
+    "installer": {
3
+        "bootmode":"efi",
4
+        "hostname": "photon-machine",
5
+        "packagelist_file": "packages_ls1012afrwy.json",
6
+        "partition_type": "msdos",
7
+        "partitions": [
8
+            {"mountpoint": "/boot/esp", "size": 12, "filesystem": "fat", "fs_options": "-n EFI"},
9
+            {"mountpoint": "/", "size": 0, "filesystem": "ext4", "fs_options": "-F -O ^huge_file -b 4096 -L rootfs"}
10
+        ]
11
+    },
12
+    "image_type": "ls1012afrwy",
15 13
     "size": 512,
16
-    "bootmode":"efi",
17
-    "public_key":"<ssh-key-here>",
18 14
     "postinstallscripts": ["ls1012afrwy-custom-patch.sh"],
19 15
     "additionalfiles": [
20 16
                             {"resizefs.sh": "/usr/local/bin/resizefs.sh"},
... ...
@@ -1,15 +1,15 @@
1 1
 {
2
-    "image_type": "ova",
3
-    "hostname": "photon-machine",
4
-    "password": 
5
-        {
6
-            "crypted": true,
2
+    "installer": {
3
+        "bootmode":"dualboot",
4
+        "hostname": "photon-machine",
5
+        "packagelist_file": "packages_ova.json",
6
+        "password": {
7
+            "crypted": false,
7 8
             "text": "changeme"
8
-        },
9
-    "packagelist_file": "packages_ova.json",
9
+        }
10
+    },
11
+    "image_type": "ova",
10 12
     "size": 16384,
11
-    "bootmode":"dualboot",
12
-    "public_key":"<ssh-key-here>",
13 13
     "expirepassword": true,
14 14
     "artifacttype": "ova",
15 15
     "keeprawdisk": false
... ...
@@ -1,15 +1,15 @@
1 1
 {
2
+    "installer": {
3
+        "bootmode":"efi",
4
+        "hostname": "photon-machine",
5
+        "password": {
6
+            "crypted": false,
7
+            "text": "changeme"
8
+        },
9
+        "packagelist_file": "packages_ova.json"
10
+    },
2 11
     "image_type": "ova_uefi",
3
-	"hostname": "photon-machine",
4
-	"password": 
5
-		{
6
-			"crypted": true,
7
-			"text": "changeme"
8
-		},
9
-	"packagelist_file": "packages_ova.json",
10 12
     "size": 16384,
11
-    "bootmode":"efi",
12
-    "public_key":"<ssh-key-here>",
13 13
     "expirepassword": true,
14 14
     "artifacttype": "ova",
15 15
     "keeprawdisk": false
... ...
@@ -1,20 +1,16 @@
1 1
 {
2
-	"image_type": "rpi3",
3
-	"hostname": "photon-machine",
4
-	"password":
5
-		{
6
-			"crypted": false,
7
-			"text": "PASSWORD"
8
-		},
9
-	"packagelist_file": "packages_rpi3.json",
10
-    "partition_type": "msdos",
11
-    "partitions": [
12
-                        {"mountpoint": "/boot/esp", "size": 30, "filesystem": "fat"},
13
-                        {"mountpoint": "/", "size": 0, "filesystem": "ext4"}
14
-                    ],
2
+    "installer": {
3
+        "bootmode":"efi",
4
+        "hostname": "photon-machine",
5
+        "packagelist_file": "packages_rpi3.json",
6
+        "partition_type": "msdos",
7
+        "partitions": [
8
+            {"mountpoint": "/boot/esp", "size": 30, "filesystem": "fat"},
9
+            {"mountpoint": "/", "size": 0, "filesystem": "ext4"}
10
+        ]
11
+    },
12
+    "image_type": "rpi3",
15 13
     "size": 512,
16
-    "bootmode":"efi",
17
-    "public_key":"<ssh-key-here>",
18 14
     "postinstallscripts": ["rpi3-custom-patch.sh"],
19 15
     "additionalfiles": [
20 16
                             {"resizefs.sh": "/usr/local/bin/resizefs.sh"},