Browse code

Adding kickstart support

* Kickstart support enabled using json configuration file.
* Checked in sample_ks.cfg for reference.
* The ks conf can be served from:
* file system, or
* http server, or
* an attached cd.

Mahmoud Bassiouny authored on 2015/06/18 05:20:13
Showing 14 changed files
... ...
@@ -4,3 +4,8 @@ label install
4 4
 	menu default
5 5
 	kernel vmlinuz
6 6
 	append initrd=initrd.img root=/dev/ram0 loglevel=3
7
+
8
+label quiet
9
+	menu label ^Quiet Install
10
+	kernel vmlinuz
11
+	append initrd=initrd.img root=/dev/ram0 ks=cdrom:/isolinux/sample_ks.cfg loglevel=3
... ...
@@ -11,19 +11,21 @@ import crypt
11 11
 import re
12 12
 import random
13 13
 import string
14
-import time
15 14
 import shutil
16 15
 import fnmatch
17 16
 import signal
18 17
 import sys
18
+import glob
19
+import modules.commons
19 20
 from jsonwrapper import JsonWrapper
20 21
 from progressbar import ProgressBar
21 22
 from window import Window
22 23
 from actionresult import ActionResult
23 24
 
24 25
 class Installer(object):
25
-    def __init__(self, install_config, maxy = 0, maxx = 0, iso_installer = False, tools_path = "../stage", rpm_path = "../stage/RPMS", log_path = "../stage/LOGS"):
26
+    def __init__(self, install_config, maxy = 0, maxx = 0, iso_installer = False, tools_path = "../stage", rpm_path = "../stage/RPMS", log_path = "../stage/LOGS", ks_config = None):
26 27
         self.install_config = install_config
28
+        self.ks_config = ks_config
27 29
         self.iso_installer = iso_installer
28 30
         self.tools_path = tools_path
29 31
         self.rpm_path = rpm_path
... ...
@@ -45,13 +47,6 @@ class Installer(object):
45 45
         self.photon_root = self.working_directory + "/photon-chroot";
46 46
 
47 47
         self.restart_command = "shutdown"
48
-        self.hostname_file = self.photon_root + "/etc/hostname"
49
-        self.hosts_file = self.photon_root + "/etc/hosts"
50
-        self.passwd_filename = self.photon_root + "/etc/passwd"
51
-        self.shadow_filename = self.photon_root + "/etc/shadow"
52
-        self.authorized_keys_dir = self.photon_root + "/root/.ssh"
53
-        self.authorized_keys_filename = self.authorized_keys_dir + "/authorized_keys"
54
-        self.sshd_config_filename = self.photon_root + "/etc/ssh/sshd_config"
55 48
 
56 49
         if self.iso_installer:
57 50
             self.output = open(os.devnull, 'w')
... ...
@@ -100,6 +95,8 @@ class Installer(object):
100 100
             self.progress_bar.initialize('Initializing installation...')
101 101
             self.progress_bar.show()
102 102
 
103
+        self.execute_modules(modules.commons.PRE_INSTALL)
104
+
103 105
         self.initialize_system()
104 106
 
105 107
         #install packages
... ...
@@ -121,26 +118,21 @@ class Installer(object):
121 121
         self.finalize_system()
122 122
 
123 123
         if not self.install_config['iso_system']:
124
+            # Execute post installation modules
125
+            self.execute_modules(modules.commons.POST_INSTALL)
126
+
124 127
             # install grub
125 128
             process = subprocess.Popen([self.setup_grub_command, '-w', self.photon_root, self.install_config['disk']['disk'], self.install_config['disk']['root']], stdout=self.output)
126 129
             retval = process.wait()
127 130
 
128
-            #update root password
129
-            self.update_root_password()
130
-
131
-            #update hostname
132
-            self.update_hostname()
133
-
134
-            #update openssh config
135
-            self.update_openssh_config()
136
-
137 131
         process = subprocess.Popen([self.unmount_disk_command, '-w', self.photon_root], stdout=self.output)
138 132
         retval = process.wait()
139 133
 
140 134
         if self.iso_installer:
141 135
             self.progress_bar.hide()
142 136
             self.window.addstr(0, 0, 'Congratulations, Photon has been installed in {0} secs.\n\nPress any key to continue to boot...'.format(self.progress_bar.time_elapsed))
143
-            self.window.content_window().getch()
137
+            if self.ks_config == None:
138
+                self.window.content_window().getch()
144 139
 
145 140
         return ActionResult(True, None)
146 141
 
... ...
@@ -186,19 +178,6 @@ class Installer(object):
186 186
         process = subprocess.Popen(['cp', '-r', "../installer", self.photon_root], stdout=self.output)
187 187
         retval = process.wait()
188 188
 
189
-        # get the RPMS dir form the cd
190
-        if self.rpm_path == 'cdrom':
191
-            # Mount the cd to get the RPMS
192
-            process = subprocess.Popen(['mkdir', '-p', '/mnt/cdrom'], stdout=self.output)
193
-            retval = process.wait()
194
-
195
-            process = subprocess.Popen(['mount', '/dev/cdrom', '/mnt/cdrom'], stdout=self.output)
196
-            retval = process.wait()
197
-            if retval != 0:
198
-                self.exit_gracefully(None, None)
199
-            self.rpm_path = '/mnt/cdrom/RPMS'
200
-            self.tools_path = '/mnt/cdrom'
201
-
202 189
         self.copy_rpms()
203 190
 
204 191
     def initialize_system(self):
... ...
@@ -235,47 +214,30 @@ class Installer(object):
235 235
         process = subprocess.Popen([self.chroot_command, '-w', self.photon_root, self.install_package_command, '-w', self.photon_root, package_name, rpm_params],  stdout=self.output)
236 236
         return process.wait()
237 237
 
238
-    def replace_string_in_file(self,  filename,  search_string,  replace_string):
239
-        with open(filename, "r") as source:
240
-            lines=source.readlines()
241
-
242
-        with open(filename, "w") as destination:
243
-            for line in lines:
244
-                destination.write(re.sub(search_string,  replace_string,  line))
245
-
246
-    def update_root_password(self):
247
-        shadow_password = self.install_config['password']
248
-
249
-        #replace root blank password in passwd file to point to shadow file
250
-        self.replace_string_in_file(self.passwd_filename,  "root::", "root:x:")
251
-
252
-        if os.path.isfile(self.shadow_filename) == False:
253
-            with open(self.shadow_filename, "w") as destination:
254
-                destination.write("root:"+shadow_password+":")
255
-        else:
256
-            #add password hash in shadow file
257
-            self.replace_string_in_file(self.shadow_filename, "root::", "root:"+shadow_password+":")
258
-
259
-    def update_hostname(self):
260
-        self.hostname = self.install_config['hostname']
261
-        outfile = open(self.hostname_file,  'wb')
262
-        outfile.write(self.hostname)
263
-        outfile.close()
264
-
265
-        self.replace_string_in_file(self.hosts_file, r'127\.0\.0\.1\s+localhost', '127.0.0.1\tlocalhost\n127.0.0.1\t' + self.hostname)
266
-
267
-    def update_openssh_config(self):
268
-        if 'public_key' in self.install_config:
269
-
270
-            # Adding the authorized keys
271
-            if not os.path.exists(self.authorized_keys_dir):
272
-                os.makedirs(self.authorized_keys_dir)
273
-            with open(self.authorized_keys_filename, "a") as destination:
274
-                destination.write(self.install_config['public_key'] + "\n")
275
-            os.chmod(self.authorized_keys_filename, 0600)
276
-
277
-            # Change the sshd config to allow root login
278
-            process = subprocess.Popen(["sed", "-i", "s/^\\s*PermitRootLogin\s\+no/PermitRootLogin yes/", self.sshd_config_filename], stdout=self.output)
279
-            return process.wait()
280
-
281
-
238
+    def execute_modules(self, phase):
239
+        modules = glob.glob('modules/m_*.py')
240
+        for mod_path in modules:
241
+            module = mod_path.replace('/', '.', 1)
242
+            module = os.path.splitext(module)[0]
243
+            try:
244
+                __import__(module)
245
+                mod = sys.modules[module]
246
+            except ImportError:
247
+                print >> sys.stderr,  'Error importing module %s' % module
248
+                continue
249
+            
250
+            # the module default is disabled
251
+            if not hasattr(mod, 'enabled') or mod.enabled == False:
252
+                print >> sys.stderr,  "module %s is not enabled" % module
253
+                continue
254
+            # check for the install phase
255
+            if not hasattr(mod, 'install_phase'):
256
+                print >> sys.stderr,  "Error: can not defind module %s phase" % module
257
+                continue
258
+            if mod.install_phase != phase:
259
+                print >> sys.stderr,  "Skipping module %s for phase %s" % (module, phase)
260
+                continue
261
+            if not hasattr(mod, 'execute'):
262
+                print >> sys.stderr,  "Error: not able to execute module %s" % module
263
+                continue
264
+            mod.execute(module, self.ks_config, self.install_config, self.photon_root)
... ...
@@ -6,6 +6,12 @@
6 6
 
7 7
 import curses
8 8
 import sys
9
+import subprocess
10
+import re
11
+import requests
12
+import json
13
+import time
14
+import os
9 15
 from diskpartitioner import DiskPartitioner
10 16
 from packageselector import PackageSelector
11 17
 from custompackageselector import CustomPackageSelector
... ...
@@ -17,6 +23,48 @@ from license import License
17 17
 
18 18
 class IsoInstaller(object):
19 19
 
20
+    def get_config(self, path, cd_path):
21
+        if path.startswith("http"):
22
+            # Do 3 trials to get the kick start
23
+            # TODO: make sure the installer run after network is up
24
+            for x in range(0,3):
25
+                err_msg = ""
26
+                try:
27
+                    response = requests.get(path, timeout=3)
28
+                    if response.ok:
29
+                        return json.loads(response.text)
30
+                    err_msg = response.text
31
+                except Exception as e:
32
+                    err_msg = e
33
+                print >> sys.stderr, "Failed to get the kickstart file at {0}, error msg: {1}".format(path, err_msg)
34
+                print "Failed to get the kickstart file at {0}, retry in a second".format(path)
35
+                time.sleep(1)
36
+
37
+
38
+            # Something went wrong
39
+            print "Failed to get the kickstart file at {0}, exiting the installer, check the logs for more details".format(path)
40
+            raise Exception(err_msg)
41
+        else:
42
+            if path.startswith("cdrom:/"):
43
+                path = os.path.join(cd_path, path.replace("cdrom:/", "", 1))
44
+            return (JsonWrapper(path)).read();
45
+
46
+    def mount_RPMS_cd(self):
47
+        # Mount the cd to get the RPMS
48
+        process = subprocess.Popen(['mkdir', '-p', '/mnt/cdrom'])
49
+        retval = process.wait()
50
+
51
+        # Retry mount the CD
52
+        for i in range(0,3):
53
+            process = subprocess.Popen(['mount', '/dev/cdrom', '/mnt/cdrom'])
54
+            retval = process.wait()
55
+            if retval == 0:
56
+                return "/mnt/cdrom"
57
+            print "Failed to mount the cd, retry in a second"
58
+            time.sleep(1)
59
+        print "Failed to mount the cd, exiting the installer, check the logs for more details"
60
+        raise Exception("Can not mount the cd")
61
+    
20 62
     def __init__(self, stdscreen):
21 63
         self.screen = stdscreen
22 64
 
... ...
@@ -33,13 +81,20 @@ class IsoInstaller(object):
33 33
 
34 34
         curses.curs_set(0)
35 35
 
36
-        self.install_config = {}
36
+        self.install_config = {'iso_system': False}
37 37
 
38
-        license_agreement = License(self.maxy, self.maxx)
38
+        # Mount the cd for the RPM, tools, and may be the ks
39
+        cd_path = self.mount_RPMS_cd()
40
+        
41
+        # check the kickstart params
42
+        ks_config = None
43
+        kernel_params = subprocess.check_output(['cat', '/proc/cmdline'])
44
+        m = re.match(r".*ks=(\S+)\s*.*\s*", kernel_params)
45
+        if m != None:
46
+            ks_config = self.get_config(m.group(1), cd_path)
39 47
 
40
-        self.install_config['iso_system'] = False
48
+        license_agreement = License(self.maxy, self.maxx)
41 49
         select_disk = SelectDisk(self.maxy, self.maxx, self.install_config)
42
-
43 50
         package_selector = PackageSelector(self.maxy, self.maxx, self.install_config)
44 51
         custom_package_selector = CustomPackageSelector(self.maxy, self.maxx, self.install_config)
45 52
         hostname_reader = WindowStringReader(self.maxy, self.maxx, 10, 70, False,  'Choose the hostname for your system',
... ...
@@ -48,18 +103,20 @@ class IsoInstaller(object):
48 48
         root_password_reader = WindowStringReader(self.maxy, self.maxx, 10, 70, True,  'Set up root password',
49 49
             'Root password:', 
50 50
             2, self.install_config)
51
-        installer = Installer(self.install_config, self.maxy, self.maxx, True, tools_path=None, rpm_path='cdrom', log_path="/var/log")
51
+        installer = Installer(self.install_config, self.maxy, self.maxx, True, tools_path=cd_path, rpm_path=os.path.join(cd_path, "RPMS"), log_path="/var/log", ks_config=ks_config)
52 52
 
53 53
         # This represents the installer screen, the bool indicated if I can go back to this window or not
54
-        items = [
54
+        items = []
55
+        if not ks_config:
56
+            items = items + [
55 57
                     (license_agreement.display, False),
56 58
                     (select_disk.display, True),
57 59
                     (package_selector.display, True),
58 60
                     (custom_package_selector.display, False),
59 61
                     (hostname_reader.get_user_string, True),
60 62
                     (root_password_reader.get_user_string, True),
61
-                    (installer.install, False)
62
-                ]
63
+                 ]
64
+        items = items + [(installer.install, False)]
63 65
 
64 66
         index = 0
65 67
         params = None
... ...
@@ -36,12 +36,13 @@ PACKAGE_LIST_FILE=$4
36 36
 WORKINGDIR=${BUILDROOT}
37 37
 BUILDROOT=${BUILDROOT}/photon-chroot
38 38
 
39
-mkdir ${WORKINGDIR}/isolinux
40
-cp BUILD_DVD/isolinux/* ${WORKINGDIR}/isolinux/
39
+cp -r BUILD_DVD/isolinux ${WORKINGDIR}/
40
+cp sample_ks.cfg ${WORKINGDIR}/isolinux/
41 41
 
42 42
 find ${BUILDROOT} -name linux-[0-9]*.rpm | head -1 | xargs rpm2cpio | cpio -iv --to-stdout ./boot/vmlinuz* > ${WORKINGDIR}/isolinux/vmlinuz
43 43
 
44 44
 rm -f ${BUILDROOT}/installer/*.pyc
45
+rm -rf ${BUILDROOT}/installer/BUILD_DVD
45 46
 # replace default package_list with specific one
46 47
 cp $PACKAGE_LIST_FILE ${BUILDROOT}/installer/package_list.json
47 48
 
48 49
new file mode 100644
49 50
new file mode 100644
... ...
@@ -0,0 +1,12 @@
0
+import re
1
+
2
+PRE_INSTALL = "pre-install"
3
+POST_INSTALL = "post-install"
4
+
5
+def replace_string_in_file(filename,  search_string,  replace_string):
6
+    with open(filename, "r") as source:
7
+        lines=source.readlines()
8
+
9
+    with open(filename, "w") as destination:
10
+        for line in lines:
11
+            destination.write(re.sub(search_string,  replace_string,  line))
0 12
\ No newline at end of file
1 13
new file mode 100644
... ...
@@ -0,0 +1,24 @@
0
+import os
1
+import commons
2
+from jsonwrapper import JsonWrapper
3
+
4
+install_phase = commons.PRE_INSTALL
5
+enabled = True
6
+
7
+def execute(name, ks_config, config, root):
8
+
9
+    if ks_config:
10
+        package_list = JsonWrapper("package_list.json").read()
11
+
12
+        if ks_config['type'] == 'micro':
13
+            packages = package_list["micro_packages"]
14
+        elif ks_config['type'] == 'minimal':
15
+            packages = package_list["minimal_packages"]
16
+        elif ks_config['type'] == 'full':
17
+            packages = package_list["minimal_packages"] + package_list["optional_packages"]
18
+        else:
19
+            #TODO: error
20
+            packages = []
21
+
22
+        config['type'] = ks_config['type']        
23
+        config["packages"] = packages
0 24
new file mode 100644
... ...
@@ -0,0 +1,35 @@
0
+import os
1
+import subprocess
2
+import commons
3
+
4
+install_phase = commons.PRE_INSTALL
5
+enabled = True
6
+
7
+def partition_disk(disk):
8
+    partitions_data = {}
9
+    partitions_data['disk'] = disk
10
+    partitions_data['root'] = disk + '2'
11
+
12
+    output = open(os.devnull, 'w')
13
+
14
+    # Clear the disk
15
+    process = subprocess.Popen(['sgdisk', '-o', '-g', partitions_data['disk']], stdout = output)
16
+    retval = process.wait()
17
+
18
+    # 1: grub, 2: filesystem
19
+    process = subprocess.Popen(['sgdisk', '-n', '1::+2M', '-n', '2:', '-p', partitions_data['disk']], stdout = output)
20
+    retval = process.wait()
21
+
22
+    # Add the grub flags
23
+    process = subprocess.Popen(['sgdisk', '-t1:ef02', partitions_data['disk']], stdout = output)
24
+    retval = process.wait()
25
+
26
+    # format the file system
27
+    process = subprocess.Popen(['mkfs', '-t', 'ext4', partitions_data['root']], stdout = output)
28
+    retval = process.wait()
29
+    return partitions_data
30
+
31
+def execute(name, ks_config, config, root):
32
+
33
+	if ks_config:
34
+		config['disk'] = partition_disk(ks_config['disk'])
0 35
new file mode 100644
... ...
@@ -0,0 +1,24 @@
0
+import os
1
+import subprocess
2
+import commons
3
+
4
+install_phase = commons.POST_INSTALL
5
+enabled = True
6
+
7
+def execute(name, ks_config, config, root):
8
+
9
+    if ks_config and 'postinstall' in ks_config:
10
+        config['postinstall'] = ks_config['postinstall']
11
+    if 'postinstall' not in config:
12
+    	return
13
+    # run the script in the chroot environment
14
+    script = config['postinstall']
15
+
16
+    script_file = os.path.join(root, 'tmp/postinstall.sh')
17
+
18
+    with open(script_file,  'wb') as outfile:
19
+        outfile.write("\n".join(script))
20
+
21
+    os.chmod(script_file, 0700);
22
+    process = subprocess.Popen(["./mk-run-chroot.sh", '-w', root, "/tmp/postinstall.sh"])
23
+    process.wait()
0 24
new file mode 100644
... ...
@@ -0,0 +1,19 @@
0
+import os
1
+import commons
2
+
3
+install_phase = commons.POST_INSTALL
4
+enabled = True
5
+
6
+def execute(name, ks_config, config, root):
7
+
8
+    if ks_config:
9
+        config["hostname"] = ks_config["hostname"]
10
+    hostname = config['hostname']
11
+
12
+    hostname_file = os.path.join(root, 'etc/hostname')
13
+    hosts_file = os.path.join(root, 'etc/hosts')
14
+
15
+    with open(hostname_file,  'wb') as outfile:
16
+    	outfile.write(hostname)
17
+
18
+    commons.replace_string_in_file(hosts_file, r'127\.0\.0\.1\s+localhost', '127.0.0.1\tlocalhost\n127.0.0.1\t' + hostname)
0 19
new file mode 100644
... ...
@@ -0,0 +1,34 @@
0
+import os
1
+import commons
2
+import crypt
3
+import random
4
+import string
5
+
6
+install_phase = commons.POST_INSTALL
7
+enabled = True
8
+
9
+def execute(name, ks_config, config, root):
10
+
11
+    if ks_config:
12
+        # crypt the password if needed
13
+        if ks_config['password']['crypted']:
14
+            config['password'] = ks_config['password']['text']
15
+        else:
16
+            config['password'] = crypt.crypt(ks_config['password']['text'], 
17
+                "$6$" + "".join([random.choice(string.ascii_letters + string.digits) for _ in range(16)]))
18
+    
19
+    shadow_password = config['password']
20
+
21
+    passwd_filename = os.path.join(root, 'etc/passwd')
22
+    shadow_filename = os.path.join(root, 'etc/shadow')
23
+    
24
+    #replace root blank password in passwd file to point to shadow file
25
+    commons.replace_string_in_file(passwd_filename,  "root::", "root:x:")
26
+
27
+    if os.path.isfile(shadow_filename) == False:
28
+        with open(shadow_filename, "w") as destination:
29
+            destination.write("root:"+shadow_password+":")
30
+    else:
31
+        #add password hash in shadow file
32
+        commons.replace_string_in_file(shadow_filename, "root::", "root:"+shadow_password+":")
33
+
0 34
new file mode 100644
... ...
@@ -0,0 +1,27 @@
0
+import os
1
+import subprocess
2
+import commons
3
+
4
+install_phase = commons.POST_INSTALL
5
+enabled = True
6
+
7
+def execute(name, ks_config, config, root):
8
+    if ks_config and 'public_key' in ks_config:
9
+        config['public_key'] = ks_config['public_key']
10
+    if 'public_key' not in config:
11
+        return
12
+
13
+    authorized_keys_dir = os.path.join(root, "root/.ssh")
14
+    authorized_keys_filename = os.path.join(authorized_keys_dir, "authorized_keys")
15
+    sshd_config_filename = os.path.join(root, "etc/ssh/sshd_config")
16
+
17
+    # Adding the authorized keys
18
+    if not os.path.exists(authorized_keys_dir):
19
+        os.makedirs(authorized_keys_dir)
20
+    with open(authorized_keys_filename, "a") as destination:
21
+        destination.write(config['public_key'] + "\n")
22
+    os.chmod(authorized_keys_filename, 0600)
23
+
24
+    # Change the sshd config to allow root login
25
+    process = subprocess.Popen(["sed", "-i", "s/^\\s*PermitRootLogin\s\+no/PermitRootLogin yes/", sshd_config_filename])
26
+    return process.wait()
... ...
@@ -11,7 +11,7 @@
11 11
                 "linux", "patch", "cpio",
12 12
                 "Linux-PAM", "attr", "libcap", "systemd", "dbus",
13 13
                 "elfutils-libelf", "elfutils-libelf-devel", "sqlite-autoconf", "nspr", "nss-devel", "nss", "popt", "lua", "rpm",
14
-                "which", "gptfdisk", "tar", "gzip", "python2", "python2-devel", "python2-libs", "python2-tools",
14
+                "which", "gptfdisk", "tar", "gzip", "openssl", "python2", "python2-devel", "python2-libs", "python2-tools", "python-requests",
15 15
                 "pcre", "pcre-devel", "glib", "glib-devel", "parted", "libsigc++", "XML-Parser", "glibmm", "dparted",
16 16
                 "libgsystem", "ostree"],
17 17
 
18 18
new file mode 100644
... ...
@@ -0,0 +1,15 @@
0
+{
1
+    "hostname": "photon-machine",
2
+    "password": 
3
+        {
4
+            "crypted": false,
5
+            "text": "VMware123!"
6
+        },
7
+    "disk": "/dev/sda",
8
+    "type": "minimal",
9
+    "postinstall": [
10
+                		"#!/bin/sh",
11
+                    	"echo \"Hello World\" > /etc/postinstall"
12
+                   ],
13
+    "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDNBCufEER/6lBwFp+osVc/2wIZZXRRjvQkA4dPXOTcSfJDIcvRoGDWbEREQK95GJ8BGQ3IJUDXzb2uSt9daPU0FiPqoFHh84pX8M6EVMpyVrEW0xRutt5KBshQ3BQ6Cmw8bstSFtNYZeB7RnR8cU5xMlhtwEkoECwQhJsFtDzVEfrjJLKdEzNMaFlGbX/QxmY434JhpDo+BYjTYMns5lsz+AORPpCmG/48UtsmqAWsWhndYgD9GK53ISMw3LOg2ocmiP0yVYIYNmFmAztzD8g3erqmVlI64HOcn8VYlUUienu0nPt1lX5uNEGXn8fTOBvuTVkHKGs/f8yzWETTytzTweurBI6EeQVt0QP0+XI6osCxNMMvnh/Vx/xl+dgv9EK61pgzttzA8IbWLGUQu9mLAmHa17R/xoygaGUDKGdcZk4EAtAY2RbhO+xBW0nwyt4977CJGIz672H/oSQ3HtNSdIZI/fA7mDWbymSiFZgAXNhnm/jCO7ZR2AjZ7hd9sQzX+mVOH9lGMBEbznYyVOK+Fy5rHqPtQfcAYCfuoVRhJHhwq90jPl4bXv1Z++Nw97M7DJ2uXy+OFu+3HOQ2HliOozwkDUeoUV7pBX9W7idRr6Xb1h3DEE7Xa5R4QEhmCLb0Hr5sg4+0IADIgg7wrBgkA+l+c6EqRNKFYOzy3xAQqw== mbassiouny@vmware.com"
14
+}