def __init__(self, if_addr, if_net, if_len, ip_addr_range_bottom, ip_addr_range_top, mac_ip_map=None, allow_unmapped=False, debug=False, ip_mode=4): assert ip_mode in (4, 6) ttbl.power.impl_c.__init__(self) ttbl.tt_power_control_impl.__init__(self) self.allow_unmapped = allow_unmapped if mac_ip_map == None: self._mac_ip_map = {} else: self._mac_ip_map = mac_ip_map # FIXME: move to power_on_do, to get this info from target's tags self._params = dict( ip_mode=ip_mode, tftp_prefix=tftp_prefix, if_net=if_net, if_addr=if_addr, if_len=if_len, ip_addr_range_bottom=ip_addr_range_bottom, ip_addr_range_top=ip_addr_range_top, dhcp_architecture_types=self._mk_pxe_arch_type_config(), ) self.ip_mode = ip_mode if ip_mode == 4: self._params['if_netmask'] = commonl.ipv4_len_to_netmask_ascii( if_len) if allow_unmapped: self._params["allow_known_clients"] = "allow known clients;" else: self._params["allow_known_clients"] = "# all clients allowed" self.debug = debug self.log = None self.target = None # we find this when power_*_do() is called self.state_dir = None self.pxe_dir = None self.dhcpd_pidfile = None
def __init__(self, if_addr, if_net, if_len, ip_addr_range_bottom, ip_addr_range_top, mac_ip_map = None, allow_unmapped = False, debug = False, ip_mode = 4): assert ip_mode in (4, 6) ttbl.tt_power_control_impl.__init__(self) self.allow_unmapped = allow_unmapped if mac_ip_map == None: self._mac_ip_map = {} else: self._mac_ip_map = mac_ip_map # FIXME: move to power_on_do, to get this info from target's tags self._params = dict( ip_mode = ip_mode, tftp_prefix = tftp_prefix, if_net = if_net, if_addr = if_addr, if_len = if_len, ip_addr_range_bottom = ip_addr_range_bottom, ip_addr_range_top = ip_addr_range_top, dhcp_architecture_types = self._mk_pxe_arch_type_config(), ) self.ip_mode = ip_mode if ip_mode == 4: self._params['if_netmask'] = commonl.ipv4_len_to_netmask_ascii(if_len) if allow_unmapped: self._params["allow_known_clients"] = "allow known clients;" else: self._params["allow_known_clients"] = "# all clients allowed" self.debug = debug self.log = None self.target = None # we find this when power_*_do() is called self.state_dir = None self.pxe_dir = None self.dhcpd_pidfile = None
def eval(self, target): SANBOOT_URL = os.environ.get("SANBOOT_URL", None) if SANBOOT_URL == None: raise tcfl.tc.blocked_e("No SANBOOT_URL environment given") target.power.cycle() boot_ic = target.kws['pos_boot_interconnect'] mac_addr = target.kws['interconnects'][boot_ic]['mac_addr'] tcfl.biosl.boot_network_pxe( target, # Eg: UEFI PXEv4 (MAC:4AB0155F98A1) r"UEFI PXEv4 \(MAC:%s\)" % mac_addr.replace(":", "").upper().strip()) # can't wait also for the "ok" -- debugging info might pop in th emiddle target.expect("iPXE initialising devices...") # if the connection is slow, we have to start sending Ctrl-B's # ASAP #target.expect(re.compile("iPXE .* -- Open Source Network Boot Firmware")) # send Ctrl-B to go to the PXE shell, to get manual control of iPXE # # do this as soon as we see the boot message from iPXE because # otherwise by the time we see the other message, it might already # be trying to boot pre-programmed instructions--we'll see the # Ctrl-B message anyway, so we expect for it. # # before sending these "Ctrl-B" keystrokes in ANSI, but we've seen # sometimes the timing window being too tight, so we just blast # the escape sequence to the console. target.console.write("\x02\x02") # use this iface so expecter time.sleep(0.3) target.console.write("\x02\x02") # use this iface so expecter time.sleep(0.3) target.console.write("\x02\x02") # use this iface so expecter time.sleep(0.3) target.expect("Ctrl-B") target.console.write("\x02\x02") # use this iface so expecter time.sleep(0.3) target.console.write("\x02\x02") # use this iface so expecter time.sleep(0.3) target.expect("iPXE>") prompt_orig = target.shell.shell_prompt_regex try: # # When matching end of line, match against \r, since depends # on the console it will send one or two \r (SoL vs SSH-SoL) # before \n -- we removed that in the kernel driver by using # crnl in the socat config # # FIXME: block on anything here? consider infra issues # on "Connection timed out", http://ipxe.org... target.shell.shell_prompt_regex = "iPXE>" kws = dict(target.kws) boot_ic = target.kws['pos_boot_interconnect'] ipv4_addr = target.kws['interconnects'][boot_ic]['ipv4_addr'] ipv4_prefix_len = target.kws['interconnects'][boot_ic]['ipv4_prefix_len'] kws['ipv4_netmask'] = commonl.ipv4_len_to_netmask_ascii(ipv4_prefix_len) if False: #dhcp: target.shell.run("dhcp", re.compile("Configuring.*ok")) target.shell.run("show net0/ip", "ipv4 = %s" % ipv4_addr) else: # static is much faster and we know the IP address already # anyway; but then we don't have DNS as it is way more # complicated to get it target.shell.run("set net0/ip %s" % ipv4_addr) target.shell.run("set net0/netmask %s" % kws['ipv4_netmask']) target.shell.run("ifopen") target.send("sanboot %s" % SANBOOT_URL) # can't use shell.run...it will timeout, since we'll print no more prompt target.expect("Booting from SAN device") finally: target.shell.shell_prompt_regex = prompt_orig
def power_on_pre_pos_setup(target): pos_mode = target.fsdb.get("pos_mode") if pos_mode == None: target.log.info("POS boot: ignoring, pos_mode property not set") return # We only care if mode is set to pxe or local -- local makes us # tell the thing to go boot local disk if pos_mode != "pxe" and pos_mode != "local": target.log.info("POS boot: ignoring, pos_mode set to %s " "(vs PXE or local)" % pos_mode) return boot_ic = target.tags.get('pos_boot_interconnect', None) if boot_ic == None: raise RuntimeError('no "pos_boot_interconnect" tag/property defined, ' 'can\'t boot off network') if not boot_ic in target.tags['interconnects']: raise RuntimeError('this target does not belong to the ' 'boot interconnect "%s" defined in tag ' '"pos_boot_interconnect"' % boot_ic) interconnect = target.tags['interconnects'][boot_ic] # FIXME: at some point, for ic-less POS-PXE boot we could get this # from pos_mac_addr and default to ic['mac_addr'] mac_addr = interconnect['mac_addr'] if pos_mode == "pxe": # now this is dirty -- we kinda hacking here but otherwise, how do # we get to the pos_* kws? ic = ttbl.config.targets[boot_ic] # The service kws = dict(target.tags) if not 'bsp' in target.tags: bsps = target.tags['bsps'].keys() kws['bsp'] = sorted(bsps)[0] kws.update( dict( ipv4_addr=interconnect['ipv4_addr'], ipv4_gateway=interconnect.get('ipv4_gateway', ""), ipv4_netmask=commonl.ipv4_len_to_netmask_ascii( interconnect['ipv4_prefix_len']), mac_addr=mac_addr, name=target.id, )) # There might be a prefix to the path to the boot kernel and # initrd; we let the target override it and default to the # network's or nothing _tag_get_from_ic_target(kws, 'pos_http_url_prefix', ic, target) _tag_get_from_ic_target(kws, 'pos_nfs_server', ic, target) _tag_get_from_ic_target(kws, 'pos_nfs_path', ic, target) _tag_get_from_ic_target(kws, 'pos_image', ic, target, 'tcf-live') # generate configuration for the target to boot the POS's linux # kernel with the root fs over NFS kws['extra_kopts'] = " ".join(pos_cmdline_opts[kws['pos_image']]) # Generate the PXE linux configuration # # note the syslinux/pxelinux format supports no long line # breakage, so we use Python's \ for clearer, shorter lines which # will be pasted all together # # Most of the juicy stuff comes from the pos_cmdline_opts for # each image (see, eg: tcf-live's) which is filled in # extra_opts a few lines above config = """\ say TCF Network boot to Service OS #serial 0 115200 default boot prompt 0 label boot # boot to %(pos_image)s linux %(pos_http_url_prefix)svmlinuz-%(pos_image)s append console=tty0 console=%(linux_serial_console_default)s,115200 \ %(extra_kopts)s """ config = template_rexpand(config, kws) else: # pos_mode is local config = """\ say TCF Network boot redirecting to local boot serial 0 115200 default localboot prompt 0 label localboot localboot 0 """ # Write the TFTP configuration -- when the target boots and does # a DHCP request, DHCP daemon will send it a DHCP address and also # tell it to boot off TFTP with a pxelinux bootloader; it'll load # that bootloader, which will look for different configuration # files off TFTP_DIR/TFTP_PREFIX/pxelinux.cfg/ in a given order, # one of us being 01-ITSMACADDR # # It will find this one we are now making and boot to the POS for # provisioning or to local boot. tftp_config_file_name = os.path.join( tftp_dir, tftp_prefix, "pxelinux.cfg", # 01- is the ARP type 1 for ethernet; also note the PXE client # asks with the hex digits in lower case. "01-" + mac_addr.replace(":", "-").lower()) with open(tftp_config_file_name, "w") as tf: tf.write(config) tf.flush() # We know the file exists, so it is safe to chmod like this os.chmod(tf.name, 0o644)
def _dhcp_conf_write(self, f): kws = dict(self._params) # generate DHCP configuration file based on hackish templating self.log.info( "%(if_name)s: IPv%(ip_mode)d addr/net/mask " "%(if_addr)s/%(if_net)s/%(if_len)s", self._params) if self.ip_mode == 4: # We only do PXE over ipv4 # FIXME: make it so using pxelinux is a configuratio template # (likewise on the tftp side, so we can switch to EFI boot or # whatever we want) # %(dhcp_architecture_types)s is the output of # _mk_pxe_arch_type_config() f.write("""\ option space pxelinux; option pxelinux.magic code 208 = string; option pxelinux.configfile code 209 = text; option pxelinux.pathprefix code 210 = text; option pxelinux.reboottime code 211 = unsigned integer 32; # To be used in the pxeclients class option architecture-type code 93 = unsigned integer 16; subnet %(if_net)s netmask %(if_netmask)s { pool { %(allow_known_clients)s range %(ip_addr_range_bottom)s %(ip_addr_range_top)s; } class "pxeclients" { match if substring (option vendor-class-identifier, 0, 9) = "PXEClient"; # http://www.syslinux.org/wiki/index.php?title=PXELINUX#UEFI %(dhcp_architecture_types)s # Point to the TFTP server, which is the same as this next-server %(if_addr)s; } } """ % self._params) else: f.write("""\ # This one line must be outside any bracketed scope option architecture-type code 93 = unsigned integer 16; subnet6 %(if_net)s/%(if_len)s { range6 %(ip_addr_range_bottom)s %(ip_addr_range_top)s; class "pxeclients" { match if substring (option vendor-class-identifier, 0, 9) = "PXEClient"; # http://www.syslinux.org/wiki/index.php?title=PXELINUX#UEFI %(dhcp_architecture_types)s # Point to the TFTP server, which is the same as this # next-server %(if_addr)s; } } """ % self._params) # Now, enumerate the targets that are in this local # configuration and figure out what's their IP address in # this network; create a hardcoded entry for them. # # FIXME: This leaves a gap, as targets in other servers could # be connected to this network. Sigh. for target_id, target in ttbl.config.targets.iteritems(): interconnects = target.tags.get('interconnects', {}) ic = self.target boot_ic = target.tags.get('pos_boot_interconnect', None) if boot_ic == None: ic.log.info('%s: target has no "pos_boot_interconnect" ' 'tag/property defined, ignoring' % target_id) continue # FIXME: these two checks shall be consistency done when # the target is being added if not boot_ic in target.tags['interconnects']: raise RuntimeError('%s: target does not belong to the ' 'boot interconnect "%s" defined in tag ' '"pos_boot_interconnect"' % (target_id, boot_ic)) if not boot_ic in ttbl.config.targets: raise RuntimeError('%s: this target\'s boot interconnect %s ' 'defined in "pos_boot_interconnect" tag ' 'is not available in this server' % (target_id, boot_ic)) if not 'bsp' in target.tags: bsps = target.tags['bsps'].keys() kws['bsp'] = sorted(bsps)[0] kws.update( dict( ipv4_gateway=ic.tags.get('ipv4_gateway', ""), ipv4_netmask=commonl.ipv4_len_to_netmask_ascii( ic.tags['ipv4_prefix_len']), name=target.id, )) # There might be a prefix to the path to the boot kernel and # initrd; we let the target override it and default to the # network's or nothing # FIXME: need v6 nfs_server and http_url _tag_get_from_ic_target(kws, 'pos_http_url_prefix', ic, target) _tag_get_from_ic_target(kws, 'pos_nfs_server', ic, target) _tag_get_from_ic_target(kws, 'pos_nfs_path', ic, target) for ic_id, interconnect in interconnects.iteritems(): if '#' in ic_id: real_ic_id, instance = ic_id.split("#", 1) kws['hostname'] = target.id + "-" + instance else: real_ic_id = ic_id kws['hostname'] = target.id if real_ic_id != self.target.id: continue kws['mac_addr'] = interconnect.get('mac_addr', None) kws['ipv4_addr'] = interconnect.get('ipv4_addr', None) kws['ipv6_addr'] = interconnect.get('ipv6_addr', None) if self.ip_mode == 4: config = """\ host %(hostname)s { hardware ethernet %(mac_addr)s; fixed-address %(ipv4_addr)s; option host-name "%(hostname)s"; # note how we are forcing NFSv3, as it might default to v2 # FIXME: parameter? # Also UDP, more resilient for our use and soft so we can # recover in some cases more easily option root-path "%(pos_nfs_server)s:%(pos_nfs_path)s,udp,soft,nfsvers=3"; } """ else: config = """\ host %(hostname)s { hardware ethernet %(mac_addr)s; fixed-address6 %(ipv6_addr)s; option host-name "%(hostname)s"; # note how we are forcing NFSv3, as it might default to v2 # FIXME: parameter? # Also UDP, more resilient for our use and soft so we can # recover in some cases more easily # FIXME: pos_nfs_server6? option root-path "%(pos_nfs_server)s:%(pos_nfs_path)s,udp,soft,nfsvers=3"; } """ f.write(template_rexpand(config, kws))
def power_on_pre_pos_setup(target): """ Hook called before power on to setup TFTP to boot a target in Provisioning Mode The DHCP server started by :mod:`ttbl.dnsmasq` or :mod:`ttbl.dhcp` are configured to direct a target to PXE boot *syslinux*; this will ask the TFTP server for a config file for the target's MAC address. This function is called before powering on the target to create said configuration file; based on the value of the target's *pos_mode* property, a config file that boots the Provisioning OS or that redirects to the local disk will be created. """ pos_mode = target.fsdb.get("pos_mode") if pos_mode == None: target.log.info("POS boot: ignoring, pos_mode property not set") return # We only care if mode is set to pxe or local -- local makes us # tell the thing to go boot local disk # if none, we assume go local if pos_mode != "pxe" and pos_mode != "local": pos_mode = "local" boot_ic = target.tags.get('pos_boot_interconnect', None) if boot_ic == None: raise RuntimeError("CONFIG ERROR: no 'pos_boot_interconnect'" " tag defined, can't boot off network") if not boot_ic in target.tags['interconnects']: raise RuntimeError("CONFIG ERROR: this target does not belong to" " the boot interconnect '%s' defined in tag " "'pos_boot_interconnect'" % boot_ic) interconnect = target.tags['interconnects'][boot_ic] # FIXME: at some point, for ic-less POS-PXE boot we could get this # from pos_mac_addr and default to ic['mac_addr'] mac_addr = interconnect['mac_addr'] # we need the interconnect object to get some values ic = ttbl.test_target.get(boot_ic) if pos_mode == "local": # pos_mode is local config = """\ say TCF Network boot redirecting to local boot serial 0 115200 default localboot prompt 0 label localboot localboot """ else: # PXE mode # get some values we need to generate the Syslinux config file kws = dict(target.tags) if not 'bsp' in target.tags: bsps = target.tags.get('bsps', {}).keys() if bsps: kws['bsp'] = sorted(bsps)[0] kws.update( dict( ipv4_addr=interconnect['ipv4_addr'], ipv4_gateway=interconnect.get('ipv4_gateway', ""), ipv4_netmask=commonl.ipv4_len_to_netmask_ascii( interconnect['ipv4_prefix_len']), mac_addr=mac_addr, name=target.id, )) # There might be a prefix to the path to the boot kernel and # initrd; we let the target override it and default to the # network's or nothing tag_get_from_ic_target(kws, 'pos_http_url_prefix', ic, target) tag_get_from_ic_target(kws, 'pos_nfs_server', ic, target) tag_get_from_ic_target(kws, 'pos_nfs_path', ic, target) tag_get_from_ic_target(kws, 'pos_image', ic, target, 'tcf-live') # generate configuration for the target to boot the POS's linux # kernel with the root fs over NFS kws['extra_kopts'] = " ".join(pos_cmdline_opts[kws['pos_image']]) # Generate the PXE linux configuration # # note the syslinux/pxelinux format supports no long line # breakage, so we use Python's \ for clearer, shorter lines which # will be pasted all together # # Most of the juicy stuff comes from the pos_cmdline_opts for # each image (see, eg: tcf-live's) which is filled in # extra_opts a few lines above # ## serial 0 115200 config = """\ say TCF Network boot to Provisioning OS default boot prompt 0 label boot linux %(pos_http_url_prefix)svmlinuz-%(pos_image)s append console=tty0 console=%(linux_serial_console_default)s,115200 \ %(extra_kopts)s """ config = template_rexpand(config, kws) # Write the TFTP configuration -- when the target boots and does # a DHCP request, DHCP daemon will send it a DHCP address and also # tell it to boot off TFTP with a pxelinux bootloader; it'll load # that bootloader, which will look for different configuration # files off TFTP_ROOT/pxelinux.cfg/ in a given order, # one of us being 01-ITSMACADDR # # It will find this one we are now making and boot to the POS for # provisioning or to local boot. tftp_root_ic = os.path.join(ic.state_dir, "tftp.root") if os.path.isdir(tftp_root_ic): # the configuration has one TFTP root per interconnect, drop # it there (eg: when using ttbl.dnsmasq) tftp_root = tftp_root_ic pxelinux_cfg_dir = os.path.join(tftp_root, "pxelinux.cfg") else: # One global TFTP (eg: when using the system's global tftp # daemon) tftp_prefix = "ttbd" + ttbl.config.instance_suffix pxelinux_cfg_dir = os.path.join(tftp_dir, tftp_prefix, "pxelinux.cfg") # config file is named 01-MACADDR tftp_config_file_name = os.path.join( pxelinux_cfg_dir, # 01- is the ARP type 1 for ethernet; also note the PXE client # asks with the hex digits in lower case. "01-" + mac_addr.replace(":", "-").lower()) with open(tftp_config_file_name, "w") as tf: tf.write(config) # We know the file exists, so it is safe to chmod like this os.chmod(tftp_config_file_name, 0o644)
def power_on_pre_pos_setup(target): pos_mode = target.fsdb.get("pos_mode") if pos_mode == None: target.log.info("POS boot: ignoring, pos_mode property not set") return # We only care if mode is set to pxe or local -- local makes us # tell the thing to go boot local disk if pos_mode != "pxe" and pos_mode != "local": target.log.info("POS boot: ignoring, pos_mode set to %s " "(vs PXE or local)" % pos_mode) return boot_ic = target.tags.get('pos_boot_interconnect', None) if boot_ic == None: raise RuntimeError('no "pos_boot_interconnect" tag/property defined, ' 'can\'t boot off network') if not boot_ic in target.tags['interconnects']: raise RuntimeError('this target does not belong to the ' 'boot interconnect "%s" defined in tag ' '"pos_boot_interconnect"' % boot_ic) interconnect = target.tags['interconnects'][boot_ic] # FIXME: at some point, for ic-less POS-PXE boot we could get this # from pos_mac_addr and default to ic['mac_addr'] mac_addr = interconnect['mac_addr'] if pos_mode == "pxe": # now this is dirty -- we kinda hacking here but otherwise, how do # we get to the pos_* kws? ic = ttbl.config.targets[boot_ic] # The service kws = dict(target.tags) if not 'bsp' in target.tags: bsps = target.tags['bsps'].keys() kws['bsp'] = sorted(bsps)[0] kws.update(dict( ipv4_addr = interconnect['ipv4_addr'], ipv4_gateway = interconnect.get('ipv4_gateway', ""), ipv4_netmask = commonl.ipv4_len_to_netmask_ascii( interconnect['ipv4_prefix_len']), mac_addr = mac_addr, name = target.id, )) # There might be a prefix to the path to the boot kernel and # initrd; we let the target override it and default to the # network's or nothing _tag_get_from_ic_target(kws, 'pos_http_url_prefix', ic, target) _tag_get_from_ic_target(kws, 'pos_nfs_server', ic, target) _tag_get_from_ic_target(kws, 'pos_nfs_path', ic, target) # generate configuration for the target to boot the POS's linux # kernel with the root fs over NFS # FIXME: the name of the pos image and the command line extras # should go over configuration and the target's configuration # should be able to say which image it wants (defaulting everyone # to whichever). kws['extra_kopts'] = "" kws['pos_image'] = 'tcf-live' kws['root_dev'] = '/dev/nfs' # no 'single' so it force starts getty on different ports # nfsroot: note we defer to whatever we are given over DHCP kws['extra_kopts'] += \ "initrd=%(pos_http_url_prefix)sinitramfs-%(pos_image)s " \ "rd.live.image selinux=0 audit=0 ro " \ "rd.luks=0 rd.lvm=0 rd.md=0 rd.dm=0 rd.multipath=0 " \ "plymouth.enable=0 " # Generate the PXE linux configuration # # note the syslinux/pxelinux format supports no long line # breakage, so we use Python's \ for clearer, shorter lines which # will be pasted all together # # FIXME: move somewhere else more central? # # IP specification is needed so the kernel acquires an IP address # and can syslog/nfsmount, etc Note we know the fields from the # target's configuration, as they are pre-assigned # # ip=DHCP so we get always the same IP address and NFS root # info (in option root-path when writing the DHCP config file) config = """\ say TCF Network boot to Service OS #serial 0 115200 default boot prompt 0 label boot # boot to %(pos_image)s linux %(pos_http_url_prefix)svmlinuz-%(pos_image)s append console=tty0 console=%(linux_serial_console_default)s,115200 \ ip=dhcp \ root=%(root_dev)s %(extra_kopts)s """ config = template_rexpand(config, kws) else: # pos_mode is local config = """\ say TCF Network boot redirecting to local boot serial 0 115200 default localboot prompt 0 label localboot localboot 0 """ # Write the TFTP configuration -- when the target boots and does # a DHCP request, DHCP daemon will send it a DHCP address and also # tell it to boot off TFTP with a pxelinux bootloader; it'll load # that bootloader, which will look for different configuration # files off TFTP_DIR/TFTP_PREFIX/pxelinux.cfg/ in a given order, # one of us being 01-ITSMACADDR # # It will find this one we are now making and boot to the POS for # provisioning or to local boot. tftp_config_file_name = os.path.join( tftp_dir, tftp_prefix, "pxelinux.cfg", # 01- is the ARP type 1 for ethernet; also note the PXE client # asks with the hex digits in lower case. "01-" + mac_addr.replace(":", "-").lower()) with open(tftp_config_file_name, "w") as tf: tf.write(config) tf.flush() # We know the file exists, so it is safe to chmod like this os.chmod(tf.name, 0o644)
def _dhcp_conf_write(self, f): kws = dict(self._params) # generate DHCP configuration file based on hackish templating self.log.info( "%(if_name)s: IPv%(ip_mode)d addr/net/mask " "%(if_addr)s/%(if_net)s/%(if_len)s", self._params) if self.ip_mode == 4: # We only do PXE over ipv4 # FIXME: make it so using pxelinux is a configuratio template # (likewise on the tftp side, so we can switch to EFI boot or # whatever we want) # %(dhcp_architecture_types)s is the output of # _mk_pxe_arch_type_config() f.write("""\ option space pxelinux; option pxelinux.magic code 208 = string; option pxelinux.configfile code 209 = text; option pxelinux.pathprefix code 210 = text; option pxelinux.reboottime code 211 = unsigned integer 32; # To be used in the pxeclients class option architecture-type code 93 = unsigned integer 16; subnet %(if_net)s netmask %(if_netmask)s { pool { %(allow_known_clients)s range %(ip_addr_range_bottom)s %(ip_addr_range_top)s; } class "pxeclients" { match if substring (option vendor-class-identifier, 0, 9) = "PXEClient"; # http://www.syslinux.org/wiki/index.php?title=PXELINUX#UEFI %(dhcp_architecture_types)s # Point to the TFTP server, which is the same as this next-server %(if_addr)s; } } """ % self._params) else: f.write("""\ # This one line must be outside any bracketed scope option architecture-type code 93 = unsigned integer 16; subnet6 %(if_net)s/%(if_len)s { range6 %(ip_addr_range_bottom)s %(ip_addr_range_top)s; class "pxeclients" { match if substring (option vendor-class-identifier, 0, 9) = "PXEClient"; # http://www.syslinux.org/wiki/index.php?title=PXELINUX#UEFI %(dhcp_architecture_types)s # Point to the TFTP server, which is the same as this # next-server %(if_addr)s; } } """ % self._params) # Now, enumerate the targets that are in this local # configuration and figure out what's their IP address in # this network; create a hardcoded entry for them. # # FIXME: This leaves a gap, as targets in other servers could # be connected to this network. Sigh. for target_id, target in ttbl.config.targets.iteritems(): interconnects = target.tags.get('interconnects', {}) ic = self.target boot_ic = target.tags.get('pos_boot_interconnect', None) if boot_ic == None: ic.log.info('%s: target has no "pos_boot_interconnect" ' 'tag/property defined, ignoring' % target_id) continue # FIXME: these two checks shall be consistency done when # the target is being added if not boot_ic in target.tags['interconnects']: raise RuntimeError('%s: target does not belong to the ' 'boot interconnect "%s" defined in tag ' '"pos_boot_interconnect"' % (target_id, boot_ic)) if not boot_ic in ttbl.config.targets: raise RuntimeError('%s: this target\'s boot interconnect %s ' 'defined in "pos_boot_interconnect" tag ' 'is not available in this server' % (target_id, boot_ic)) if not 'bsp' in target.tags: bsps = target.tags['bsps'].keys() kws['bsp'] = sorted(bsps)[0] kws.update(dict( ipv4_gateway = ic.tags.get('ipv4_gateway', ""), ipv4_netmask = commonl.ipv4_len_to_netmask_ascii( ic.tags['ipv4_prefix_len']), name = target.id, )) # There might be a prefix to the path to the boot kernel and # initrd; we let the target override it and default to the # network's or nothing # FIXME: need v6 nfs_server and http_url _tag_get_from_ic_target(kws, 'pos_http_url_prefix', ic, target) _tag_get_from_ic_target(kws, 'pos_nfs_server', ic, target) _tag_get_from_ic_target(kws, 'pos_nfs_path', ic, target) for ic_id, interconnect in interconnects.iteritems(): if '#' in ic_id: real_ic_id, instance = ic_id.split("#", 1) kws['hostname'] = target.id + "-" + instance else: real_ic_id = ic_id kws['hostname'] = target.id if real_ic_id != self.target.id: continue kws['mac_addr'] = interconnect.get('mac_addr', None) kws['ipv4_addr'] = interconnect.get('ipv4_addr', None) kws['ipv6_addr'] = interconnect.get('ipv6_addr', None) if self.ip_mode == 4: config = """\ host %(hostname)s { hardware ethernet %(mac_addr)s; fixed-address %(ipv4_addr)s; option host-name "%(hostname)s"; # note how we are forcing NFSv3, as it might default to v2 # FIXME: parameter? # Also UDP, more resilient for our use and soft so we can # recover in some cases more easily option root-path "%(pos_nfs_server)s:%(pos_nfs_path)s,udp,soft,nfsvers=3"; } """ else: config = """\ host %(hostname)s { hardware ethernet %(mac_addr)s; fixed-address6 %(ipv6_addr)s; option host-name "%(hostname)s"; # note how we are forcing NFSv3, as it might default to v2 # FIXME: parameter? # Also UDP, more resilient for our use and soft so we can # recover in some cases more easily # FIXME: pos_nfs_server6? option root-path "%(pos_nfs_server)s:%(pos_nfs_path)s,udp,soft,nfsvers=3"; } """ f.write(template_rexpand(config, kws))
def power_on_pre_pos_setup(target): pos_mode = target.fsdb.get("pos_mode") if pos_mode == None: target.log.info("POS boot: ignoring, pos_mode property not set") return # We only care if mode is set to pxe or local -- local makes us # tell the thing to go boot local disk if pos_mode != "pxe" and pos_mode != "local": target.log.info("POS boot: ignoring, pos_mode set to %s " "(vs PXE or local)" % pos_mode) return boot_ic = target.tags.get('pos_boot_interconnect', None) if boot_ic == None: raise RuntimeError('no "pos_boot_interconnect" tag/property defined, ' 'can\'t boot off network') if not boot_ic in target.tags['interconnects']: raise RuntimeError('this target does not belong to the ' 'boot interconnect "%s" defined in tag ' '"pos_boot_interconnect"' % boot_ic) interconnect = target.tags['interconnects'][boot_ic] # FIXME: at some point, for ic-less POS-PXE boot we could get this # from pos_mac_addr and default to ic['mac_addr'] mac_addr = interconnect['mac_addr'] if pos_mode == "pxe": # now this is dirty -- we kinda hacking here but otherwise, how do # we get to the pos_* kws? ic = ttbl.config.targets[boot_ic] # The service kws = dict(target.tags) if not 'bsp' in target.tags: bsps = target.tags['bsps'].keys() kws['bsp'] = sorted(bsps)[0] kws.update( dict( ipv4_addr=interconnect['ipv4_addr'], ipv4_gateway=interconnect.get('ipv4_gateway', ""), ipv4_netmask=commonl.ipv4_len_to_netmask_ascii( interconnect['ipv4_prefix_len']), mac_addr=mac_addr, name=target.id, )) # There might be a prefix to the path to the boot kernel and # initrd; we let the target override it and default to the # network's or nothing _tag_get_from_ic_target(kws, 'pos_http_url_prefix', ic, target) _tag_get_from_ic_target(kws, 'pos_nfs_server', ic, target) _tag_get_from_ic_target(kws, 'pos_nfs_path', ic, target) # generate configuration for the target to boot the POS's linux # kernel with the root fs over NFS # FIXME: the name of the pos image and the command line extras # should go over configuration and the target's configuration # should be able to say which image it wants (defaulting everyone # to whichever). kws['extra_kopts'] = "" kws['pos_image'] = 'tcf-live' kws['root_dev'] = '/dev/nfs' # no 'single' so it force starts getty on different ports # nfsroot: note we defer to whatever we are given over DHCP kws['extra_kopts'] += \ "initrd=%(pos_http_url_prefix)sinitramfs-%(pos_image)s " \ "rd.live.image selinux=0 audit=0 ro " \ "rd.luks=0 rd.lvm=0 rd.md=0 rd.dm=0 rd.multipath=0 " \ "plymouth.enable=0 " # Generate the PXE linux configuration # # note the syslinux/pxelinux format supports no long line # breakage, so we use Python's \ for clearer, shorter lines which # will be pasted all together # # FIXME: move somewhere else more central? # # IP specification is needed so the kernel acquires an IP address # and can syslog/nfsmount, etc Note we know the fields from the # target's configuration, as they are pre-assigned # # ip=DHCP so we get always the same IP address and NFS root # info (in option root-path when writing the DHCP config file) config = """\ say TCF Network boot to Service OS #serial 0 115200 default boot prompt 0 label boot # boot to %(pos_image)s linux %(pos_http_url_prefix)svmlinuz-%(pos_image)s append console=tty0 console=%(linux_serial_console_default)s,115200 \ ip=dhcp \ root=%(root_dev)s %(extra_kopts)s """ config = template_rexpand(config, kws) else: # pos_mode is local config = """\ say TCF Network boot redirecting to local boot serial 0 115200 default localboot prompt 0 label localboot localboot 0 """ # Write the TFTP configuration -- when the target boots and does # a DHCP request, DHCP daemon will send it a DHCP address and also # tell it to boot off TFTP with a pxelinux bootloader; it'll load # that bootloader, which will look for different configuration # files off TFTP_DIR/TFTP_PREFIX/pxelinux.cfg/ in a given order, # one of us being 01-ITSMACADDR # # It will find this one we are now making and boot to the POS for # provisioning or to local boot. tftp_config_file_name = os.path.join( tftp_dir, tftp_prefix, "pxelinux.cfg", # 01- is the ARP type 1 for ethernet; also note the PXE client # asks with the hex digits in lower case. "01-" + mac_addr.replace(":", "-").lower()) with open(tftp_config_file_name, "w") as tf: tf.write(config) tf.flush() # We know the file exists, so it is safe to chmod like this os.chmod(tf.name, 0o644)
def eval(self, target): if self.sanboot_url == None: SANBOOT_URL = os.environ.get("SANBOOT_URL", None) if SANBOOT_URL == None: raise tcfl.tc.blocked_e("No default sanboot_url programmed or" " SANBOOT_URL environment given") self.sanboot_url = SANBOOT_URL target.power.cycle() boot_ic = target.kws['pos_boot_interconnect'] mac_addr = target.kws['interconnects'][boot_ic]['mac_addr'] tcfl.biosl.boot_network_pxe( target, # Eg: UEFI PXEv4 (MAC:4AB0155F98A1) r"UEFI PXEv4 \(MAC:%s\)" % mac_addr.replace(":", "").upper().strip()) # can't wait also for the "ok" -- debugging info might pop in th emiddle target.expect("iPXE initialising devices...") # if the connection is slow, we have to start sending Ctrl-B's # ASAP #target.expect(re.compile("iPXE .* -- Open Source Network Boot Firmware")) # send Ctrl-B to go to the PXE shell, to get manual control of iPXE # # do this as soon as we see the boot message from iPXE because # otherwise by the time we see the other message, it might already # be trying to boot pre-programmed instructions--we'll see the # Ctrl-B message anyway, so we expect for it. # # before sending these "Ctrl-B" keystrokes in ANSI, but we've seen # sometimes the timing window being too tight, so we just blast # the escape sequence to the console. target.console.write("\x02\x02") # use this iface so expecter time.sleep(0.3) target.console.write("\x02\x02") # use this iface so expecter time.sleep(0.3) target.console.write("\x02\x02") # use this iface so expecter time.sleep(0.3) target.expect("Ctrl-B", timeout=250) target.console.write("\x02\x02") # use this iface so expecter time.sleep(0.3) target.console.write("\x02\x02") # use this iface so expecter time.sleep(0.3) target.expect("iPXE>") prompt_orig = target.shell.prompt_regex try: # # When matching end of line, match against \r, since depends # on the console it will send one or two \r (SoL vs SSH-SoL) # before \n -- we removed that in the kernel driver by using # crnl in the socat config # # FIXME: block on anything here? consider infra issues # on "Connection timed out", http://ipxe.org... target.shell.prompt_regex = "iPXE>" kws = dict(target.kws) boot_ic = target.kws['pos_boot_interconnect'] mac_addr = target.kws['interconnects'][boot_ic]['mac_addr'] ipv4_addr = target.kws['interconnects'][boot_ic]['ipv4_addr'] ipv4_prefix_len = target.kws['interconnects'][boot_ic][ 'ipv4_prefix_len'] kws['ipv4_netmask'] = commonl.ipv4_len_to_netmask_ascii( ipv4_prefix_len) # Find what network interface our MAC address is; the # output of ifstat looks like: # ## net0: 00:26:55:dd:4a:9d using 82571eb on 0000:6d:00.0 (open) ## [Link:up, TX:8 TXE:1 RX:44218 RXE:44205] ## [TXE: 1 x "Network unreachable (http://ipxe.org/28086090)"] ## [RXE: 43137 x "Operation not supported (http://ipxe.org/3c086083)"] ## [RXE: 341 x "The socket is not connected (http://ipxe.org/380f6093)"] ## [RXE: 18 x "Invalid argument (http://ipxe.org/1c056082)"] ## [RXE: 709 x "Error 0x2a654089 (http://ipxe.org/2a654089)"] ## net1: 00:26:55:dd:4a:9c using 82571eb on 0000:6d:00.1 (open) ## [Link:down, TX:0 TXE:0 RX:0 RXE:0] ## [Link status: Down (http://ipxe.org/38086193)] ## net2: 00:26:55:dd:4a:9f using 82571eb on 0000:6e:00.0 (open) ## [Link:down, TX:0 TXE:0 RX:0 RXE:0] ## [Link status: Down (http://ipxe.org/38086193)] ## net3: 00:26:55:dd:4a:9e using 82571eb on 0000:6e:00.1 (open) ## [Link:down, TX:0 TXE:0 RX:0 RXE:0] ## [Link status: Down (http://ipxe.org/38086193)] ## net4: 98:4f:ee:00:05:04 using NII on NII-0000:01:00.0 (open) ## [Link:up, TX:10 TXE:0 RX:8894 RXE:8441] ## [RXE: 8173 x "Operation not supported (http://ipxe.org/3c086083)"] ## [RXE: 268 x "The socket is not connected (http://ipxe.org/380f6093)"] # # thus we need to match the one that fits our mac address ifstat = target.shell.run("ifstat", output=True, trim=True) regex = re.compile( "(?P<ifname>net[0-9]+): %s using" % mac_addr.lower(), re.MULTILINE) m = regex.search(ifstat) if not m: raise tcfl.tc.error_e( "iPXE: cannot find interface name for MAC address %s;" " is the MAC address in the configuration correct?" % mac_addr.lower(), dict(target=target, ifstat=ifstat, mac_addr=mac_addr.lower())) ifname = m.groupdict()['ifname'] dhcp = bool(target.property_get("ipxe.dhcp", True)) if dhcp: target.shell.run("dhcp " + ifname, re.compile("Configuring.*ok")) target.shell.run("show %s/ip" % ifname, "ipv4 = %s" % ipv4_addr) else: # static is much faster and we know the IP address already # anyway; but then we don't have DNS as it is way more # complicated to get it target.shell.run("set %s/ip %s" % (ifname, ipv4_addr)) target.shell.run("set %s/netmask %s" % (ifname, kws['ipv4_netmask'])) target.shell.run("ifopen " + ifname) if self.sanboot_url == "skip": target.report_info("not booting", level=0) else: target.send("sanboot %s" % self.sanboot_url) # can't use shell.run...it will timeout, since we'll print no more prompt # ESXi would print now... #target.expect("Booting from SAN device") finally: target.shell.prompt_regex = prompt_orig