replace most of subprocess calls by new runner apis
[tools/mic.git] / mic / utils / partitionedfs.py
1 #
2 # partitionedfs.py: partitioned files system class, extends fs.py
3 #
4 # Copyright 2007-2008, Red Hat  Inc.
5 # Copyright 2008, Daniel P. Berrange
6 # Copyright 2008,  David P. Huff
7 #
8 # Copyright 2009, 2010, 2011 Intel, Inc.
9 #
10 # This program is free software; you can redistribute it and/or modify
11 # it under the terms of the GNU General Public License as published by
12 # the Free Software Foundation; version 2 of the License.
13 #
14 # This program is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17 # GNU Library General Public License for more details.
18 #
19 # You should have received a copy of the GNU General Public License
20 # along with this program; if not, write to the Free Software
21 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
22
23 import os
24
25 from mic import msger
26 from mic.utils import runner
27 from mic.utils.errors import MountError
28 from mic.utils.fs_related import *
29
30 class PartitionedMount(Mount):
31     def __init__(self, disks, mountdir, skipformat = False):
32         Mount.__init__(self, mountdir)
33         self.disks = {}
34         for name in disks.keys():
35             self.disks[name] = { 'disk': disks[name],  # Disk object
36                                  'mapped': False, # True if kpartx mapping exists
37                                  'numpart': 0, # Number of allocate partitions
38                                  'partitions': [], # indexes to self.partitions
39                                  # Partitions with part num higher than 3 will
40                                  # be put inside extended partition.
41                                  'extended': 0, # Size of extended partition
42                                  # Sector 0 is used by the MBR and can't be used
43                                  # as the start, so setting offset to 1.
44                                  'offset': 1 } # Offset of next partition (in sectors)
45
46         self.partitions = []
47         self.subvolumes = []
48         self.mapped = False
49         self.mountOrder = []
50         self.unmountOrder = []
51         self.parted=find_binary_path("parted")
52         self.kpartx=find_binary_path("kpartx")
53         self.mkswap=find_binary_path("mkswap")
54         self.btrfscmd=None
55         self.mountcmd=find_binary_path("mount")
56         self.umountcmd=find_binary_path("umount")
57         self.skipformat = skipformat
58         self.snapshot_created = self.skipformat
59         # Size of a sector used in calculations
60         self.sector_size = 512
61
62     def add_partition(self, size, disk, mountpoint, fstype = None, fsopts = None, boot = False):
63         # Converting M to s for parted
64         size = size * 1024 * 1024 / self.sector_size
65
66         """ We need to handle subvolumes for btrfs """
67         if fstype == "btrfs" and fsopts and fsopts.find("subvol=") != -1:
68             self.btrfscmd=find_binary_path("btrfs")
69             subvol = None
70             opts = fsopts.split(",")
71             for opt in opts:
72                 if opt.find("subvol=") != -1:
73                     subvol = opt.replace("subvol=", "").strip()
74                     break
75             if not subvol:
76                 raise MountError("No subvolume: %s" % fsopts)
77             self.subvolumes.append({'size': size, # In sectors
78                                     'mountpoint': mountpoint, # Mount relative to chroot
79                                     'fstype': fstype, # Filesystem type
80                                     'fsopts': fsopts, # Filesystem mount options
81                                     'disk': disk, # physical disk name holding partition
82                                     'device': None, # kpartx device node for partition
83                                     'mount': None, # Mount object
84                                     'subvol': subvol, # Subvolume name
85                                     'boot': boot, # Bootable flag
86                                     'mounted': False # Mount flag
87                                    })
88
89         """ We still need partition for "/" or non-subvolume """
90         if mountpoint == "/" or not fsopts or fsopts.find("subvol=") == -1:
91             """ Don't need subvolume for "/" because it will be set as default subvolume """
92             if fsopts and fsopts.find("subvol=") != -1:
93                 opts = fsopts.split(",")
94                 for opt in opts:
95                     if opt.strip().startswith("subvol="):
96                         opts.remove(opt)
97                         break
98                 fsopts = ",".join(opts)
99             self.partitions.append({'size': size, # In sectors
100                                     'mountpoint': mountpoint, # Mount relative to chroot
101                                     'fstype': fstype, # Filesystem type
102                                     'fsopts': fsopts, # Filesystem mount options
103                                     'disk': disk, # physical disk name holding partition
104                                     'device': None, # kpartx device node for partition
105                                     'mount': None, # Mount object
106                                     'num': None, # Partition number
107                                     'boot': boot}) # Bootable flag
108
109     def __create_part_to_image(self,device, parttype, fstype, start, size):
110         # Start is included to the size so we need to substract one from the end.
111         end = start+size-1
112         msger.debug("Added '%s' part at %d of size %d" % (parttype,start,end))
113         part_cmd = [self.parted, "-s", device, "unit", "s", "mkpart", parttype]
114         if fstype:
115             part_cmd.extend([fstype])
116         part_cmd.extend(["%d" % start, "%d" % end])
117
118         msger.debug(part_cmd)
119         rc, out = runner.runtool(part_cmd, catch=3)
120         out = out.strip()
121         if out:
122             msger.debug('"parted" output: %s' % out)
123         return rc
124
125     def __format_disks(self):
126         msger.debug("Assigning partitions to disks")
127
128         mbr_sector_skipped = False
129
130         for n in range(len(self.partitions)):
131             p = self.partitions[n]
132
133             if not self.disks.has_key(p['disk']):
134                 raise MountError("No disk %s for partition %s" % (p['disk'], p['mountpoint']))
135
136             if not mbr_sector_skipped:
137                 # This hack is used to remove one sector from the first partition,
138                 # that is the used to the MBR.
139                 p['size'] -= 1
140                 mbr_sector_skipped = True
141
142             d = self.disks[p['disk']]
143             d['numpart'] += 1
144             if d['numpart'] > 3:
145                 # Increase allocation of extended partition to hold this partition
146                 d['extended'] += p['size']
147                 p['type'] = 'logical'
148                 p['num'] = d['numpart'] + 1
149             else:
150                 p['type'] = 'primary'
151                 p['num'] = d['numpart']
152
153             p['start'] = d['offset']
154             d['offset'] += p['size']
155             d['partitions'].append(n)
156             msger.debug("Assigned %s to %s%d at %d at size %d" % (p['mountpoint'], p['disk'], p['num'], p['start'], p['size']))
157
158         if self.skipformat:
159             msger.debug("Skipping disk format, because skipformat flag is set.")
160             return
161
162         for dev in self.disks.keys():
163             d = self.disks[dev]
164             msger.debug("Initializing partition table for %s" % (d['disk'].device))
165             rc, out = runner.runtool([self.parted, "-s", d['disk'].device, "mklabel", "msdos"], catch=3)
166             out = out.strip()
167             if out:
168                 msger.debug('"parted" output: %s' % out)
169
170             if rc != 0:
171                 # NOTE: We don't throw exception when return code is not 0, because
172                 # parted always fails to reload part table with loop devices.
173                 # This prevents us from distinguishing real errors based on return code.
174                 msger.debug("WARNING: parted returned '%s' instead of 0 when creating partition-table for disk '%s'." % (p1.returncode,d['disk'].device))
175
176         msger.debug("Creating partitions")
177
178         for p in self.partitions:
179             d = self.disks[p['disk']]
180             if p['num'] == 5:
181                 self.__create_part_to_image(d['disk'].device,"extended",None,p['start'],d['extended'])
182
183             if p['fstype'] == "swap":
184                 parted_fs_type = "linux-swap"
185             elif p['fstype'] == "vfat":
186                 parted_fs_type = "fat32"
187             elif p['fstype'] == "msdos":
188                 parted_fs_type = "fat16"
189             else:
190                 # Type for ext2/ext3/ext4/btrfs
191                 parted_fs_type = "ext2"
192
193             # Boot ROM of OMAP boards require vfat boot partition to have an
194             # even number of sectors.
195             if p['mountpoint'] == "/boot" and p['fstype'] in ["vfat","msdos"] and p['size'] % 2:
196                 msger.debug("Substracting one sector from '%s' partition to get even number of sectors for the partition." % (p['mountpoint']))
197                 p['size'] -= 1
198
199             ret = self.__create_part_to_image(d['disk'].device,p['type'],
200                                              parted_fs_type, p['start'],
201                                              p['size'])
202
203             if ret != 0:
204                 # NOTE: We don't throw exception when return code is not 0, because
205                 # parted always fails to reload part table with loop devices.
206                 # This prevents us from distinguishing real errors based on return code.
207                 msger.debug("WARNING: parted returned '%s' instead of 0 when creating partition '%s' for disk '%s'." % (p1.returncode,p['mountpoint'],d['disk'].device))
208
209             if p['boot']:
210                 msger.debug("Setting boot flag for partition '%s' on disk '%s'." % (p['num'],d['disk'].device))
211                 boot_cmd = [self.parted, "-s", d['disk'].device, "set", "%d" % p['num'], "boot", "on"]
212                 msger.debug(boot_cmd)
213                 rc = runner.show(boot_cmd)
214
215                 if rc != 0:
216                     # NOTE: We don't throw exception when return code is not 0, because
217                     # parted always fails to reload part table with loop devices.
218                     # This prevents us from distinguishing real errors based on return code.
219                     msger.warning("parted returned '%s' instead of 0 when adding boot flag for partition '%s' disk '%s'." % (rc,p['num'],d['disk'].device))
220
221     def __map_partitions(self):
222         """Load it if dm_snapshot isn't loaded"""
223         load_module("dm_snapshot")
224
225         for dev in self.disks.keys():
226             d = self.disks[dev]
227             if d['mapped']:
228                 continue
229
230             msger.debug("Running kpartx on %s" % d['disk'].device )
231             rc, kpartxOutput = runner.runtool([self.kpartx, "-l", "-v", d['disk'].device])
232             kpartxOutput = kpartxOutput.splitlines()
233
234             if rc != 0:
235                 raise MountError("Failed to query partition mapping for '%s'" %
236                                  d['disk'].device)
237
238             # Strip trailing blank and mask verbose output
239             i = 0
240             while i < len(kpartxOutput) and kpartxOutput[i][0:4] != "loop":
241                i = i + 1
242             kpartxOutput = kpartxOutput[i:]
243
244             # Quick sanity check that the number of partitions matches
245             # our expectation. If it doesn't, someone broke the code
246             # further up
247             if len(kpartxOutput) != d['numpart']:
248                 raise MountError("Unexpected number of partitions from kpartx: %d != %d" %
249                                  (len(kpartxOutput), d['numpart']))
250
251             for i in range(len(kpartxOutput)):
252                 line = kpartxOutput[i]
253                 newdev = line.split()[0]
254                 mapperdev = "/dev/mapper/" + newdev
255                 loopdev = d['disk'].device + newdev[-1]
256
257                 msger.debug("Dev %s: %s -> %s" % (newdev, loopdev, mapperdev))
258                 pnum = d['partitions'][i]
259                 self.partitions[pnum]['device'] = loopdev
260
261                 # grub's install wants partitions to be named
262                 # to match their parent device + partition num
263                 # kpartx doesn't work like this, so we add compat
264                 # symlinks to point to /dev/mapper
265                 if os.path.lexists(loopdev):
266                     os.unlink(loopdev)
267                 os.symlink(mapperdev, loopdev)
268
269             msger.debug("Adding partx mapping for %s" % d['disk'].device)
270             rc = runner.show([self.kpartx, "-v", "-a", d['disk'].device])
271
272             if rc != 0:
273                 # Make sure that the device maps are also removed on error case.
274                 # The d['mapped'] isn't set to True if the kpartx fails so
275                 # failed mapping will not be cleaned on cleanup either.
276                 runner.quiet([self.kpartx, "-d", d['disk'].device])
277                 raise MountError("Failed to map partitions for '%s'" %
278                                  d['disk'].device)
279
280             d['mapped'] = True
281
282     def __unmap_partitions(self):
283         for dev in self.disks.keys():
284             d = self.disks[dev]
285             if not d['mapped']:
286                 continue
287
288             msger.debug("Removing compat symlinks")
289             for pnum in d['partitions']:
290                 if self.partitions[pnum]['device'] != None:
291                     os.unlink(self.partitions[pnum]['device'])
292                     self.partitions[pnum]['device'] = None
293
294             msger.debug("Unmapping %s" % d['disk'].device)
295             rc = runner.quiet([self.kpartx, "-d", d['disk'].device])
296             if rc != 0:
297                 raise MountError("Failed to unmap partitions for '%s'" %
298                                  d['disk'].device)
299
300             d['mapped'] = False
301
302     def __calculate_mountorder(self):
303         msger.debug("Calculating mount order")
304         for p in self.partitions:
305             self.mountOrder.append(p['mountpoint'])
306             self.unmountOrder.append(p['mountpoint'])
307
308         self.mountOrder.sort()
309         self.unmountOrder.sort()
310         self.unmountOrder.reverse()
311
312     def cleanup(self):
313         Mount.cleanup(self)
314         self.__unmap_partitions()
315         for dev in self.disks.keys():
316             d = self.disks[dev]
317             try:
318                 d['disk'].cleanup()
319             except:
320                 pass
321
322     def unmount(self):
323         self.__unmount_subvolumes()
324         for mp in self.unmountOrder:
325             if mp == 'swap':
326                 continue
327             p = None
328             for p1 in self.partitions:
329                 if p1['mountpoint'] == mp:
330                     p = p1
331                     break
332
333             if p['mount'] != None:
334                 try:
335                     """ Create subvolume snapshot here """
336                     if p['fstype'] == "btrfs" and p['mountpoint'] == "/" and not self.snapshot_created:
337                         self.__create_subvolume_snapshots(p, p["mount"])
338                     p['mount'].cleanup()
339                 except:
340                     pass
341                 p['mount'] = None
342
343     """ Only for btrfs """
344     def __get_subvolume_id(self, rootpath, subvol):
345         if not self.btrfscmd:
346             self.btrfscmd=find_binary_path("btrfs")
347         argv = [ self.btrfscmd, "subvolume", "list", rootpath ]
348
349         rc, out = runner.runtool(argv)
350         msger.debug(out)
351
352         if rc != 0:
353             raise MountError("Failed to get subvolume id from %s', return code: %d." % (rootpath, rc))
354
355         subvolid = -1
356         for line in out.splitlines():
357             if line.endswith(" path %s" % subvol):
358                 subvolid = line.split()[1]
359                 if not subvolid.isdigit():
360                     raise MountError("Invalid subvolume id: %s" % subvolid)
361                 subvolid = int(subvolid)
362                 break
363         return subvolid
364
365     def __create_subvolume_metadata(self, p, pdisk):
366         if len(self.subvolumes) == 0:
367             return
368
369         argv = [ self.btrfscmd, "subvolume", "list", pdisk.mountdir ]
370         rc, out = runner.runtool(argv)
371         msger.debug(out)
372
373         if rc != 0:
374             raise MountError("Failed to get subvolume id from %s', return code: %d." % (pdisk.mountdir, rc))
375
376         subvolid_items = out.splitlines()
377         subvolume_metadata = ""
378         for subvol in self.subvolumes:
379             for line in subvolid_items:
380                 if line.endswith(" path %s" % subvol["subvol"]):
381                     subvolid = line.split()[1]
382                     if not subvolid.isdigit():
383                         raise MountError("Invalid subvolume id: %s" % subvolid)
384
385                     subvolid = int(subvolid)
386                     opts = subvol["fsopts"].split(",")
387                     for opt in opts:
388                         if opt.strip().startswith("subvol="):
389                             opts.remove(opt)
390                             break
391                     fsopts = ",".join(opts)
392                     subvolume_metadata += "%d\t%s\t%s\t%s\n" % (subvolid, subvol["subvol"], subvol['mountpoint'], fsopts)
393
394         if subvolume_metadata:
395             fd = open("%s/.subvolume_metadata" % pdisk.mountdir, "w")
396             fd.write(subvolume_metadata)
397             fd.close()
398
399     def __get_subvolume_metadata(self, p, pdisk):
400         subvolume_metadata_file = "%s/.subvolume_metadata" % pdisk.mountdir
401         if not os.path.exists(subvolume_metadata_file):
402             return
403
404         fd = open(subvolume_metadata_file, "r")
405         content = fd.read()
406         fd.close()
407
408         for line in content.splitlines():
409             items = line.split("\t")
410             if items and len(items) == 4:
411                 self.subvolumes.append({'size': 0, # In sectors
412                                         'mountpoint': items[2], # Mount relative to chroot
413                                         'fstype': "btrfs", # Filesystem type
414                                         'fsopts': items[3] + ",subvol=%s" %  items[1], # Filesystem mount options
415                                         'disk': p['disk'], # physical disk name holding partition
416                                         'device': None, # kpartx device node for partition
417                                         'mount': None, # Mount object
418                                         'subvol': items[1], # Subvolume name
419                                         'boot': False, # Bootable flag
420                                         'mounted': False # Mount flag
421                                    })
422
423     def __create_subvolumes(self, p, pdisk):
424         """ Create all the subvolumes """
425
426         for subvol in self.subvolumes:
427             argv = [ self.btrfscmd, "subvolume", "create", pdisk.mountdir + "/" + subvol["subvol"]]
428
429             rc = runner.show(argv)
430             if rc != 0:
431                 raise MountError("Failed to create subvolume '%s', return code: %d." % (subvol["subvol"], rc))
432
433         """ Set default subvolume, subvolume for "/" is default """
434         subvol = None
435         for subvolume in self.subvolumes:
436             if subvolume["mountpoint"] == "/" and p["disk"] == subvolume["disk"]:
437                 subvol = subvolume
438                 break
439
440         if subvol:
441             """ Get default subvolume id """
442             subvolid = self. __get_subvolume_id(pdisk.mountdir, subvol["subvol"])
443             """ Set default subvolume """
444             if subvolid != -1:
445                 rc = runner.show([ self.btrfscmd, "subvolume", "set-default", "%d" % subvolid, pdisk.mountdir])
446                 if rc != 0:
447                     raise MountError("Failed to set default subvolume id: %d', return code: %d." % (subvolid, rc))
448
449         self.__create_subvolume_metadata(p, pdisk)
450
451     def __mount_subvolumes(self, p, pdisk):
452         if self.skipformat:
453             """ Get subvolume info """
454             self.__get_subvolume_metadata(p, pdisk)
455             """ Set default mount options """
456             if len(self.subvolumes) != 0:
457                 for subvol in self.subvolumes:
458                     if subvol["mountpoint"] == p["mountpoint"] == "/":
459                         opts = subvol["fsopts"].split(",")
460                         for opt in opts:
461                             if opt.strip().startswith("subvol="):
462                                 opts.remove(opt)
463                                 break
464                         pdisk.fsopts = ",".join(opts)
465                         break
466
467         if len(self.subvolumes) == 0:
468             """ Return directly if no subvolumes """
469             return
470
471         """ Remount to make default subvolume mounted """
472         rc = runner.show([self.umountcmd, pdisk.mountdir])
473         if rc != 0:
474             raise MountError("Failed to umount %s" % pdisk.mountdir)
475
476         rc = runner.show([self.mountcmd, "-o", pdisk.fsopts, pdisk.disk.device, pdisk.mountdir])
477         if rc != 0:
478             raise MountError("Failed to umount %s" % pdisk.mountdir)
479
480         for subvol in self.subvolumes:
481             if subvol["mountpoint"] == "/":
482                 continue
483             subvolid = self. __get_subvolume_id(pdisk.mountdir, subvol["subvol"])
484             if subvolid == -1:
485                 msger.debug("WARNING: invalid subvolume %s" % subvol["subvol"])
486                 continue
487             """ Replace subvolume name with subvolume ID """
488             opts = subvol["fsopts"].split(",")
489             for opt in opts:
490                 if opt.strip().startswith("subvol="):
491                     opts.remove(opt)
492                     break
493             #opts.append("subvolid=%d" % subvolid)
494             opts.extend(["subvolrootid=0", "subvol=%s" % subvol["subvol"]])
495             fsopts = ",".join(opts)
496             subvol['fsopts'] = fsopts
497             mountpoint = self.mountdir + subvol['mountpoint']
498             makedirs(mountpoint)
499             rc = runner.show([self.mountcmd, "-o", fsopts, pdisk.disk.device, mountpoint])
500             if rc != 0:
501                 raise MountError("Failed to mount subvolume %s to %s" % (subvol["subvol"], mountpoint))
502             subvol["mounted"] = True
503
504     def __unmount_subvolumes(self):
505         """ It may be called multiple times, so we need to chekc if it is still mounted. """
506         for subvol in self.subvolumes:
507             if subvol["mountpoint"] == "/":
508                 continue
509             if not subvol["mounted"]:
510                 continue
511             mountpoint = self.mountdir + subvol['mountpoint']
512             rc = runner.show([self.umountcmd, mountpoint])
513             if rc != 0:
514                 raise MountError("Failed to unmount subvolume %s from %s" % (subvol["subvol"], mountpoint))
515             subvol["mounted"] = False
516
517     def __create_subvolume_snapshots(self, p, pdisk):
518         import time
519
520         if self.snapshot_created:
521             return
522
523         """ Remount with subvolid=0 """
524         rc = runner.show([self.umountcmd, pdisk.mountdir])
525         if rc != 0:
526             raise MountError("Failed to umount %s" % pdisk.mountdir)
527         if pdisk.fsopts:
528             mountopts = pdisk.fsopts + ",subvolid=0"
529         else:
530             mountopts = "subvolid=0"
531         rc = runner.show([self.mountcmd, "-o", mountopts, pdisk.disk.device, pdisk.mountdir])
532         if rc != 0:
533             raise MountError("Failed to umount %s" % pdisk.mountdir)
534
535         """ Create all the subvolume snapshots """
536         snapshotts = time.strftime("%Y%m%d-%H%M")
537         for subvol in self.subvolumes:
538             subvolpath = pdisk.mountdir + "/" + subvol["subvol"]
539             snapshotpath = subvolpath + "_%s-1" % snapshotts
540             rc = runner.show([ self.btrfscmd, "subvolume", "snapshot", subvolpath, snapshotpath ])
541             if rc != 0:
542                 raise MountError("Failed to create subvolume snapshot '%s' for '%s', return code: %d." % (snapshotpath, subvolpath, rc))
543
544         self.snapshot_created = True
545
546     def mount(self):
547         for dev in self.disks.keys():
548             d = self.disks[dev]
549             d['disk'].create()
550
551         self.__format_disks()
552         self.__map_partitions()
553         self.__calculate_mountorder()
554
555         for mp in self.mountOrder:
556             p = None
557             for p1 in self.partitions:
558                 if p1['mountpoint'] == mp:
559                     p = p1
560                     break
561
562             if mp == 'swap':
563                 runner.show([self.mkswap, p['device']])
564                 continue
565
566             rmmountdir = False
567             if p['mountpoint'] == "/":
568                 rmmountdir = True
569             if p['fstype'] == "vfat" or p['fstype'] == "msdos":
570                 myDiskMount = VfatDiskMount
571             elif p['fstype'] in ("ext2", "ext3", "ext4"):
572                 myDiskMount = ExtDiskMount
573             elif p['fstype'] == "btrfs":
574                 myDiskMount = BtrfsDiskMount
575             else:
576                 raise MountError("Fail to support file system " + p['fstype'])
577
578             if p['fstype'] == "btrfs" and not p['fsopts']:
579                 p['fsopts'] = "subvolid=0"
580
581             pdisk = myDiskMount(RawDisk(p['size'] * self.sector_size, p['device']),
582                                  self.mountdir + p['mountpoint'],
583                                  p['fstype'],
584                                  4096,
585                                  p['mountpoint'],
586                                  rmmountdir,
587                                  self.skipformat,
588                                  fsopts = p['fsopts'])
589             pdisk.mount(pdisk.fsopts)
590             if p['fstype'] == "btrfs" and p['mountpoint'] == "/":
591                 if not self.skipformat:
592                     self.__create_subvolumes(p, pdisk)
593                 self.__mount_subvolumes(p, pdisk)
594             p['mount'] = pdisk
595
596     def resparse(self, size = None):
597         # Can't re-sparse a disk image - too hard
598         pass