#!/usr/bin/python """ Written by Igor Pashev The author has placed this work in the Public Domain, thereby relinquishing all copyrights. Everyone is free to use, modify, republish, sell or give away this work without prior consent from anybody. """ from __future__ import print_function from lib.hdd import HDD from lib.snack import * from snack import * from subprocess import Popen, PIPE, call from pprint import pprint from tempfile import mkstemp, mkdtemp from urllib2 import urlopen, URLError, HTTPError from time import sleep from os.path import basename import os import re import sys import math # Snack screen screen = None # List of disks installed on the system: hdds = [] # List of zpools found on system: zpools = [] # List of imported zpools, we should export them after installation: imported_zpools = [] # Name of the root ZFS pool: either existing or newly created rpool = None # Name of the root slice, where ZFS root pool will be created (e. g. c0t0d0s0) rslice = None # Created boot environment, e. g. rpoot/ROOT/osdy-0 bootenv = None # Install root dir rootdir = None # Version to install: codename = 'bok' # Arch to install: # XXX Use dpkg-architecture when we support more ;-) archname = 'illumos-amd64' # Dyson mirrors: mirror = None mirrors = [ ('http://apt.osdyson.org', 'Russia'), ('http://mirror-us.osdyson.org/apt/', 'USA'), ] # Stages of debootstraping in order. Debootstrap gives # progress for each stage, but we need entire progress. # Format: mile : (a, z), where: # - mile is a stage name, e. g. DOWNDEBS; # - a, z - the start and the end position on progress bar, # e. i. a=0 z=100 - entire progress bar (100%); miles = { 'START' : (0, 0), # Dummy 'DOWNREL' : (0, 1), # Downloading the 'Release' file 'DOWNPKGS' : (1, 5), # Downloading the 'Packages' file 'SIZEDEBS' : (5, 10), # Finding packags sizes 'DOWNDEBS' : (10, 25), # Downloading packages (*.deb) 'EXTRACTPKGS' : (25, 45), # Extracting core packages 'INSTCORE' : (45, 50), # Installing core packages 'UNPACKREQ' : (50, 60), # Unpacking required packages 'CONFREQ' : (60, 70), # Configuring required packages 'UNPACKBASE' : (70, 85), # Unpacking the base system 'CONFBASE' : (85, 100), # Configuring the base system } # FS to be created in boot environment. # Order is important. befs = [ {'name':'/usr', 'options':{}}, {'name':'/usr/local', 'options':{ 'compression':'on', }}, {'name':'/usr/src', 'options':{ 'compression':'on', }}, {'name':'/var', 'options':{}}, {'name':'/var/cache', 'options':{}}, {'name':'/var/lib', 'options':{}}, {'name':'/var/lib/dpkg', 'options':{ 'compression' : 'on', 'setuid' : 'off', 'devices' : 'off', }}, {'name':'/var/log', 'options':{ 'compression' : 'on', 'setuid' : 'off', 'devices' : 'off', }}, {'name':'/var/mail', 'options':{ 'compression' : 'on', 'setuid' : 'off', 'devices' : 'off', }}, {'name':'/var/spool', 'options':{ 'compression' : 'on', 'setuid' : 'off', 'devices' : 'off', }}, {'name':'/var/tmp', 'options':{ 'compression' : 'on', 'setuid' : 'off', 'devices' : 'off', }}, ] # XXX Not yet. befs = [] class Abort(Exception): ''' User clicked "Cancel" ''' pass class NoDisks(Exception): ''' No Hard Drives found ''' pass def welcome(): welcome = ButtonChoiceWindow(screen, "Welcome to the Dyson installer", ''' Dyson is an operating system derived from Debian and based on Illumos core. \ It uses Illumos unix kernel and libc, ZFS file system and SMF to manage system startup. \ To learn more visit http://osdyson.org This installer will guide you through disk paritioning, filesystem creation, \ installation of base system and minimal configuration. Please note, this system IS VERY EXPERIMENTAL. \ You can LOOSE ALL YOUR DATA which this system can reach ;-) Whould you like to continue?''', buttons=[('Install Dyson', True), ('Exit the installer', False)], width=70) if not welcome: raise Abort('Installation canceled') def find_zpools(): global zpools progress = ProgressMessage(screen, title='Please wait', text='Searching for ZFS pools ...') zpools = [] zpool_cmd = Popen(['zpool', 'list', '-H', '-o', 'name'], stdout=PIPE, stderr=PIPE) for line in zpool_cmd.stdout: zpools.append(line.rstrip()) zpool_cmd = Popen("zpool import | awk '/pool:/ {print $2}'", shell=True, stdout=PIPE, stderr=PIPE) for line in zpool_cmd.stdout: zpools.append(line.rstrip()) def find_hdds(): global hdds progress = ProgressMessage(screen, title='Please wait', text='Searching for disks ...') hdds = [] pat = re.compile('\d+\.\s+(\S+)\s+<(.+?)(\s+cyl.*)?>') format_cmd = Popen('format 0: rpool = choose_rpool() if not rpool: hdd = choose_hdd(len(zpools)) if hdd: rpool = create_rpool(hdd) if rpool: if not pool_is_imported(rpool): progress = ProgressMessage(screen, title='Please wait', text='Importing ZFS pool "{}" ...'.format(rpool)) zpool_cmd = Popen(['zpool', 'import', '-R', '/mnt/'+rpool, '-f', rpool], stderr=PIPE, stdout=PIPE) out, err = zpool_cmd.communicate() if 0 != zpool_cmd.returncode: ButtonChoiceWindow(screen, title='Importing of ZFS pool "{}" failed'.format(rpool), text=err, buttons=['Ok'], width=70) rpool = None imported_zpools.append(rpool) def zfs_exists(fs): fs_exists = call(['zfs', 'list', '-H', fs], stdout=PIPE, stderr=PIPE) return fs_exists == 0 def zfs_create(fs, zvol=None, options={}): args = ['zfs', 'create'] if zvol: args += ['-V', zvol] for o in options: args += ['-o', o + '=' + options[o]] args.append(fs) zfs_cmd = Popen(args, stderr=PIPE, stdout=PIPE) out, err = zfs_cmd.communicate() if 0 != zfs_cmd.returncode: ButtonChoiceWindow(screen, title='Creating of ZFS dataset failed', text=err, buttons = ['Ok'], width=70) return False return True def configure_zfs(): global bootenv global rootdir progress = ProgressBar(screen, title='Creating ZFS filesystems', top = 4 + len(befs), # 4 for ROOT, /swap, /home, and BE width = 70, ) root = rpool + '/ROOT' if not zfs_exists(root): progress.text = 'Creating ' + root zfs_create(root, options={'canmount':'off', 'mountpoint':'none'}) progress.advance() swap = rpool + '/swap' if not zfs_exists(swap): progress.text = 'Creating ' + swap zfs_create(swap, zvol='2G') progress.advance() home = rpool + '/home' if not zfs_exists(home): progress.text = 'Creating ' + home zfs_create(home, options={'mountpoint':'/home', 'compression':'on', 'devices':'off', 'setuid':'off'}) progress.advance() # Make sure we are creating new boot environment for be_no in range(50): bootenv = root + '/osdy-' + str(be_no) if not zfs_exists(bootenv): break else: bootenv = None rootdir = mkdtemp(prefix='install-') progress.text = 'Creating ' + bootenv zfs_create(bootenv, options={'mountpoint':'legacy'}) call(['mount', '-F', 'zfs', bootenv, rootdir]) progress.advance() for fs in befs: p = bootenv + fs['name'] progress.text = 'Creating ' + p zfs_create(p, options=fs['options']) progress.advance() call(['zfs', 'snapshot', '-r', bootenv + '@empty']) def choose_hdd(number_of_zpools = 0): hdd_items = map(lambda x: '{}: {} {}'.format(x.name, x.capacity, x.description), hdds) choice = None buttons = [] buttons.append(('Use selected disk', 'ok')) if number_of_zpools > 0: buttons.append(('Back', 'zpool')) buttons.append(('Cancel', 'cancel')) while not choice in ['ok', 'zpool', 'cancel']: (choice, hdd_no) = ListboxChoiceWindow(screen, title='Choose hard disk drive', text='Choose a disk for the root ZFS pool', items=hdd_items, buttons=buttons, width=76, scroll=1, default=0, help=None ) if choice in ['ok', None]: choice = configure_partitions(hdds[hdd_no], len(hdds)) if choice == 'another': choice = None if choice == 'ok': return hdds[hdd_no] if choice == 'zpool': return None raise Abort('HDD choosing') def configure_partitions(hdd, number_of_disks = 1): choice = None top_text = 'For the root ZFS pool you need a disk containing a Solaris partition (id 0xbf). \ If this disk includes such a partition and you are happy with it, use this disk. \ Otherwise you have to change partitioning. Note that only the first Solaris partition \ will be used.' while not choice in ['ok', 'another', 'cancel']: part_info = top_text part_info += '\n\n' have_partitions = len(hdd.partitions) > 0 have_solaris_partition = False have_gpt = False buttons = [] if have_partitions: part_info += 'Partitions on {name} {desc} {size}:\n\n'.format( name=hdd.name, desc=hdd.description, size=hdd.capacity) part_info += ' TYPE ID NAME SIZE\n' n = 0 for p in hdd.partitions: n = n + 1 if p.id == 0xBF: have_solaris_partition = True if p.id == 0xEE: have_gpt = True part_info += '#{n:<2} {primary} {id:#04x} {system:25} {capacity:10}\n'.format( n=n, capacity=p.capacity, system=p.system, id=p.id, primary=['primary', 'logical'][p.logical]) else: part_info += 'This disk is not partitioned.\n' if have_solaris_partition: buttons.append(('Use this disk', 'ok')) if have_gpt: part_info += '\nThis disk has GUID Partition Table (GPT) and is unsupported.\n' # XXX gparted else: buttons.append(('Edit partitions', 'fdisk')) if number_of_disks > 1: buttons.append(('Back', 'another')) buttons.append(('Cancel', 'cancel')) choice = ButtonChoiceWindow(screen, title='Partitions of {}'.format(hdd.name), text=part_info, buttons=buttons, width=76, help=None ) if choice == 'fdisk': screen.suspend() call(['cfdisk', hdd.raw_device]) hdd.reread_partitions() screen.resume() # end while if choice == 'cancel': raise Abort('Partitioning') return choice def get_mirror(): if not hasattr(get_mirror, "m"): get_mirror.m='' while True: (choice, entry) = EntryWindow(screen, title='Enter APT repository URL', text='If you want to have line ' '"deb ftp://192.168.1.1 bok main" in /etc/apt/sources.list ' 'enter "ftp://192.168.1.1" here.\n' 'Remember to add URL protocol.\n', prompts=[('URL', get_mirror.m)], buttons = [ ('Ok', 'ok'), ('Back', 'back'), ('Cancel', 'cancel')], width=70, entryWidth=50, ) if choice == 'back': return None if choice in ['ok', None]: get_mirror.m = entry[0].strip() if valid_mirror(get_mirror.m): return get_mirror.m if choice == 'cancel': raise Abort('Entering an APT mirror') def valid_mirror(mirror): progress = ProgressMessage(screen, title='Please wait...', text='Checking APT mirror: ' + mirror) try: o = urlopen('{mirror}/dists/{codename}/Release'.format( mirror=mirror, codename=codename), timeout=15) have_code_name = False have_arch = False re_arch = re.compile(r'^Architectures:.+\b{archname}\b.*$'.format(archname=archname)) re_code = re.compile(r'^Codename: +\b{codename}\b.*$'.format(codename=codename)) for line in o.read(400).split('\n'): if re_code.match(line): have_code_name = True if re_arch.match(line): have_arch = True if have_code_name and have_arch: return True raise URLError('Not a valid Dyson APT repository') except HTTPError as e: ButtonChoiceWindow(screen, title='Error', text='{code} {reason}.\n\nMirror {mirror} is not usable.'.format( mirror=mirror, code=e.code, reason=e.reason), buttons = ['Ok'], width=70) except URLError as e: ButtonChoiceWindow(screen, title='Error', text='{reason}.\n\nMirror {mirror} is not usable.'.format( reason=e.reason,mirror=mirror), buttons = ['Ok'], width=70) except ValueError: ButtonChoiceWindow(screen, title='Failed to check mirror', text='Invalid URL: ' + mirror, buttons = ['Ok'], width=70) except Exception: ButtonChoiceWindow(screen, title='Failed to check mirror', text='Unknown error: ' + mirror, buttons = ['Ok'], width=70) return False def configure_mirror(): global mirror items = map(lambda x: '{url} ({info})'.format(url=x[0], info=x[1]), mirrors) items.append(('Enter another mirror', None)) mirror = None while not mirror: (choice, m) = ListboxChoiceWindow(screen, title='Choose APT mirror', text='Choose APT repository from which Dyson will be installed', items=items, buttons=[('Ok','ok'), ('Cancel', 'cancel')], width=76, scroll=1, default=0, ) if choice in ['ok', None]: if m == None: mirror = get_mirror() else: if valid_mirror(mirrors[m][0]): mirror = mirrors[m][0] if choice == 'cancel': raise Abort('Choosing APT mirror') def debootstrap(): progress = ProgressBar(screen, title='Installing base system, please wait...', width=76) progress.text = ' ' read, write = os.pipe() os.makedirs(rootdir + '/root') log = os.open(rootdir + '/root/debootstrap.log', os.O_WRONLY + os.O_CREAT) pid = os.fork() if pid == 0: os.close(read) os.dup2(write, 3) os.dup2(log, 1) os.dup2(log, 2) os.close(log) os.close(write) try: os.execl('/usr/sbin/debootstrap', 'debootstrap', '--debian-installer', '--no-check-gpg', '--exclude=gawk,aptitude,aptitude-common,libboost-iostreams1.48.0,libboost-iostreams1.49.0,libcwidget3', '--include=illumos-grub,illumos-kernel,locales', codename, rootdir, mirror) except OSError as e: sys.exit(e.errno) else: os.close(write) di = os.fdopen(read, 'r', 1) info_re = re.compile(' +') m = 'START' IA = [] # Arguments for IF, cleaned after each IF PA = [] # Arguments for PF, cleaned after each PF EA = [] # Arguments for EF, cleaned after each EF WA = [] # Arguments for WF, cleaned after each WF while True: line = di.readline() if not line: break try: colon = line.find(':') if colon == -1: continue cmd = line[:colon] info = line[colon+1:].strip() text = None if cmd == 'P': # "P: 23 454 XYZ" or "P: 123 3445" p = info_re.split(info) if len(p) == 3: m = p[2].strip() progress.progress = math.ceil(miles[m][0] + float(p[0])*(miles[m][1] - miles[m][0]) / float(p[1])) elif cmd == 'IA': IA.append(info) elif cmd == 'PA': PA.append(info) elif cmd == 'EA': EA.append(info) elif cmd == 'WA': WA.append(info) elif cmd == 'IF': text = (info % tuple(IA)) IA = [] elif cmd == 'PF': text = (info % tuple(PA)) PA = [] elif cmd == 'EF': text = (info % tuple(EA)) EA = [] elif cmd == 'WF': text = (info % tuple(WA)) WA = [] if text: progress.text = text os.write(log, text) os.write(log, '\n') except: pass status = os.wait()[1] os.close(log) return os.WEXITSTATUS(status) def install(): while True: configure_mirror() code = debootstrap() if code == 0: return while True: choice = ButtonChoiceWindow(screen, title='Installation failed', text='Debootstrap failed.', buttons=[('View log', 'log'), ('Try again', 'again'), ('Cancel', 'cancel')], width=40) if choice == 'log': screen.suspend() call(['less', rootdir + '/root/debootstrap.log']) screen.resume() continue if choice == 'again': break if choice == 'cancel': raise Abort('debootstrap failed') p = ProgressMessage(screen, title='Please wait', text='Undoing previous try ...') umount_in_bootenv() # debootstrap mounts required FS for us call(['zfs', 'rollback', '-r', bootenv + '@empty']) def umount_in_bootenv(): '''unmount all FS mounted in BE on final cleanup. These FS also may be left after interruption or debootstrap failure''' global bootenv global rootdir if not bootenv: return fslist = ['/dev/fd', '/proc', '/devices', '/home'] for fs in fslist: call(['umount', rootdir + fs], stdout=PIPE, stderr=PIPE) def in_bootenv(cmd): chroot = ['chroot', rootdir] chroot += cmd return call(chroot, stderr=PIPE, stdout=PIPE) def write_vfstab(): vfstab=''' #device device mount FS fsck mount mount #to mount to fsck point type pass at boot options # fd - /dev/fd fd - no - swap - /tmp tmpfs - yes - /dev/zvol/dsk/{rpool}/swap - - swap - no - '''.format(rpool=rpool) try: f = open(rootdir + '/etc/vfstab', 'w') print(vfstab, file=f) f.close() except: pass # http://stackoverflow.com/questions/2532053/validate-a-hostname-string def isValidNodename(hostname): if len(hostname) > 63: return False allowed = re.compile("(?!-)[A-Z\d-]{1,63}(?