a first stab at upgrading nodes
[bootmanager.git] / source / steps / InstallBootstrapFS.py
1 #!/usr/bin/python
2 #
3 # Copyright (c) 2003 Intel Corporation
4 # All rights reserved.
5 #
6 # Copyright (c) 2004-2007 The Trustees of Princeton University
7 # All rights reserved.
8 # expected /proc/partitions format
9
10 import os, string
11 import popen2
12 import shutil
13 import traceback 
14 import time
15
16 from Exceptions import *
17 import utils
18 import BootServerRequest
19 import BootAPI
20
21
22 def Run(vars, upgrade, log):
23     """
24     Download core + extensions bootstrapfs tarballs and install on the hard drive
25
26     the upgrade boolean is True when we are upgrading a node root install while 
27     preserving its slice contents; in that case we just perform extra cleanup
28     before unwrapping the bootstrapfs
29     this is because the running system may have extraneous files
30     that is to say, files that are *not* present in the bootstrapfs
31     and that can impact/clobber the resulting upgrade
32     
33     Expect the following variables from the store:
34     SYSIMG_PATH          the path where the system image will be mounted
35     PARTITIONS           dictionary of generic part. types (root/swap)
36                          and their associated devices.
37     NODE_ID              the id of this machine
38     
39     Sets the following variables:
40     TEMP_BOOTCD_PATH     where the boot cd is remounted in the temp
41                          path
42     ROOT_MOUNTED         set to 1 when the the base logical volumes
43                          are mounted.
44     """
45
46     log.write("\n\nStep: Install: bootstrapfs tarball (upgrade={}).\n".format(upgrade))
47
48     # make sure we have the variables we need
49     try:
50         SYSIMG_PATH = vars["SYSIMG_PATH"]
51         if SYSIMG_PATH == "":
52             raise ValueError("SYSIMG_PATH")
53
54         PARTITIONS = vars["PARTITIONS"]
55         if PARTITIONS == None:
56             raise ValueError("PARTITIONS")
57
58         NODE_ID = vars["NODE_ID"]
59         if NODE_ID == "":
60             raise ValueError("NODE_ID")
61
62         VERSION = vars['VERSION'] or 'unknown'
63
64     except KeyError as var:
65         raise BootManagerException("Missing variable in vars: {}\n".format(var))
66     except ValueError as var:
67         raise BootManagerException("Variable in vars, shouldn't be: {}\n".format(var))
68
69
70     try:
71         # make sure the required partitions exist
72         val = PARTITIONS["root"]
73         val = PARTITIONS["swap"]
74         val = PARTITIONS["vservers"]
75     except KeyError as part:
76         log.write("Missing partition in PARTITIONS: {}\n".format(part))
77         return 0   
78
79     bs_request = BootServerRequest.BootServerRequest(vars)
80     
81     log.write("turning on swap space\n")
82     utils.sysexec("swapon {}".format(PARTITIONS["swap"]), log)
83
84     # make sure the sysimg dir is present
85     utils.makedirs(SYSIMG_PATH)
86
87     log.write("mounting root file system\n")
88     utils.sysexec("mount -t ext3 {} {}".format(PARTITIONS["root"], SYSIMG_PATH), log)
89
90     fstype = 'ext3' if vars['virt']=='vs' else 'btrfs'
91
92     one_partition = vars['ONE_PARTITION']=='1'
93
94     if (not one_partition):
95         log.write("mounting vserver partition in root file system (type {})\n".format(fstype))
96         utils.makedirs(SYSIMG_PATH + "/vservers")
97         utils.sysexec("mount -t {} {} {}/vservers"\
98                       .format(fstype, PARTITIONS["vservers"], SYSIMG_PATH), log)
99
100         if vars['virt']=='lxc':
101             # NOTE: btrfs quota is supported from version: >= btrfs-progs-0.20 (f18+)
102             #       older versions will not recongize the 'quota' command.
103             log.write("Enabling btrfs quota on {}/vservers\n".format(SYSIMG_PATH))
104             utils.sysexec_noerr("btrfs quota enable {}/vservers".format(SYSIMG_PATH))
105
106     vars['ROOT_MOUNTED'] = 1
107
108     # this is now retrieved in GetAndUpdateNodeDetails
109     nodefamily = vars['nodefamily']
110     extensions = vars['extensions']
111
112     # in upgrade mode: we need to cleanup the disk to make
113     # it safe to just untar the new bootstrapfs tarball again
114     # on top of the hard drive
115     if upgrade:
116         CleanupSysimgBeforeUpgrade(SYSIMG_PATH, nodefamily, log)
117
118     # the 'plain' option is for tests mostly
119     plain = vars['plain']
120     if plain:
121         download_suffix = ".tar"
122         uncompress_option = ""
123         log.write("Using plain bootstrapfs images\n")
124     else:
125         download_suffix = ".tar.bz2"
126         uncompress_option = "-j"
127         log.write("Using compressed bootstrapfs images\n")
128
129     log.write ("Using nodefamily={}\n".format(nodefamily))
130     if not extensions:
131         log.write("Installing only core software\n")
132     else:
133         log.write("Requested extensions {}\n".format(extensions))
134     
135     bootstrapfs_names = [ nodefamily ] + extensions
136
137     for name in bootstrapfs_names:
138         tarball = "bootstrapfs-{}{}".format(name, download_suffix)
139         source_file = "/boot/{}".format(tarball)
140         dest_file = "{}/{}".format(SYSIMG_PATH, tarball)
141
142         source_hash_file = "/boot/{}.sha1sum".format(tarball)
143         dest_hash_file = "{}/{}.sha1sum".format(SYSIMG_PATH, tarball)
144
145         time_beg = time.time()
146         log.write("downloading {}\n".format(source_file))
147         # 30 is the connect timeout, 14400 is the max transfer time in
148         # seconds (4 hours)
149         result = bs_request.DownloadFile(source_file, None, None,
150                                          1, 1, dest_file,
151                                          30, 14400)
152         time_end = time.time()
153         duration = int(time_end - time_beg)
154         log.write("Done downloading ({} seconds)\n".format(duration))
155         if result:
156             # Download SHA1 checksum file
157             log.write("downloading sha1sum for {}\n".format(source_file))
158             result = bs_request.DownloadFile(source_hash_file, None, None,
159                                              1, 1, dest_hash_file,
160                                              30, 14400)
161  
162             log.write("verifying sha1sum for {}\n".format(source_file))
163             if not utils.check_file_hash(dest_file, dest_hash_file):
164                 raise BootManagerException(
165                     "FATAL: SHA1 checksum does not match between {} and {}"\
166                     .format(source_file, source_hash_file))
167                 
168             
169             time_beg = time.time()
170             log.write("extracting {} in {}\n".format(dest_file, SYSIMG_PATH))
171             result = utils.sysexec("tar -C {} -xpf {} {}".format(SYSIMG_PATH, dest_file, uncompress_option), log)
172             time_end = time.time()
173             duration = int(time_end - time_beg)
174             log.write("Done extracting ({} seconds)\n".format(duration))
175             utils.removefile(dest_file)
176         else:
177             # the main tarball is required
178             if name == nodefamily:
179                 raise BootManagerException(
180                     "FATAL: Unable to download main tarball {} from server."\
181                     .format(source_file))
182             # for extensions, just issue a warning
183             else:
184                 log.write("WARNING: tarball for extension {} not found\n".format(name))
185
186     # copy resolv.conf from the base system into our temp dir
187     # so DNS lookups work correctly while we are chrooted
188     log.write("Copying resolv.conf to temp dir\n")
189     utils.sysexec("cp /etc/resolv.conf {}/etc/".format(SYSIMG_PATH), log)
190
191     # Copy the boot server certificate(s) and GPG public key to
192     # /usr/boot in the temp dir.
193     log.write("Copying boot server certificates and public key\n")
194
195     if os.path.exists("/usr/boot"):
196         # do nothing in case of upgrade
197         if not os.path.exists(SYSIMG_PATH + "/usr/boot"):
198             utils.makedirs(SYSIMG_PATH + "/usr")
199             shutil.copytree("/usr/boot", SYSIMG_PATH + "/usr/boot")
200     elif os.path.exists("/usr/bootme"):
201         # do nothing in case of upgrade
202         if not os.path.exists(SYSIMG_PATH + "/usr/bootme"):
203             utils.makedirs(SYSIMG_PATH + "/usr/boot")
204             boot_server = file("/usr/bootme/BOOTSERVER").readline().strip()
205             shutil.copy("/usr/bootme/cacert/" + boot_server + "/cacert.pem",
206                         SYSIMG_PATH + "/usr/boot/cacert.pem")
207             file(SYSIMG_PATH + "/usr/boot/boot_server", "w").write(boot_server)
208             shutil.copy("/usr/bootme/pubring.gpg", SYSIMG_PATH + "/usr/boot/pubring.gpg")
209         
210     # For backward compatibility
211     if os.path.exists("/usr/bootme"):
212         # do nothing in case of upgrade
213         if not os.path.exists(SYSIMG_PATH + "/mnt/cdrom/bootme"):
214             utils.makedirs(SYSIMG_PATH + "/mnt/cdrom")
215             shutil.copytree("/usr/bootme", SYSIMG_PATH + "/mnt/cdrom/bootme")
216
217     # ONE_PARTITION => new distribution type
218     if (vars['ONE_PARTITION'] != '1'):
219         # Import the GPG key into the RPM database so that RPMS can be verified
220         utils.makedirs(SYSIMG_PATH + "/etc/pki/rpm-gpg")
221         utils.sysexec("gpg --homedir=/root --export --armor"
222                       " --no-default-keyring --keyring {}/usr/boot/pubring.gpg"
223                       " > {}/etc/pki/rpm-gpg/RPM-GPG-KEY-planetlab".format(SYSIMG_PATH, SYSIMG_PATH), log)
224         utils.sysexec_chroot(SYSIMG_PATH, "rpm --import /etc/pki/rpm-gpg/RPM-GPG-KEY-planetlab", log)
225
226     # keep a log on the installed hdd
227     stamp = file(SYSIMG_PATH + "/bm-install.txt", 'a')
228     now = time.strftime("%Y-%b-%d @ %H:%M %Z", time.gmtime())
229     stamp.write("Hard drive installed by BootManager {}\n".format(VERSION))
230     stamp.write("Finished extraction of bootstrapfs on {}\n".format(now))
231     # do not modify this, the upgrade code uses this line for checking compatibility
232     stamp.write("Using nodefamily {}\n".format(nodefamily))
233     stamp.close()
234
235     return 1
236
237 # the upgrade hook
238 def CleanupSysimgBeforeUpgrade(sysimg, target_nodefamily, log):
239
240     areas_to_cleanup = [
241         '/usr/lib',
242         '/var',
243         '/etc',
244         '/boot',
245     ]
246
247     target_pldistro, target_fcdistro, target_arch = target_nodefamily.split('-')
248
249     # minimal check : not all configurations are possible...
250
251     installed_pldistro, installed_fcdistro, installed_arch = None, None, None
252     installed_virt = None
253     prefix = "Using nodefamily "
254     try:
255         with open("{}/bm-install.txt".format(sysimg)) as infile:
256             for line in infile:
257                 if line.startswith(prefix):
258                     installed_nodefamily = line.replace(prefix,"").strip()
259                     installed_pldistro, installed_fcdistro, installed_arch = installed_nodefamily.split('-')
260         with open("{}/etc/planetlab/virt".format(sysimg)) as infile:
261             installed_virt = infile.read().strip()
262     except Exception as e:
263         print_exc()
264         raise BootManagerException("Could not retrieve data about previous installation - cannot upgrade")
265
266     # moving from vservers to lxc also means another filesystem
267     # so plain reinstall is the only option
268     if installed_virt != 'lxc':
269         raise BootManagerException("Can only upgrade nodes running lxc containers (vservers not supported)")
270
271     # changing arch is not reasonable either
272     if target_arch != installed_arch:
273         raise BootManagerException("Cannot upgrade from arch={} to arch={}"
274                                    .format(installed_arch, target_arch))
275
276     if target_pldistro != installed_pldistro:
277         log.write("\nWARNING: upgrading across pldistros {} to {} - might not work well..\n"
278                   .format(installed_pldistro, target_pldistro))
279     
280     # otherwise at this point we do not do any more advanced checking
281     log.write("\n\nPseudo step CleanupSysimgBeforeUpgrade : cleaning up hard drive\n")
282     
283     for area in areas_to_cleanup:
284         utils.sysexec("rm -rf {}/{}".format(sysimg, area))