diff --git a/answerfile.py b/answerfile.py index c9af0cdc..f46f77c5 100644 --- a/answerfile.py +++ b/answerfile.py @@ -92,6 +92,8 @@ def processAnswerfile(self): else: raise AnswerfileException("Unknown mode, %s" % install_type) + results['repo-gpgcheck'] = getBoolAttribute(self.top_node, ['repo-gpgcheck'], default=True) + results['gpgcheck'] = getBoolAttribute(self.top_node, ['gpgcheck'], default=True) results.update(self.parseCommon()) elif self.operation == 'restore': results = self.parseRestore() @@ -133,6 +135,7 @@ def parseFreshInstall(self): results['preserve-settings'] = False results['backup-existing-installation'] = False + results.update(self.parseRaid()) results.update(self.parseDisks()) results.update(self.parseInterface()) results.update(self.parseRootPassword()) @@ -266,7 +269,21 @@ def parseSource(self): if rtype == 'url': address = util.URL(address) - results['sources'].append({'media': rtype, 'address': address}) + # workaround getBoolAttribute() not allowing "None" as + # default, by using a getStrAttribute() call first to + # handle the default situation where the attribute is not + # specified + repo_gpgcheck = (None if getStrAttribute(i, ['repo-gpgcheck'], default=None) is None + else getBoolAttribute(i, ['repo-gpgcheck'])) + gpgcheck = (None if getStrAttribute(i, ['gpgcheck'], default=None) is None + else getBoolAttribute(i, ['gpgcheck'])) + + results['sources'].append({ + 'media': rtype, 'address': address, + 'repo_gpgcheck': repo_gpgcheck, + 'gpgcheck': gpgcheck, + }) + logger.log("parsed source %s" % results['sources'][-1]) return results @@ -293,6 +310,16 @@ def parseDriverSource(self): results['extra-repos'].append((rtype, address)) return results + def parseRaid(self): + results = {} + for raid_node in getElementsByTagName(self.top_node, ['raid']): + disk_device = normalize_disk(getStrAttribute(raid_node, ['device'], mandatory=True)) + disks = [normalize_disk(getText(node)) for node in getElementsByTagName(raid_node, ['disk'])] + if 'raid' not in results: + results['raid'] = {} + results['raid'][disk_device] = disks + return results + def parseDisks(self): results = {} @@ -324,7 +351,7 @@ def parseDisks(self): results['sr-type'] = getMapAttribute(self.top_node, ['sr-type', 'srtype'], [('lvm', SR_TYPE_LVM), - ('ext', SR_TYPE_EXT)], default='lvm') + ('ext', SR_TYPE_EXT)], default='ext') return results def parseFCoEInterface(self): diff --git a/backend.py b/backend.py index 09467a27..e7dce051 100644 --- a/backend.py +++ b/backend.py @@ -33,6 +33,7 @@ import version from version import * from constants import * +from diskutil import getRemovableDeviceList MY_PRODUCT_BRAND = PRODUCT_BRAND or PLATFORM_NAME @@ -104,6 +105,7 @@ def getPrepSequence(ans, interactive): Task(util.getUUID, As(ans), ['installation-uuid']), Task(util.getUUID, As(ans), ['control-domain-uuid']), Task(util.randomLabelStr, As(ans), ['disk-label-suffix']), + Task(diskutil.create_raid, A(ans, 'raid'), []), Task(inspectTargetDisk, A(ans, 'primary-disk', 'installation-to-overwrite', 'preserve-first-partition','sr-on-primary'), ['target-boot-mode', 'boot-partnum', 'primary-partnum', 'backup-partnum', 'logs-partnum', 'swap-partnum', 'storage-partnum']), ] @@ -161,7 +163,7 @@ def getMainRepoSequence(ans, repos): def getRepoSequence(ans, repos): seq = [] for repo in repos: - seq.append(Task(repo.installPackages, A(ans, 'mounts'), [], + seq.append(Task(repo.installPackages, A(ans, 'mounts', 'kernel-alt'), [], progress_scale=100, pass_progress_callback=True, progress_text="Installing %s..." % repo.name())) @@ -171,6 +173,7 @@ def getRepoSequence(ans, repos): def getFinalisationSequence(ans): seq = [ + Task(importYumAndRpmGpgKeys, A(ans, 'mounts'), []), Task(writeResolvConf, A(ans, 'mounts', 'manual-hostname', 'manual-nameservers'), []), Task(writeMachineID, A(ans, 'mounts'), []), Task(writeKeyboardConfiguration, A(ans, 'mounts', 'keymap'), []), @@ -192,6 +195,7 @@ def getFinalisationSequence(ans): 'boot-partnum', 'primary-partnum', 'target-boot-mode', 'branding', 'disk-label-suffix', 'bootloader-location', 'write-boot-entry', 'install-type', 'serial-console', 'boot-serial', 'host-config', 'fcoe-interfaces'), []), + Task(postInstallAltKernel, A(ans, 'mounts', 'kernel-alt'), []), Task(touchSshAuthorizedKeys, A(ans, 'mounts'), []), Task(setRootPassword, A(ans, 'mounts', 'root-password'), [], args_sensitive=True), Task(setTimeZone, A(ans, 'mounts', 'timezone'), []), @@ -376,7 +380,7 @@ def handleRepos(repos, ans): main_repositories = [] update_repositories = [] - def add_repos(main_repositories, update_repositories, repos): + def add_repos(main_repositories, update_repositories, repos, repo_gpgcheck, gpgcheck): """Add repositories to the appropriate list, ensuring no duplicates, that the main repository is at the beginning, and that the order of the rest is maintained.""" @@ -393,20 +397,28 @@ def add_repos(main_repositories, update_repositories, repos): else: repo_list.append(repo) + if repo_list is main_repositories: # i.e., if repo is a "main repository" + repo.setRepoGpgCheck(repo_gpgcheck) + repo.setGpgCheck(gpgcheck) + + default_repo_gpgcheck = answers.get('repo-gpgcheck', True) + default_gpgcheck = answers.get('gpgcheck', True) # A list of sources coming from the answerfile if 'sources' in answers_pristine: for i in answers_pristine['sources']: repos = repository.repositoriesFromDefinition(i['media'], i['address']) - add_repos(main_repositories, update_repositories, repos) + repo_gpgcheck = default_repo_gpgcheck if i['repo_gpgcheck'] is None else i['repo_gpgcheck'] + gpgcheck = default_gpgcheck if i['gpgcheck'] is None else i['gpgcheck'] + add_repos(main_repositories, update_repositories, repos, repo_gpgcheck, gpgcheck) # A single source coming from an interactive install if 'source-media' in answers_pristine and 'source-address' in answers_pristine: repos = repository.repositoriesFromDefinition(answers_pristine['source-media'], answers_pristine['source-address']) - add_repos(main_repositories, update_repositories, repos) + add_repos(main_repositories, update_repositories, repos, default_repo_gpgcheck, default_gpgcheck) for media, address in answers_pristine['extra-repos']: repos = repository.repositoriesFromDefinition(media, address) - add_repos(main_repositories, update_repositories, repos) + add_repos(main_repositories, update_repositories, repos, default_repo_gpgcheck, default_gpgcheck) if not main_repositories or main_repositories[0].identifier() != MAIN_REPOSITORY_NAME: raise RuntimeError("No main repository found") @@ -421,8 +433,19 @@ def add_repos(main_repositories, update_repositories, repos): if r.accessor().canEject(): r.accessor().eject() + # XCP-ng: so, very unfortunately we don't remember with precision why this was added and + # no commit message or comment can help us here. + # It may be related to the fact that the "all_repositories" above doesn't contain + # the installation CD-ROM or USB stick in the case of a netinstall. + # Question: why it is needed at all since there's no repository on the netinstall + # installation media? + if answers.get('netinstall'): + for device in getRemovableDeviceList(): + util.runCmd2(['eject', device]) + if interactive and (constants.HAS_SUPPLEMENTAL_PACKS or "driver-repos" in answers): + # Add supp packs in a loop while True: media_ans = dict(answers_pristine) @@ -1139,7 +1162,11 @@ def installBootLoader(mounts, disk, boot_partnum, primary_partnum, target_boot_m setEfiBootEntry(mounts, disk, boot_partnum, install_type, branding) else: if location == constants.BOOT_LOCATION_MBR: - installGrub2(mounts, disk, False) + if diskutil.is_raid(disk): + for member in diskutil.getDeviceSlaves(disk): + installGrub2(mounts, member, False) + else: + installGrub2(mounts, disk, False) else: installGrub2(mounts, root_partition, True) @@ -1523,15 +1550,15 @@ def configureNetworking(mounts, admin_iface, admin_bridge, admin_config, hn_conf print >>mc, "NETMASK='%s'" % admin_config.netmask if admin_config.gateway: print >>mc, "GATEWAY='%s'" % admin_config.gateway - if manual_nameservers: - print >>mc, "DNS='%s'" % (','.join(nameservers),) - if domain: - print >>mc, "DOMAIN='%s'" % domain print >>mc, "MODEV6='%s'" % netinterface.NetInterface.getModeStr(admin_config.modev6) if admin_config.modev6 == netinterface.NetInterface.Static: print >>mc, "IPv6='%s'" % admin_config.ipv6addr if admin_config.ipv6_gateway: print >>mc, "IPv6_GATEWAY='%s'" % admin_config.ipv6_gateway + if manual_nameservers: + print >>mc, "DNS='%s'" % (','.join(nameservers),) + if domain: + print >>mc, "DOMAIN='%s'" % domain if admin_config.vlan: print >>mc, "VLAN='%d'" % admin_config.vlan mc.close() @@ -1573,12 +1600,18 @@ def configureNetworking(mounts, admin_iface, admin_bridge, admin_config, hn_conf # now we need to write /etc/sysconfig/network nfd = open("%s/etc/sysconfig/network" % mounts["root"], "w") nfd.write("NETWORKING=yes\n") - if admin_config.modev6: + ipv6 = admin_config.modev6 is not None + if ipv6: nfd.write("NETWORKING_IPV6=yes\n") util.runCmd2(['chroot', mounts['root'], 'systemctl', 'enable', 'ip6tables']) else: nfd.write("NETWORKING_IPV6=no\n") netutil.disable_ipv6_module(mounts["root"]) + + with open("%s/etc/sysctl.d/91-net-ipv6.conf" % mounts["root"], "w") as ipv6_conf: + for i in ['all', 'default']: + ipv6_conf.write('net.ipv6.conf.%s.disable_ipv6=%d\n' % (i, int(not ipv6))) + nfd.write("IPV6_AUTOCONF=no\n") nfd.write('NTPSERVERARGS="iburst prefer"\n') nfd.close() @@ -1662,6 +1695,57 @@ def touchSshAuthorizedKeys(mounts): fh = open("%s/root/.ssh/authorized_keys" % mounts['root'], 'a') fh.close() +def importYumAndRpmGpgKeys(mounts): + # Python script that uses yum functions to import the GPG key for our repositories + import_yum_keys = """#!/bin/env python +from __future__ import print_function +from yum import YumBase + +def retTrue(*args, **kwargs): + return True + +base = YumBase() +for repo in base.repos.repos.itervalues(): + if repo.id.startswith('xcp-ng'): + print("*** Importing GPG key for repository %s - %s" % (repo.id, repo.name)) + base.getKeyForRepo(repo, callback=retTrue) +""" + internal_tmp_filepath = '/tmp/import_yum_keys.py' + external_tmp_filepath = mounts['root'] + internal_tmp_filepath + with open(external_tmp_filepath, 'w') as f: + f.write(import_yum_keys) + # bind mount /dev, necessary for NSS initialization without which RPM won't work + util.bindMount('/dev', "%s/dev" % mounts['root']) + try: + util.runCmd2(['chroot', mounts['root'], 'python', internal_tmp_filepath]) + util.runCmd2(['chroot', mounts['root'], 'rpm', '--import', '/etc/pki/rpm-gpg/RPM-GPG-KEY-xcpng']) + finally: + util.umount("%s/dev" % mounts['root']) + os.unlink(external_tmp_filepath) + +def postInstallAltKernel(mounts, kernel_alt): + """ Install our alternate kernel. Must be called after the bootloader installation. """ + if not kernel_alt: + logger.log('kernel-alt not installed') + return + + util.bindMount("/proc", "%s/proc" % mounts['root']) + util.bindMount("/sys", "%s/sys" % mounts['root']) + util.bindMount("/dev", "%s/dev" % mounts['root']) + + try: + rc, out = util.runCmd2(['chroot', mounts['root'], 'rpm', '-q', 'kernel-alt', '--qf', '%{version}'], + with_stdout=True) + version = out + # Generate the initrd as it was disabled during initial installation + util.runCmd2(['chroot', mounts['root'], 'dracut', '-f', '/boot/initrd-%s.img' % version, version]) + + # Update grub + util.runCmd2(['chroot', mounts['root'], 'python', '/usr/lib/python2.7/site-packages/xcp/updategrub.py', 'add', 'kernel-alt', version]) + finally: + util.umount("%s/dev" % mounts['root']) + util.umount("%s/sys" % mounts['root']) + util.umount("%s/proc" % mounts['root']) ################################################################################ # OTHER HELPERS diff --git a/constants.py b/constants.py index 525f3616..7a604d66 100644 --- a/constants.py +++ b/constants.py @@ -58,9 +58,9 @@ def error_string(error, logname, with_hd): ) = range(3) ERROR_STRINGS = { - ERROR_STRING_UNKNOWN_ERROR_WITH_HD: "An unrecoverable error has occurred. The details of the error can be found in the log file, which has been written to /tmp/%s (and /root/%s on your hard disk if possible).\n\nPlease refer to your user guide or contact a Technical Support Representative for more details.", - ERROR_STRING_UNKNOWN_ERROR_WITHOUT_HD: "An unrecoverable error has occurred. The details of the error can be found in the log file, which has been written to /tmp/%s.\n\nPlease refer to your user guide or contact a Technical Support Representative for more details.", - ERROR_STRING_KNOWN_ERROR: "An unrecoverable error has occurred. The error was:\n\n%s\n\nPlease refer to your user guide, or contact a Technical Support Representative, for further details." + ERROR_STRING_UNKNOWN_ERROR_WITH_HD: "An unrecoverable error has occurred. The details of the error can be found in the log file, which has been written to /tmp/%s (and /root/%s on your hard disk if possible).", + ERROR_STRING_UNKNOWN_ERROR_WITHOUT_HD: "An unrecoverable error has occurred. The details of the error can be found in the log file, which has been written to /tmp/%s.", + ERROR_STRING_KNOWN_ERROR: "An unrecoverable error has occurred. The error was:\n\n%s" } if error == "": @@ -154,6 +154,9 @@ def error_string(error, logname, with_hd): HYPERVISOR_CAPS_FILE = "/sys/hypervisor/properties/capabilities" SAFE_2_UPGRADE = "var/preserve/safe2upgrade" +# NTP server domains to treat as 'default' servers +DEFAULT_NTP_DOMAINS = [".centos.pool.ntp.org", ".xenserver.pool.ntp.org"] + # timer to exit installer after fatal error AUTO_EXIT_TIMER = 10 * 1000 diff --git a/disktools.py b/disktools.py index 749b5922..af7ac40f 100644 --- a/disktools.py +++ b/disktools.py @@ -490,7 +490,7 @@ def diskDevice(partitionDevice): def determineMidfix(device): DISK_PREFIX = '/dev/' - P_STYLE_DISKS = [ 'cciss', 'ida', 'rd', 'sg', 'i2o', 'amiraid', 'iseries', 'emd', 'carmel', 'mapper/', 'nvme', 'md' ] + P_STYLE_DISKS = [ 'cciss', 'ida', 'rd', 'sg', 'i2o', 'amiraid', 'iseries', 'emd', 'carmel', 'mapper/', 'nvme', 'md', 'mmcblk' ] PART_STYLE_DISKS = [ 'disk/by-id' ] for key in P_STYLE_DISKS: diff --git a/diskutil.py b/diskutil.py index 588f4379..885c62f2 100644 --- a/diskutil.py +++ b/diskutil.py @@ -113,6 +113,9 @@ def mpath_disable(): for major in range(48, 56): disk_nodes += [ (major, x * 8) for x in range(32) ] +# /dev/mmcblk: mmcblk has major 179, each device usually (per kernel) has 7 minors +disk_nodes += [ (179, x * 8) for x in range(32) ] + def getDiskList(): # read the partition tables: parts = open("/proc/partitions") @@ -153,6 +156,21 @@ def getDiskList(): return disks +def create_raid(configuration): + if configuration: + for raid_device, members in configuration.viewitems(): + # allows for idempotence + if not os.path.exists(raid_device): + for dev in members: + util.runCmd2(['sgdisk', '--zap-all', dev]) + util.runCmd2(['mdadm', '--zero-superblock', '--force', dev]) + # let it fail without catching + cmd = ['mdadm', '--create', raid_device, '--run', '--metadata=1.0', '--level=mirror', + '--raid-devices=%s' % (len(members))] + members + rc, out, err = util.runCmd2(cmd, with_stdout=True, with_stderr=True) + if rc != 0: + raise Exception('Error running: %s\n%s\n\n%s' % (' '.join(cmd), out, err)) + def getPartitionList(): disks = getDiskList() rv = [] @@ -280,6 +298,9 @@ def getDiskDeviceSize(dev): return int(__readOneLineFile__("/sys/block/%s/device/block/size" % dev)) elif os.path.exists("/sys/block/%s/size" % dev): return int(__readOneLineFile__("/sys/block/%s/size" % dev)) + else: + raise Exception("%s not found as %s or %s" % (dev, "/sys/block/%s/device/block/size", + "/sys/block/%s/size")) def getDiskSerialNumber(dev): # For Multipath nodes return info about 1st slave diff --git a/doc/answerfile.txt b/doc/answerfile.txt index edcf261f..79541adc 100644 --- a/doc/answerfile.txt +++ b/doc/answerfile.txt @@ -34,6 +34,29 @@ Restore: ... + +Common Attributes +----------------- + + repo-gpgcheck="false" + + Disable check of repodata signature (`repo_gpgcheck=0` in + `yum.conf`), for all yum repositories that are not Supplemental + Packs (none of which are checked). Don't use this for a network + install of a production server, and make sure to verify the + authenticity of your install media through other means. + + Validity: any operation. + + gpgcheck="false" + + Disable check of rpm signature (`gpgcheck=0` in `yum.conf`), for + all yum repositories that are not Supplemental Packs (none of + which are checked). Don't use this for a production server. + + Validity: any operation. + + Elements common to all answerfiles, both 'installation' and 'restore' --------------------------------------------------------------------- @@ -100,6 +123,15 @@ Elements for 'installation' modes The location of the installation repository or a Supplemental Pack. There may be multiple 'source' elements. + Optional attributes for only: + + repo-gpgcheck=bool + gpgcheck=bool + + Override the global yum gpgcheck setting, respectively for + repodata and RPMs, for this source only. Only applies to + repositories that are not Supplemental Packs (none of which + are checked). grub2|extlinux[D]|grub[D]? @@ -156,6 +188,16 @@ Format of 'source' and 'driver-source' (Re)Install Elements -------------------- + + dev1 + dev2 + ? + + Specifies the target disks and md device for creating a + software RAID 1 array. The md device can then be used in + below. (new in xcp-ng 7.5.0-2 and 7.6) + + dev Specifies the target disk for installation. @@ -301,7 +343,11 @@ Format of 'source' and 'driver-source' Local SR type. - Default: lvm + Default: ext + + Note that while it reflects the default choice in the text UI, + this differs from XenServer and from XCP-ng releases lower than + 8.3, where the default was "lvm". Upgrade Elements diff --git a/doc/parameters.txt b/doc/parameters.txt index c0f78819..3f5f3ed3 100644 --- a/doc/parameters.txt +++ b/doc/parameters.txt @@ -220,3 +220,13 @@ Installer --cc-preparations Prepare configuration for common criteria security. + + + --no-repo-gpgcheck + + Disable check of repodata signature, for all yum repositories. + + + --no-gpgcheck + + Disable check of rpm signature, for all yum repositories. diff --git a/install.py b/install.py index 825b569e..cc7085c4 100755 --- a/install.py +++ b/install.py @@ -128,6 +128,20 @@ def go(ui, args, answerfile_address, answerfile_script): elif opt == "--cc-preparations": constants.CC_PREPARATIONS = True results['network-backend'] = constants.NETWORK_BACKEND_BRIDGE + # XCP-ng addition: alternate kernel + elif opt == "--kernel-alt": + results['kernel-alt'] = True + logger.log("Using alternate kernel.") + # XCP-ng: netinstall + elif opt == "--netinstall": + results['netinstall'] = True + logger.log("This is a netinstall.") + elif opt == "--no-repo-gpgcheck": + results['repo-gpgcheck'] = False + logger.log("Yum gpg check of repository disabled on command-line") + elif opt == "--no-gpgcheck": + results['gpgcheck'] = False + logger.log("Yum gpg check of RPMs disabled on command-line") if boot_console and not serial_console: serial_console = boot_console diff --git a/netinterface.py b/netinterface.py index d5c788db..59af509c 100644 --- a/netinterface.py +++ b/netinterface.py @@ -16,7 +16,7 @@ def getTextOrNone(nodelist): rc = rc + node.data return rc == "" and None or rc.strip().encode() -class NetInterface: +class NetInterface(object): """ Represents the configuration of a network interface. """ Static = 1 @@ -122,10 +122,18 @@ def valid(self): return False return self.mode or self.modev6 - def isStatic(self): - """ Returns true if a static interface configuration is represented. """ + def isStatic4(self): + """ Returns true if an IPv4 static interface configuration is represented. """ return self.mode == self.Static + def isStatic6(self): + """ Returns true if an IPv6 static interface configuration is represented. """ + return self.modev6 == self.Static + + def isDynamic(self): + """ Returns true if a dynamic interface configuration is represented. """ + return self.mode == self.DHCP or self.modev6 == self.DHCP or self.modev6 == self.Autoconf + def isVlan(self): return self.vlan is not None @@ -141,8 +149,7 @@ def writeRHStyleInterface(self, iface): """ Write a RedHat-style configuration entry for this interface to file object f using interface name iface. """ - assert self.modev6 is None - assert self.mode + assert self.modev6 or self.mode iface_vlan = self.getInterfaceName(iface) f = open('/etc/sysconfig/network-scripts/ifcfg-%s' % iface_vlan, 'w') @@ -151,7 +158,7 @@ def writeRHStyleInterface(self, iface): if self.mode == self.DHCP: f.write("BOOTPROTO=dhcp\n") f.write("PERSISTENT_DHCLIENT=1\n") - else: + elif self.mode == self.Static: # CA-11825: broadcast needs to be determined for non-standard networks bcast = self.getBroadcast() f.write("BOOTPROTO=none\n") @@ -161,20 +168,45 @@ def writeRHStyleInterface(self, iface): f.write("NETMASK=%s\n" % self.netmask) if self.gateway: f.write("GATEWAY=%s\n" % self.gateway) + + if self.modev6: + with open('/etc/sysconfig/network', 'w') as net_conf: + net_conf.write("NETWORKING_IPV6=yes\n") + f.write("IPV6INIT=yes\n") + f.write("IPV6_DEFROUTE=yes\n") + f.write("IPV6_DEFAULTDEV=%s\n" % iface_vlan) + + if self.modev6 == self.DHCP: + f.write("DHCPV6C=yes\n") + f.write("PERSISTENT_DHCLIENT_IPV6=yes\n") + f.write("IPV6_FORCE_ACCEPT_RA=yes\n") + f.write("IPV6_AUTOCONF=no\n") + elif self.modev6 == self.Static: + f.write("IPV6ADDR=%s\n" % self.ipv6addr) + if self.ipv6_gateway: + f.write("IPV6_DEFAULTGW=%s\n" % (self.ipv6_gateway)) + f.write("IPV6_AUTOCONF=no\n") + elif self.modev6 == self.Autoconf: + f.write("IPV6_AUTOCONF=yes\n") + if self.vlan: f.write("VLAN=yes\n") f.close() def waitUntilUp(self, iface): - if not self.isStatic(): - return True - if not self.gateway: - return True + iface_name = self.getInterfaceName(iface) + if self.isStatic4() and self.gateway and util.runCmd2( + ['/usr/sbin/arping', '-f', '-w', '120', '-I', iface_name, self.gateway] + ): + return False + + if self.isStatic6() and self.ipv6_gateway and util.runCmd2( + ['/usr/sbin/ndisc6', '-1', '-w', '120', self.ipv6_gateway, iface_name] + ): + return False - rc = util.runCmd2(['/usr/sbin/arping', '-f', '-w', '120', '-I', - self.getInterfaceName(iface), self.gateway]) - return rc == 0 + return True @staticmethod def getModeStr(mode): @@ -185,3 +217,22 @@ def getModeStr(mode): if mode == NetInterface.Autoconf: return 'autoconf' return 'none' + +class NetInterfaceV6(NetInterface): + def __init__(self, mode, hwaddr, ipaddr=None, netmask=None, gateway=None, dns=None, domain=None, vlan=None): + super(NetInterfaceV6, self).__init__(None, hwaddr, None, None, None, None, None, vlan) + + ipv6addr = None + if mode == self.Static: + assert ipaddr + assert netmask + + ipv6addr = ipaddr + "/" + netmask + if dns == '': + dns = None + elif isinstance(dns, str): + dns = [ dns ] + self.dns = dns + self.domain = domain + + self.addIPv6(mode, ipv6addr=ipv6addr, ipv6gw=gateway) diff --git a/netutil.py b/netutil.py index f266695c..bcb4f3e8 100644 --- a/netutil.py +++ b/netutil.py @@ -4,12 +4,12 @@ import diskutil import util import re +import socket import subprocess import time import errno from xcp import logger from xcp.net.biosdevname import all_devices_all_names -from socket import inet_ntoa from struct import pack class NIC: @@ -73,7 +73,7 @@ def writeResolverFile(configuration, filename): for iface in configuration: settings = configuration[iface] - if settings.isStatic() and settings.dns: + if (not settings.isDynamic()) and settings.dns: if settings.dns: for server in settings.dns: outfile.write("nameserver %s\n" % server) @@ -118,7 +118,11 @@ def interfaceUp(interface): if rc != 0: return False inets = filter(lambda x: x.startswith(" inet "), out.split("\n")) - return len(inets) == 1 + if len(inets) == 1: + return True + + inet6s = filter(lambda x: x.startswith(" inet6 "), out.split("\n")) + return len(inet6s) > 1 # Not just the fe80:: address # work out if a link is up: def linkUp(interface): @@ -206,16 +210,21 @@ def valid_vlan(vlan): return False return True -def valid_ip_addr(addr): - if not re.match('^\d+\.\d+\.\d+\.\d+$', addr): - return False - els = addr.split('.') - if len(els) != 4: +def valid_ip_address_family(addr, family): + try: + socket.inet_pton(family, addr) + return True + except socket.error: return False - for el in els: - if int(el) > 255: - return False - return True + +def valid_ipv4_addr(addr): + return valid_ip_address_family(addr, socket.AF_INET) + +def valid_ipv6_addr(addr): + return valid_ip_address_family(addr, socket.AF_INET6) + +def valid_ip_addr(addr): + return valid_ipv4_addr(addr) or valid_ipv6_addr(addr) def network(ipaddr, netmask): ip = map(int,ipaddr.split('.',3)) @@ -227,7 +236,7 @@ def prefix2netmask(mask): bits = 0 for i in xrange(32-mask, 32): bits |= (1 << i) - return inet_ntoa(pack('>I', bits)) + return socket.inet_ntoa(pack('>I', bits)) class NetDevices: def __init__(self): diff --git a/product.py b/product.py index 5f42f38c..72dbe766 100644 --- a/product.py +++ b/product.py @@ -19,6 +19,7 @@ from xcp import logger import xml.dom.minidom import simplejson as json +import glob class SettingsNotAvailable(Exception): pass @@ -168,8 +169,21 @@ def _readSettings(self): ntps = [] for line in lines: if line.startswith("server "): - ntps.append(line[7:].split()[0].strip()) + s = line[7:].split()[0].strip() + if any(s.endswith(d) for d in constants.DEFAULT_NTP_DOMAINS): + continue + ntps.append(s) results['ntp-servers'] = ntps + # ntp-config-method should be set as follows: + # 'dhcp' if dhcp was in use, regardless of server configuration + # 'manual' if we had existing NTP servers defined (other than default servers) + # 'default' if no NTP servers are defined + if self._check_dhcp_ntp_status(): + results['ntp-config-method'] = 'dhcp' + elif ntps: + results['ntp-config-method'] = 'manual' + else: + results['ntp-config-method'] = 'default' # keyboard: keyboard_dict = {} @@ -216,9 +230,6 @@ def _readSettings(self): raise SettingsNotAvailable("no root password found") results['root-password'] = ('pwdhash', root_pwd) - # don't care about this too much. - results['ntp-config-method'] = 'default' - # read network configuration. We only care to find out what the # management interface is, and what its configuration was. # The dev -> MAC mapping for other devices will be preserved in the @@ -393,12 +404,26 @@ def fetchIfaceInfoFromNetworkdbAsDict(bridge, iface=None): pciback = next((x for x in kernel_args if x.startswith('xen-pciback.hide=')), None) if pciback: results['host-config']['xen-pciback.hide'] = pciback - except: - pass + except Exception as e: + logger.log('Exception whilst parsing existing bootloader config:') + logger.logException(e) self.unmount_boot() return results + def _check_dhcp_ntp_status(self): + """Validate if DHCP was in use and had provided any NTP servers""" + if os.path.exists(self.join_state_path('etc/dhcp/dhclient.d/chrony.sh')) and \ + not (os.stat(self.join_state_path('etc/dhcp/dhclient.d/chrony.sh')).st_mode & stat.S_IXUSR): + # chrony.sh not executable indicates not using DHCP for NTP + return False + + for f in glob.glob(self.join_state_path('var/lib/dhclient/chrony.servers.*')): + if os.path.getsize(f) > 0: + return True + + return False + def mount_boot(self, ro=True): opts = None if ro: @@ -534,13 +559,31 @@ def findXenSourceBackups(): b = None try: b = util.TempMount(p, 'backup-', ['ro'], 'ext3') - if os.path.exists(os.path.join(b.mount_point, '.xen-backup-partition')): - backup = XenServerBackup(p, b.mount_point) - logger.log("Found a backup: %s" % (repr(backup),)) - if backup.version >= XENSERVER_MIN_VERSION and \ - backup.version <= THIS_PLATFORM_VERSION: - backups.append(backup) - except: + if not os.path.exists(os.path.join(b.mount_point, '.xen-backup-partition')): + raise StopIteration() + + backup = XenServerBackup(p, b.mount_point) + logger.log("Found a backup: %s" % (repr(backup),)) + + if backup.version < XENSERVER_MIN_VERSION: + logger.log("findXenSourceBackups: ignoring, platform too old: %s < %s" % + (backup.version, XENSERVER_MIN_VERSION)) + raise StopIteration() + if backup.version > THIS_PLATFORM_VERSION: + logger.log("findXenSourceBackups: ignoring later platform: %s > %s" % + (backup.version, THIS_PLATFORM_VERSION)) + raise StopIteration() + if not os.path.exists(backup.root_disk): + logger.error("findXenSourceBackups: PRIMARY_DISK=%r does not exist" % + (backup.root_disk,)) + raise StopIteration() + + backups.append(backup) + + except StopIteration: + pass + except Exception as ex: + logger.log("findXenSourceBackups caught exception for partition %s: %s" % (p, ex)) pass if b: b.unmount() diff --git a/repository.py b/repository.py index 98194052..c7b8fb16 100644 --- a/repository.py +++ b/repository.py @@ -181,7 +181,7 @@ def record_install(self, answers, installed_repos): installed_repos[str(self)] = self return installed_repos - def _installPackages(self, progress_callback, mounts): + def _installPackages(self, progress_callback, mounts, kernel_alt): assert self._targets is not None url = self._accessor.url() logger.log("URL: " + str(url)) @@ -203,13 +203,15 @@ def _installPackages(self, progress_callback, mounts): yum_conf.write(repo_config) self.disableInitrdCreation(mounts['root']) + if kernel_alt: + self._targets.append('kernel-alt') installFromYum(self._targets, mounts, progress_callback, self._cachedir) self.enableInitrdCreation() - def installPackages(self, progress_callback, mounts): + def installPackages(self, progress_callback, mounts, kernel_alt=False): self._accessor.start() try: - self._installPackages(progress_callback, mounts) + self._installPackages(progress_callback, mounts, kernel_alt) finally: self._accessor.finish() @@ -236,12 +238,14 @@ class MainYumRepository(YumRepositoryWithInfo): """Represents a Yum repository containing the main XenServer installation.""" INFO_FILENAME = ".treeinfo" - _targets = ['@xenserver_base', '@xenserver_dom0'] + _targets = ['xcp-ng-deps'] def __init__(self, accessor): super(MainYumRepository, self).__init__(accessor) self._identifier = MAIN_REPOSITORY_NAME self.keyfiles = [] + self._repo_gpg_check = True + self._gpg_check = True def get_name_version(config_parser, section, name_key, vesion_key): name, version = None, None @@ -312,10 +316,10 @@ def _repo_config(self): outfh = open(key_path, "w") outfh.write(infh.read()) return """ -gpgcheck=1 -repo_gpgcheck=1 +gpgcheck=%s +repo_gpgcheck=%s gpgkey=file://%s -""" % (key_path) +""" % (int(self._gpg_check), int(self._repo_gpg_check), key_path) finally: if infh: infh.close() @@ -351,6 +355,13 @@ def getBranding(self, branding): branding['product-build'] = self._build_number return branding + def setRepoGpgCheck(self, value): + logger.log("%s: setRepoGpgCheck(%s)" % (self, value)) + self._repo_gpg_check = value + + def setGpgCheck(self, value): + logger.log("%s: setGpgCheck(%s)" % (self, value)) + self._gpg_check = value class UpdateYumRepository(YumRepositoryWithInfo): """Represents a Yum repository containing packages and associated meta data for an update.""" @@ -821,12 +832,59 @@ def installFromYum(targets, mounts, progress_callback, cachedir): rv = p.wait() stderr.seek(0) stderr = stderr.read() + gpg_uncaught_error = 0 + gpg_error_pubring_import = 0 + gpg_error_not_signed = 0 + gpg_error_bad_repo_sig = 0 + gpg_error_rpm_missing_key = None + gpg_error_rpm_not_signed = None + gpg_error_rpm_not_found = None if stderr: logger.log("YUM stderr: %s" % stderr.strip()) + if stderr.find(' in import_key_to_pubring') >= 0: + gpg_error_pubring_import = 1 + # add any other instance of uncaught GpgmeError before this like + elif stderr.find('gpgme.GpgmeError: ') >= 0: + gpg_uncaught_error = 1 + + elif re.search("Couldn't open file [^ ]*/repodata/repomd.xml.asc", stderr): + # would otherwise be mistaken for "pubring import" !? + gpg_error_not_signed = 1 + elif stderr.find('repomd.xml signature could not be verified') >= 0: + gpg_error_bad_repo_sig = 1 + + else: + match = re.search("Public key for ([^ ]*.rpm) is not installed", stderr) + if match: + gpg_error_rpm_missing_key = match.group(1) + match = re.search("Package ([^ ]*.rpm) is not signed", stderr) + if match: + gpg_error_rpm_not_signed = match.group(1) + match = re.search(r" ([^ ]*): \[Errno [0-9]*\] No more mirrors to try", stderr) + if match: + gpg_error_rpm_not_found = match.group(1) + if rv: logger.log("Yum exited with %d" % rv) - raise ErrorInstallingPackage("Error installing packages") + if gpg_error_pubring_import: + errmsg = "Signature key import failed" + elif gpg_uncaught_error: + errmsg = "Cryptography-related yum crash" + elif gpg_error_not_signed: + errmsg = "No signature on repository metadata" + elif gpg_error_bad_repo_sig: + errmsg = "Repository signature verification failure" + elif gpg_error_rpm_missing_key: + errmsg = "Missing key for %s" % (gpg_error_rpm_missing_key,) + elif gpg_error_rpm_not_signed: + errmsg = "Package not signed: %s" % (gpg_error_rpm_not_signed,) + elif gpg_error_rpm_not_found: + # rpm not found or corrupted/re-signed/etc + errmsg = "Cannot find valid rpm for %s" % (gpg_error_rpm_not_found,) + else: + errmsg = "Error installing packages" + raise ErrorInstallingPackage(errmsg) shutil.rmtree(os.path.join(mounts['root'], cachedir)) diff --git a/restore.py b/restore.py index 3f4248da..01637f69 100644 --- a/restore.py +++ b/restore.py @@ -135,7 +135,11 @@ def restoreFromBackup(backup, progress=lambda x: ()): backend.setEfiBootEntry(mounts, disk, boot_partnum, constants.INSTALL_TYPE_RESTORE, branding) else: if location == constants.BOOT_LOCATION_MBR: - backend.installGrub2(mounts, disk, False) + if diskutil.is_raid(disk): + for member in diskutil.getDeviceSlaves(disk): + backend.installGrub2(mounts, member, False) + else: + backend.installGrub2(mounts, disk, False) else: backend.installGrub2(mounts, restore_partition, True) else: diff --git a/tui/__init__.py b/tui/__init__.py index 84975578..b0d709bd 100644 --- a/tui/__init__.py +++ b/tui/__init__.py @@ -8,6 +8,7 @@ import constants import sys from xcp import logger +import platform screen = None help_pad = [33, 17, 16] @@ -28,7 +29,7 @@ def global_help(screen, context): def init_ui(): global screen screen = SnackScreen() - screen.drawRootText(0, 0, "Welcome to %s - Version %s" % (PRODUCT_BRAND or PLATFORM_NAME, PRODUCT_VERSION_TEXT)) + screen.drawRootText(0, 0, "Welcome to %s - Version %s (Kernel %s)" % (PRODUCT_BRAND or PLATFORM_NAME, PRODUCT_VERSION_TEXT, platform.release())) if PRODUCT_BRAND: if len(COPYRIGHT_YEARS) > 0: screen.drawRootText(0, 1, "Copyright (c) %s %s" % (COPYRIGHT_YEARS, COMPANY_NAME_LEGAL)) diff --git a/tui/installer/__init__.py b/tui/installer/__init__.py index afe17fa8..ffc41ff0 100644 --- a/tui/installer/__init__.py +++ b/tui/installer/__init__.py @@ -127,6 +127,7 @@ def out_of_order_pool_upgrade_fn(answers): results['preserve-settings'] = False seq = [ + Step(uis.kernel_warning), Step(uis.welcome_screen), Step(uis.eula_screen), Step(uis.hardware_warnings, diff --git a/tui/installer/screens.py b/tui/installer/screens.py index 975015ea..5d836542 100644 --- a/tui/installer/screens.py +++ b/tui/installer/screens.py @@ -41,6 +41,28 @@ def selectDefault(key, entries): return text, k return None +# kernel-alt warning +def kernel_warning(answers): + if answers.get("kernel-alt"): + button = snackutil.ButtonChoiceWindowEx( + tui.screen, + "Alternate kernel", + """WARNING: you chose to install our alternative kernel (kernel-alt). + +It is based on our main kernel + upstream kernel.org patches, so it should be stable by construction. However it receives less testing than the main kernel. + +A boot menu entry for kernel-alt will be added, but we will still boot the main kernel by default. + +If kernel-alt works BETTER than the main kernel for you, TELL US so that we may fix the main kernel! +""", + ['Ok', 'Reboot'], width=60) + + if button == 'ok' or button is None: + return True + else: + return EXIT + return True + # welcome screen: def welcome_screen(answers): driver_answers = {'driver-repos': []} @@ -516,6 +538,56 @@ def setup_runtime_networking(answers): # Get the answers from the user return tui.network.requireNetworking(answers, defaults) +def raid_array_ui(answers): + disk_entries = sorted_disk_list() + raid_disks = [de for de in disk_entries if diskutil.is_raid(de)] + raid_slaves = [slave for master in raid_disks for slave in diskutil.getDeviceSlaves(master)] + entries = [] + for de in disk_entries: + if de not in raid_slaves and de not in raid_disks: + vendor, model, size = diskutil.getExtendedDiskInfo(de) + string_entry = "%s - %s [%s %s]" % ( + diskutil.getHumanDiskName(de), diskutil.getHumanDiskSize(size), vendor, model) + entries.append((string_entry, de)) + if len(entries) < 2: + return SKIP_SCREEN + text = TextboxReflowed(54, "Do you want to group disks in a software RAID 1 array? \n\n" + + "The array will be created immediately and erase all the target disks.") + buttons = ButtonBar(tui.screen, [('Create', 'create'), ('Back', 'back')]) + scroll, _ = snackutil.scrollHeight(3, len(entries)) + cbt = CheckboxTree(3, scroll) + for (c_text, c_item) in entries: + cbt.append(c_text, c_item, False) + gf = GridFormHelp(tui.screen, 'RAID Array', 'guestdisk:info', 1, 4) + gf.add(text, 0, 0, padding=(0, 0, 0, 1)) + gf.add(cbt, 0, 1, padding=(0, 0, 0, 1)) + gf.add(buttons, 0, 3, growx=1) + gf.addHotKey('F5') + + tui.update_help_line([None, " disk info"]) + loop = True + while loop: + rc = gf.run() + if rc == 'F5': + disk_more_info(cbt.getCurrent()) + else: + loop = False + tui.screen.popWindow() + tui.screen.popHelpLine() + + button = buttons.buttonPressed(rc) + if button == 'create': + selected = cbt.getSelection() + txt = 'The content of the disks %s will be deleted when you activate "Ok"' % (str(selected)) + title = 'RAID array creation' + confirmation = snackutil.ButtonChoiceWindowEx(tui.screen, title, txt, ('Ok', 'Cancel'), 40, default=1) + if confirmation == 'ok': + answers['raid'] = {'/dev/md127': selected} + tui.progress.showMessageDialog("Please wait", "Creating raid array...") + diskutil.create_raid(answers['raid']) + tui.progress.clearModelessDialog() + return REPEAT_STEP + def disk_more_info(context): if not context: return True @@ -544,14 +616,18 @@ def disk_more_info(context): return True def sorted_disk_list(): - return sorted(diskutil.getQualifiedDiskList(), - lambda x, y: len(x) == len(y) and cmp(x,y) or (len(x)-len(y))) + return sorted(set(diskutil.getQualifiedDiskList()), + lambda x, y: len(x) == len(y) and cmp(x, y) or (len(x) - len(y))) + +def filter_out_raid_member(diskEntries): + raid_disks = [de for de in diskEntries if diskutil.is_raid(de)] + raid_slaves = set(member for master in raid_disks for member in diskutil.getDeviceSlaves(master)) + return [e for e in diskEntries if e not in raid_slaves] # select drive to use as the Dom0 disk: def select_primary_disk(answers): button = None - diskEntries = sorted_disk_list() - + diskEntries = filter_out_raid_member(sorted_disk_list()) entries = [] target_is_sr = {} min_primary_disk_size = constants.min_primary_disk_size @@ -586,6 +662,15 @@ def select_primary_disk(answers): tui.update_help_line([None, " more info"]) + if len(entries) < 2: + logger.log("not enough disks, not proposing RAID creation") + propose_raid = False + elif any(disk.startswith("/dev/md") for label, disk in entries): + logger.log("existing md found, not proposing RAID creation") + propose_raid = False + else: + propose_raid = True + scroll, height = snackutil.scrollHeight(4, len(entries)) (button, entry) = snackutil.ListboxChoiceWindowEx( tui.screen, @@ -594,7 +679,8 @@ def select_primary_disk(answers): You may need to change your system settings to boot from this disk.""" % (MY_PRODUCT_BRAND), entries, - ['Ok', 'Back'], 55, scroll, height, default, help='pridisk:info', + ['Ok', 'Software RAID', 'Back'] if propose_raid else ['Ok', 'Back'], + 55, scroll, height, default, help='pridisk:info', hotkeys={'F5': disk_more_info}) tui.screen.popHelpLine() @@ -621,7 +707,12 @@ def select_primary_disk(answers): else: answers["preserve-first-partition"] = 'false' - if button is None: return SKIP_SCREEN + # XCP-ng: we replaced `SKIP_SCREEN` by `RIGHT_FORWARDS` for RAID support to avoid a loop after raid creation + if button is None: return RIGHT_FORWARDS + + # XCP-ng + if button == 'software raid': + return raid_array_ui(answers) return RIGHT_FORWARDS @@ -646,7 +737,7 @@ def check_sr_space(answers): return EXIT def select_guest_disks(answers): - diskEntries = sorted_disk_list() + diskEntries = filter_out_raid_member(sorted_disk_list()) # CA-38329: filter out device mapper nodes (except primary disk) as these won't exist # at XenServer boot and therefore cannot be added as physical volumes to Local SR. @@ -664,7 +755,7 @@ def select_guest_disks(answers): currently_selected = answers['guest-disks'] else: currently_selected = answers['primary-disk'] - srtype = constants.SR_TYPE_LVM + srtype = constants.SR_TYPE_EXT if 'sr-type' in answers: srtype = answers['sr-type'] @@ -679,16 +770,19 @@ def select_guest_disks(answers): cbt = CheckboxTree(3, scroll) for (c_text, c_item) in entries: cbt.append(c_text, c_item, c_item in currently_selected) - txt = "Enable thin provisioning" - if len(BRAND_VDI) > 0: - txt += " (Optimized storage for %s)" % BRAND_VDI - tb = Checkbox(txt, srtype == constants.SR_TYPE_EXT and 1 or 0) - - gf = GridFormHelp(tui.screen, 'Virtual Machine Storage', 'guestdisk:info', 1, 4) + rb_title = Textbox(15, 1, "Storage type") + rb = RadioBar(tui.screen, (("EXT: file based. Thin provisioning.", + constants.SR_TYPE_EXT, srtype == constants.SR_TYPE_EXT), + ("LVM: block based. Thick provisioning.", + constants.SR_TYPE_LVM, srtype == constants.SR_TYPE_LVM), + )) + + gf = GridFormHelp(tui.screen, 'Virtual Machine Storage', 'guestdisk:info', 1, 5) gf.add(text, 0, 0, padding=(0, 0, 0, 1)) gf.add(cbt, 0, 1, padding=(0, 0, 0, 1)) - gf.add(tb, 0, 2, padding=(0, 0, 0, 1)) - gf.add(buttons, 0, 3, growx=1) + gf.add(rb_title, 0, 2, padding=(0, 0, 0, 1)) + gf.add(rb, 0, 3, padding=(0, 0, 0, 1)) + gf.add(buttons, 0, 4, growx=1) gf.addHotKey('F5') tui.update_help_line([None, " more info"]) @@ -708,7 +802,7 @@ def select_guest_disks(answers): if button == 'back': return LEFT_BACKWARDS answers['guest-disks'] = cbt.getSelection() - answers['sr-type'] = tb.selected() and constants.SR_TYPE_EXT or constants.SR_TYPE_LVM + answers['sr-type'] = rb.getSelection() answers['sr-on-primary'] = answers['primary-disk'] in answers['guest-disks'] # if the user select no disks for guest storage, check this is what @@ -809,7 +903,7 @@ def ns_callback((enabled, )): for entry in [ns1_entry, ns2_entry, ns3_entry]: entry.setFlags(FLAG_DISABLED, enabled) - hide_rb = answers['net-admin-configuration'].isStatic() + hide_rb = not answers['net-admin-configuration'].isDynamic() # HOSTNAME: hn_title = Textbox(len("Hostname Configuration"), 1, "Hostname Configuration") @@ -939,7 +1033,7 @@ def nsvalue(answers, id): answers['manual-nameservers'][1].append(ns2_entry.value()) if ns3_entry.value() != '': answers['manual-nameservers'][1].append(ns3_entry.value()) - if 'net-admin-configuration' in answers and answers['net-admin-configuration'].isStatic(): + if 'net-admin-configuration' in answers and not answers['net-admin-configuration'].isDynamic(): answers['net-admin-configuration'].dns = answers['manual-nameservers'][1] else: answers['manual-nameservers'] = (False, None) @@ -1022,7 +1116,7 @@ def get_time_configuration_method(answers): default = None if "ntp-config-method" in answers: default = selectDefault(answers['ntp-config-method'], entries) - if answers['net-admin-configuration'].isStatic(): + if not answers['net-admin-configuration'].isDynamic(): default = ENTRY_DEFAULT_NTP (button, entry) = ListboxChoiceWindow( diff --git a/tui/network.py b/tui/network.py index faf87dc9..126974ee 100644 --- a/tui/network.py +++ b/tui/network.py @@ -9,128 +9,215 @@ from netinterface import * import version import os +import time +import socket from snack import * def get_iface_configuration(nic, txt=None, defaults=None, include_dns=False): - - def use_vlan_cb_change(): - vlan_field.setFlags(FLAG_DISABLED, vlan_cb.value()) - - def dhcp_change(): - for x in [ ip_field, gateway_field, subnet_field, dns_field ]: - x.setFlags(FLAG_DISABLED, not dhcp_rb.selected()) - - gf = GridFormHelp(tui.screen, 'Networking', 'ifconfig', 1, 8) - if txt is None: - txt = "Configuration for %s (%s)" % (nic.name, nic.hwaddr) - text = TextboxReflowed(45, txt) - b = [("Ok", "ok"), ("Back", "back")] - buttons = ButtonBar(tui.screen, b) - - ip_field = Entry(16) - subnet_field = Entry(16) - gateway_field = Entry(16) - dns_field = Entry(16) - vlan_field = Entry(16) - - if defaults and defaults.isStatic(): - # static configuration defined previously - dhcp_rb = SingleRadioButton("Automatic configuration (DHCP)", None, 0) - dhcp_rb.setCallback(dhcp_change, ()) - static_rb = SingleRadioButton("Static configuration:", dhcp_rb, 1) - static_rb.setCallback(dhcp_change, ()) - if defaults.ipaddr: - ip_field.set(defaults.ipaddr) - if defaults.netmask: - subnet_field.set(defaults.netmask) - if defaults.gateway: - gateway_field.set(defaults.gateway) - if defaults.dns: - dns_field.set(defaults.dns[0]) - else: - dhcp_rb = SingleRadioButton("Automatic configuration (DHCP)", None, 1) + def choose_primary_address_type(nic): + gf = GridFormHelp(tui.screen, 'Networking', 'Address type', 1, 8) + txt = "Choose an address type for %s (%s)" % (nic.name, nic.hwaddr) + text = TextboxReflowed(45, txt) + + b = [("Ok", "ok"), ("Back", "back")] + buttons = ButtonBar(tui.screen, b) + + # IPv4 by default + ipv4_rb = SingleRadioButton("IPv4", None, 1) + ipv6_rb = SingleRadioButton("IPv6", ipv4_rb, 0) + dual_rb = SingleRadioButton("Dual stack (IPv4 primary)", ipv6_rb, 0) + + gf.add(text, 0, 0, padding=(0, 0, 0, 1)) + gf.add(ipv4_rb, 0, 2, anchorLeft=True) + gf.add(ipv6_rb, 0, 3, anchorLeft=True) + gf.add(dual_rb, 0, 4, anchorLeft=True) + gf.add(buttons, 0, 5, growx=1) + + loop = True + direction = LEFT_BACKWARDS + address_type = None + while loop: + result = gf.run() + if buttons.buttonPressed(result) == 'back': + loop = False + elif buttons.buttonPressed(result) == 'ok': + value = None + if ipv4_rb.selected(): + value = "ipv4" + elif ipv6_rb.selected(): + value = "ipv6" + elif dual_rb.selected(): + value = "dual" + loop = False + direction = RIGHT_FORWARDS + address_type = value + + tui.screen.popWindow() + return direction, address_type + + def get_ip_configuration(nic, txt, defaults, include_dns, iface_class): + def use_vlan_cb_change(): + vlan_field.setFlags(FLAG_DISABLED, vlan_cb.value()) + + def dhcp_change(): + for x in [ ip_field, gateway_field, subnet_field, dns_field ]: + x.setFlags(FLAG_DISABLED, static_rb.selected()) + + ipv6 = iface_class == NetInterfaceV6 + + gf = GridFormHelp(tui.screen, 'Networking', 'ifconfig', 1, 10) + if txt is None: + txt = "Configuration for %s (%s)" % (nic.name, nic.hwaddr) + text = TextboxReflowed(45, txt) + b = [("Ok", "ok"), ("Back", "back")] + buttons = ButtonBar(tui.screen, b) + + #TODO? Change size for IPv6? If so which size? + ip_field = Entry(16) + subnet_field = Entry(16) + gateway_field = Entry(16) + dns_field = Entry(16) + vlan_field = Entry(16) + + static = bool(defaults and (defaults.modev6 if ipv6 else defaults.mode) == NetInterface.Static) + dhcp_rb = SingleRadioButton("Automatic configuration (DHCP)", None, not static) dhcp_rb.setCallback(dhcp_change, ()) - static_rb = SingleRadioButton("Static configuration:", dhcp_rb, 0) + static_rb = SingleRadioButton("Static configuration:", dhcp_rb, static) static_rb.setCallback(dhcp_change, ()) - ip_field.setFlags(FLAG_DISABLED, False) - subnet_field.setFlags(FLAG_DISABLED, False) - gateway_field.setFlags(FLAG_DISABLED, False) - dns_field.setFlags(FLAG_DISABLED, False) - - vlan_cb = Checkbox("Use VLAN:", defaults.isVlan() if defaults else False) - vlan_cb.setCallback(use_vlan_cb_change, ()) - if defaults and defaults.isVlan(): - vlan_field.set(str(defaults.vlan)) - else: - vlan_field.setFlags(FLAG_DISABLED, False) - - ip_text = Textbox(15, 1, "IP Address:") - subnet_text = Textbox(15, 1, "Subnet mask:") - gateway_text = Textbox(15, 1, "Gateway:") - dns_text = Textbox(15, 1, "Nameserver:") - vlan_text = Textbox(15, 1, "VLAN (1-4094):") - - entry_grid = Grid(2, include_dns and 4 or 3) - entry_grid.setField(ip_text, 0, 0) - entry_grid.setField(ip_field, 1, 0) - entry_grid.setField(subnet_text, 0, 1) - entry_grid.setField(subnet_field, 1, 1) - entry_grid.setField(gateway_text, 0, 2) - entry_grid.setField(gateway_field, 1, 2) - if include_dns: - entry_grid.setField(dns_text, 0, 3) - entry_grid.setField(dns_field, 1, 3) - - vlan_grid = Grid(2, 1) - vlan_grid.setField(vlan_text, 0, 0) - vlan_grid.setField(vlan_field, 1, 0) - - gf.add(text, 0, 0, padding=(0, 0, 0, 1)) - gf.add(dhcp_rb, 0, 2, anchorLeft=True) - gf.add(static_rb, 0, 3, anchorLeft=True) - gf.add(entry_grid, 0, 4, padding=(0, 0, 0, 1)) - gf.add(vlan_cb, 0, 5, anchorLeft=True) - gf.add(vlan_grid, 0, 6, padding=(0, 0, 0, 1)) - gf.add(buttons, 0, 7, growx=1) - - loop = True - while loop: - result = gf.run() - - if buttons.buttonPressed(result) in ['ok', None]: - # validate input - msg = '' - if static_rb.selected(): - if not netutil.valid_ip_addr(ip_field.value()): - msg = 'IP Address' - elif not netutil.valid_ip_addr(subnet_field.value()): - msg = 'Subnet mask' - elif gateway_field.value() != '' and not netutil.valid_ip_addr(gateway_field.value()): - msg = 'Gateway' - elif dns_field.value() != '' and not netutil.valid_ip_addr(dns_field.value()): - msg = 'Nameserver' - if vlan_cb.selected(): - if not netutil.valid_vlan(vlan_field.value()): - msg = 'VLAN' - if msg != '': - tui.progress.OKDialog("Networking", "Invalid %s, please check the field and try again." % msg) + if ipv6: + autoconf_rb = SingleRadioButton("Automatic configuration (Autoconf)", static_rb, 0) + autoconf_rb.setCallback(dhcp_change, ()) + dhcp_change() + + if defaults: + if ipv6: + if defaults.ipv6addr: + ip6addr, netmask = defaults.ipv6addr.split("/") + ip_field.set(ip6addr) + subnet_field.set(netmask) + if defaults.ipv6_gateway: + gateway_field.set(defaults.ipv6_gateway) + else: + if defaults.ipaddr: + ip_field.set(defaults.ipaddr) + if defaults.netmask: + subnet_field.set(defaults.netmask) + if defaults.gateway: + gateway_field.set(defaults.gateway) + + if defaults.dns: + dns_field.set(defaults.dns[0]) + + vlan_cb = Checkbox("Use VLAN:", defaults.isVlan() if defaults else False) + vlan_cb.setCallback(use_vlan_cb_change, ()) + if defaults and defaults.isVlan(): + vlan_field.set(str(defaults.vlan)) + else: + vlan_field.setFlags(FLAG_DISABLED, False) + + ip_msg = "IPv6 Address" if ipv6 else "IP Address" + mask_msg = "CIDR (4-128)" if ipv6 else "Subnet mask" + ip_text = Textbox(15, 1, "%s:" % ip_msg) + subnet_text = Textbox(15, 1, "%s:" % mask_msg) + gateway_text = Textbox(15, 1, "Gateway:") + dns_text = Textbox(15, 1, "Nameserver:") + vlan_text = Textbox(15, 1, "VLAN (1-4094):") + + entry_grid = Grid(2, include_dns and 4 or 3) + entry_grid.setField(ip_text, 0, 0) + entry_grid.setField(ip_field, 1, 0) + entry_grid.setField(subnet_text, 0, 1) + entry_grid.setField(subnet_field, 1, 1) + entry_grid.setField(gateway_text, 0, 2) + entry_grid.setField(gateway_field, 1, 2) + if include_dns: + entry_grid.setField(dns_text, 0, 3) + entry_grid.setField(dns_field, 1, 3) + + vlan_grid = Grid(2, 1) + vlan_grid.setField(vlan_text, 0, 0) + vlan_grid.setField(vlan_field, 1, 0) + + gf.add(text, 0, 0, padding=(0, 0, 0, 1)) + gf.add(dhcp_rb, 0, 2, anchorLeft=True) + gf.add(static_rb, 0, 3, anchorLeft=True) + gf.add(entry_grid, 0, 4, padding=(0, 0, 0, 1)) + if ipv6: + gf.add(autoconf_rb, 0, 5, anchorLeft=True) + # One more line for IPv6 autoconf + gf.add(vlan_cb, 0, 5 + ipv6, anchorLeft=True) + gf.add(vlan_grid, 0, 6 + ipv6, padding=(0, 0, 0, 1)) + gf.add(buttons, 0, 7 + ipv6, growx=1) + + loop = True + ip_family = socket.AF_INET6 if ipv6 else socket.AF_INET + while loop: + result = gf.run() + + if buttons.buttonPressed(result) in ['ok', None]: + # validate input + msg = '' + if static_rb.selected(): + invalid_subnet = int(subnet_field.value()) > 128 or int(subnet_field.value()) < 4 if ipv6 else not netutil.valid_ipv4_addr(subnet_field.value()) + if not netutil.valid_ip_address_family(ip_field.value(), ip_family): + msg = ip_msg + elif invalid_subnet: + msg = mask_msg + elif gateway_field.value() != '' and not netutil.valid_ip_address_family(gateway_field.value(), ip_family): + msg = 'Gateway' + elif dns_field.value() != '' and not netutil.valid_ip_address_family(dns_field.value(), ip_family): + msg = 'Nameserver' + if vlan_cb.selected(): + if not netutil.valid_vlan(vlan_field.value()): + msg = 'VLAN' + if msg != '': + tui.progress.OKDialog("Networking", "Invalid %s, please check the field and try again." % msg) + else: + loop = False else: loop = False + + tui.screen.popWindow() + + if buttons.buttonPressed(result) == 'back': return LEFT_BACKWARDS, None + + vlan_value = int(vlan_field.value()) if vlan_cb.selected() else None + if dhcp_rb.selected(): + answers = iface_class(NetInterface.DHCP, nic.hwaddr, vlan=vlan_value) + elif ipv6 and autoconf_rb.selected(): + answers = iface_class(NetInterface.Autoconf, nic.hwaddr, vlan=vlan_value) else: - loop = False + answers = iface_class(NetInterface.Static, nic.hwaddr, ip_field.value(), + subnet_field.value(), gateway_field.value(), + dns_field.value(), vlan=vlan_value) - tui.screen.popWindow() + return RIGHT_FORWARDS, answers - if buttons.buttonPressed(result) == 'back': return LEFT_BACKWARDS, None + direction, address_type = choose_primary_address_type(nic) + if direction == LEFT_BACKWARDS: + return LEFT_BACKWARDS, None + + answers = None + if address_type in ["ipv4", "dual"]: + direction, answers = get_ip_configuration(nic, txt, defaults, include_dns, NetInterface) + if direction == LEFT_BACKWARDS: + return LEFT_BACKWARDS, None + + if address_type in ["ipv6", "dual"]: + direction, answers_ipv6 = get_ip_configuration(nic, txt, defaults, include_dns, NetInterfaceV6) + if direction == LEFT_BACKWARDS: + return LEFT_BACKWARDS, None + + if answers == None: + answers = answers_ipv6 + else: + answers.modev6 = answers_ipv6.modev6 + answers.ipv6addr = answers_ipv6.ipv6addr + answers.ipv6_gateway = answers_ipv6.ipv6_gateway + if answers_ipv6.dns != None: + answers.dns = answers_ipv6.dns if answers.dns == None else answers.dns + answers_ipv6.dns - vlan_value = int(vlan_field.value()) if vlan_cb.selected() else None - if bool(dhcp_rb.selected()): - answers = NetInterface(NetInterface.DHCP, nic.hwaddr, vlan=vlan_value) - else: - answers = NetInterface(NetInterface.Static, nic.hwaddr, ip_field.value(), - subnet_field.value(), gateway_field.value(), - dns_field.value(), vlan=vlan_value) return RIGHT_FORWARDS, answers def select_netif(text, conf, offer_existing=False, default=None): @@ -286,23 +373,37 @@ def specify_configuration(answers, txt, defaults): ifaceName = conf_dict['config'].getInterfaceName(conf_dict['interface']) netutil.ifdown(ifaceName) - # check that we have *some* network: - if netutil.ifup(ifaceName) != 0 or not netutil.interfaceUp(ifaceName): + def display_error(): tui.progress.clearModelessDialog() tui.progress.OKDialog("Networking", "The network still does not appear to be active. Please check your settings, and try again.") - direction = REPEAT_STEP - else: - if answers and type(answers) == dict: - # write out results - answers[interface_key] = conf_dict['interface'] - answers[config_key] = conf_dict['config'] - # update cache of manual configurations - manual_config = {} - all_dhcp = False - if 'runtime-iface-configuration' in answers: - manual_config = answers['runtime-iface-configuration'][1] - manual_config[conf_dict['interface']] = conf_dict['config'] - answers['runtime-iface-configuration'] = (all_dhcp, manual_config) - tui.progress.clearModelessDialog() + return REPEAT_STEP + + if netutil.ifup(ifaceName) != 0: + return display_error() + + # For Autoconf wait a bit for network setup + try_nb = 20 if conf_dict['config'].modev6 == NetInterface.Autoconf else 0 + while True: + if try_nb == 0 or netutil.interfaceUp(ifaceName): + break + try_nb -= 1 + time.sleep(0.1) + + # check that we have *some* network: + if not netutil.interfaceUp(ifaceName): + return display_error() + + if answers and type(answers) == dict: + # write out results + answers[interface_key] = conf_dict['interface'] + answers[config_key] = conf_dict['config'] + # update cache of manual configurations + manual_config = {} + all_dhcp = False + if 'runtime-iface-configuration' in answers: + manual_config = answers['runtime-iface-configuration'][1] + manual_config[conf_dict['interface']] = conf_dict['config'] + answers['runtime-iface-configuration'] = (all_dhcp, manual_config) + tui.progress.clearModelessDialog() return direction diff --git a/tui/repo.py b/tui/repo.py index a46eda2e..2a23d987 100644 --- a/tui/repo.py +++ b/tui/repo.py @@ -89,6 +89,9 @@ def select_repo_source(answers, title, text, require_base_repo=True): entries = [ ENTRY_LOCAL ] default = ENTRY_LOCAL + if answers.get('netinstall'): + entries = [] + default = ENTRY_URL if len(answers['network-hardware'].keys()) > 0: entries += [ ENTRY_URL, ENTRY_NFS ] @@ -136,6 +139,8 @@ def get_url_location(answers, require_base_repo): user_field.set(answers['source-address'].getUsername()) if answers['source-address'].getPassword() is not None: passwd_field.set(answers['source-address'].getPassword()) + else: + url_field.set('http://mirrors.xcp-ng.org/netinstall/8.3') done = False while not done: diff --git a/upgrade.py b/upgrade.py index 0672b577..1e903eba 100644 --- a/upgrade.py +++ b/upgrade.py @@ -448,6 +448,12 @@ def buildRestoreList(self): # NRPE service config self.restore_list += ['etc/nagios/nrpe.cfg', {'dir': 'etc/nrpe.d'}] + # Keep IPv6 enablement/disablement upon upgrades + self.restore_list += ['etc/sysctl.d/91-net-ipv6.conf'] + + # Keep user multipath configuration + self.restore_list += [{'dir': 'etc/multipath/conf.d', 're': r'custom.*\.conf'}] + completeUpgradeArgs = ['mounts', 'installation-to-overwrite', 'primary-disk', 'backup-partnum', 'logs-partnum', 'net-admin-interface', 'net-admin-bridge', 'net-admin-configuration'] def completeUpgrade(self, mounts, prev_install, target_disk, backup_partnum, logs_partnum, admin_iface, admin_bridge, admin_config):