From 997b43045ae6f74e5f5df98b9f6cc56b3088e957 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Fri, 14 Aug 2020 15:29:55 -0400 Subject: [PATCH] Make preparations for a configbmc for inband config --- .../common/opt/confluent/bin/apiclient | 18 +- .../confluent/config/attributes.py | 12 + .../confluent/discovery/handlers/xcc.py | 2 +- confluent_server/confluent/selfservice.py | 24 +- misc/configbmc.py | 247 ++++++++++++++++++ 5 files changed, 295 insertions(+), 8 deletions(-) create mode 100644 misc/configbmc.py diff --git a/confluent_osdeploy/common/opt/confluent/bin/apiclient b/confluent_osdeploy/common/opt/confluent/bin/apiclient index 5eeceabf..b6cc3f6c 100644 --- a/confluent_osdeploy/common/opt/confluent/bin/apiclient +++ b/confluent_osdeploy/common/opt/confluent/bin/apiclient @@ -22,7 +22,7 @@ def get_apikey(nodename, mgr): return apikey class HTTPSClient(client.HTTPConnection, object): - def __init__(self, port=443): + def __init__(self, json=False, port=443): self.stdheaders = {} info = open('/etc/confluent/confluent.info').read().split('\n') host = None @@ -45,6 +45,8 @@ class HTTPSClient(client.HTTPConnection, object): ifidx = host.split('%', 1)[1] with open('/tmp/confluent.ifidx', 'w+') as ifout: ifout.write(ifidx) + if json: + self.stdheaders['ACCEPT'] = 'application/json' self.stdheaders['CONFLUENT_APIKEY'] = get_apikey(node, host) if mgtiface: self.stdheaders['CONFLUENT_MGTIFACE'] = mgtiface @@ -85,14 +87,18 @@ class HTTPSClient(client.HTTPConnection, object): if __name__ == '__main__': data = None - if len(sys.argv) == 4: + json = False + if '-j' in sys.argv: + json = True + if sys.argv[-2] == '-o': with open(sys.argv[3], 'wb') as outf: - reader = HTTPSClient().grab_url(sys.argv[1], data, returnrsp=True) + reader = HTTPSClient(json=json).grab_url( + sys.argv[1], data, returnrsp=True) chunk = reader.read(16384) while chunk: outf.write(chunk) chunk = reader.read(16384) sys.exit(0) - if len(sys.argv) == 3: - data = open(sys.argv[2]).read() - print(HTTPSClient().grab_url(sys.argv[1], data).decode()) + if os.path.exists(sys.argv[-1]): + data = open(sys.argv[-1]).read() + print(HTTPSClient(json=json).grab_url(sys.argv[1], data).decode()) diff --git a/confluent_server/confluent/config/attributes.py b/confluent_server/confluent/config/attributes.py index 28ff23c1..5cbcb28e 100644 --- a/confluent_server/confluent/config/attributes.py +++ b/confluent_server/confluent/config/attributes.py @@ -360,6 +360,18 @@ node = { 'control, get sensor data, get inventory, and so on. ' 'ipmi is used if not specified.' }, + 'hardwaremanagement.port': { + 'description': 'The port the BMC should be configured to connect to + 'network. This only has effect during deployment and ' + 'does not apply to out of band discovery. Example values ' + 'include "ocp", "ml2", "lom" (for on board port ' + 'shared with operating system), or "dedicated"', + }, + 'hardwaremanagement.vlan': { + 'description': 'The vlan that a BMC should be configured to tag ' + 'traffic. This only has effect during OS deployment ' + 'and does not apply to out of band discovery.', + }, 'enclosure.bay': { 'description': 'The bay in the enclosure, if any', # 'appliesto': ['system'], diff --git a/confluent_server/confluent/discovery/handlers/xcc.py b/confluent_server/confluent/discovery/handlers/xcc.py index 41dd61ac..f388d652 100644 --- a/confluent_server/confluent/discovery/handlers/xcc.py +++ b/confluent_server/confluent/discovery/handlers/xcc.py @@ -96,7 +96,7 @@ class NodeHandler(immhandler.NodeHandler): ipmicmd.xraw_command(netfn=0x3a, command=0xf1, data=(1,)) except pygexc.IpmiException as e: if (e.ipmicode != 193 and 'Unauthorized name' not in str(e) and - 'Incorrect password' not in str(e) and + 'Incorrect password' not in str(e) and str(e) != 'Session no longer connected'): # raise an issue if anything other than to be expected if disableipmi: diff --git a/confluent_server/confluent/selfservice.py b/confluent_server/confluent/selfservice.py index 03bc77f7..f81138e8 100644 --- a/confluent_server/confluent/selfservice.py +++ b/confluent_server/confluent/selfservice.py @@ -3,6 +3,7 @@ import confluent.collective.manager as collective import confluent.netutil as netutil import confluent.sshutil as sshutil import confluent.util as util +import eventlet.green.socket as socket import eventlet.green.subprocess as subprocess import crypt import json @@ -75,7 +76,28 @@ def handle_request(env, start_response): return if env['REQUEST_METHOD'] not in ('HEAD', 'GET') and 'CONTENT_LENGTH' in env and int(env['CONTENT_LENGTH']) > 0: reqbody = env['wsgi.input'].read(int(env['CONTENT_LENGTH'])) - if env['PATH_INFO'] == '/self/deploycfg': + if env['PATH_INFO'] == '/self/bmcconfig': + hmattr = cfg.get_node_attributes(nodename, 'hardwaremanagement.*') + res = {} + port = hmattr.get('hardwaremanagement.port', {}).get('value', None) + if port is not None: + res['bmcport'] = port + vlan = hmattr.get('hardwaremanagement.vlan', {}).get('value', None) + if vlan is not None: + res['bmcvlan'] = vlan + bmcaddr = hmattr.get('hardwaremanagement.manager', {}).get('value', + None) + bmcaddr = socket.getaddrinfo(bmcaddr, 0)[0] + bmcaddr = bmcaddr[-1][0] + if '.' in bmcaddr: # ipv4 is allowed + netconfig = netutil.get_nic_config(cfg, nodename, ip=bmcaddr) + res['bmcipv4'] = bmcaddr + res['prefixv4'] = netconfig['prefix'] + res['bmcgw'] = netconfig.get('ipv4_gateway', None) + # credential security results in user/password having to be deferred + start_response('200 OK', (('Content-Type', retype),)) + yield dumper(res) + elif env['PATH_INFO'] == '/self/deploycfg': if 'HTTP_CONFLUENT_MGTIFACE' in env: ncfg = netutil.get_nic_config(cfg, nodename, ifidx=env['HTTP_CONFLUENT_MGTIFACE']) else: diff --git a/misc/configbmc.py b/misc/configbmc.py new file mode 100644 index 00000000..f35fe75b --- /dev/null +++ b/misc/configbmc.py @@ -0,0 +1,247 @@ +# Copyright 2017 Lenovo +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse +import ctypes +import fcntl +from select import select +import struct +import sys +import time + +class IpmiMsg(ctypes.Structure): + _fields_ = [('netfn', ctypes.c_ubyte), + ('cmd', ctypes.c_ubyte), + ('data_len', ctypes.c_short), + ('data', ctypes.POINTER(ctypes.c_ubyte))] + + +class IpmiSystemInterfaceAddr(ctypes.Structure): + _fields_ = [('addr_type', ctypes.c_int), + ('channel', ctypes.c_short), + ('lun', ctypes.c_ubyte)] + + +class IpmiRecv(ctypes.Structure): + _fields_ = [('recv_type', ctypes.c_int), + ('addr', ctypes.POINTER(IpmiSystemInterfaceAddr)), + ('addr_len', ctypes.c_uint), + ('msgid', ctypes.c_long), + ('msg', IpmiMsg)] + + +class IpmiReq(ctypes.Structure): + _fields_ = [('addr', ctypes.POINTER(IpmiSystemInterfaceAddr)), + ('addr_len', ctypes.c_uint), + ('msgid', ctypes.c_long), + ('msg', IpmiMsg)] + + +_IONONE = 0 +_IOWRITE = 1 +_IOREAD = 2 +IPMICTL_SET_MY_ADDRESS_CMD = ( + _IOREAD << 30 | ctypes.sizeof(ctypes.c_uint) << 16 + | ord('i') << 8 | 17) # from ipmi.h +IPMICTL_SEND_COMMAND = ( + _IOREAD << 30 | ctypes.sizeof(IpmiReq) << 16 + | ord('i') << 8 | 13) # from ipmi.h +# next is really IPMICTL_RECEIVE_MSG_TRUNC, but will only use that +IPMICTL_RECV = ( + (_IOWRITE | _IOREAD) << 30 | ctypes.sizeof(IpmiRecv) << 16 + | ord('i') << 8 | 11) # from ipmi.h +BMC_SLAVE_ADDR = ctypes.c_uint(0x20) +CURRCHAN = 0xf +ADDRTYPE = 0xc + + +class Session(object): + def __init__(self, devnode='/dev/ipmi0'): + """Create a local session inband + + :param: devnode: The path to the ipmi device + """ + self.ipmidev = open(devnode, 'r+') + fcntl.ioctl(self.ipmidev, IPMICTL_SET_MY_ADDRESS_CMD, BMC_SLAVE_ADDR) + # the interface is initted, create some reusable memory for our session + self.databuffer = ctypes.create_string_buffer(4096) + self.req = IpmiReq() + self.rsp = IpmiRecv() + self.addr = IpmiSystemInterfaceAddr() + self.req.msg.data = ctypes.cast( + ctypes.addressof(self.databuffer), + ctypes.POINTER(ctypes.c_ubyte)) + self.rsp.msg.data = self.req.msg.data + self.userid = None + self.password = None + + def await_reply(self): + rd, _, _ = select((self.ipmidev,), (), (), 1) + while not rd: + rd, _, _ = select((self.ipmidev,), (), (), 1) + + def pause(self, seconds): + time.sleep(seconds) + + @property + def parsed_rsp(self): + response = {'netfn': self.rsp.msg.netfn, 'command': self.rsp.msg.cmd, + 'code': bytearray(self.databuffer.raw)[0], + 'data': bytearray( + self.databuffer.raw[1:self.rsp.msg.data_len])} + return response + + def raw_command(self, + netfn, + command, + data=(), + bridge_request=None, + retry=True, + delay_xmit=None, + timeout=None, + waitall=False, rslun=0): + self.addr.channel = CURRCHAN + self.addr.addr_type = ADDRTYPE + self.addr.lun = rslun + self.req.addr_len = ctypes.sizeof(IpmiSystemInterfaceAddr) + self.req.addr = ctypes.pointer(self.addr) + self.req.msg.netfn = netfn + self.req.msg.cmd = command + if data: + data = memoryview(bytearray(data)) + try: + self.databuffer[:len(data)] = data[:len(data)] + except ValueError: + self.databuffer[:len(data)] = data[:len(data)].tobytes() + self.req.msg.data_len = len(data) + fcntl.ioctl(self.ipmidev, IPMICTL_SEND_COMMAND, self.req) + self.await_reply() + self.rsp.msg.data_len = 4096 + self.rsp.addr = ctypes.pointer(self.addr) + self.rsp.addr_len = ctypes.sizeof(IpmiSystemInterfaceAddr) + fcntl.ioctl(self.ipmidev, IPMICTL_RECV, self.rsp) + return self.parsed_rsp + + +def _is_tsm(model): + return model in ('7y00', '7z01', '7y98', '7y99') + +def set_port(s, port, vendor, model): + oport = port + if vendor not in ('IBM', 'Lenovo'): + raise Exception('{0} not implemented'.format(vendor)) + if _is_tsm(model): + set_port_tsm(s, port, model) + else: + set_port_xcc(s, port, model) + + +def set_port_tsm(s, port, model): + oport = port + sys.stdout.write('Setting TSM port to "{}"...'.format(oport)) + sys.stdout.flush() + if port == 'ocp': + s.raw_command(0x32, 0x71, b'\x00\x01\x00') + elif port == 'dedicated': + s.raw_command(0x32, 0x71, b'\x00\x00\x00') + timer = 15 + while timer: + time.sleep(1.0) + sys.stdout.write('.') + sys.stdout.flush() + if port == 'ocp': + s.raw_command(0x32, 0x71, b'\x00\x00\x03') + elif port == 'dedicated': + s.raw_command(0x32, 0x71, b'\x00\x01\x03') + print('Complete') + + +def set_port_xcc(s, port, model): + oport = port + if port.lower() == 'dedicated': + port = b'\x01' + elif port.lower() in ('ml2', 'ocp'): + port = b'\x02\x00' + elif port.lower() == 'lom': + if model == '7x58': + port = b'\x00\x02' + else: + port = b'\x00\x00' + else: + port = port.split(' ') + port = bytes(bytearray([int(x) for x in port])) + currport = bytes(s.raw_command(0xc, 2, b'\x01\xc0\x00\x00')['data'][1:]) + if port == currport: + sys.stdout.write('XCC port already set to "{}"\n'.format(oport)) + return + sys.stdout.write('Setting XCC port to "{}"...'.format(oport)) + sys.stdout.flush() + s.raw_command(0xc, 1, b'\x01\xc0' + port) + tries = 60 + while currport != port and tries: + tries -= 1 + time.sleep(0.5) + sys.stdout.write('.') + sys.stdout.flush() + currport = bytes(s.raw_command(0xc, 2, b'\x01\xc0\x00\x00')['data'][1:]) + if not tries: + raise Exception('Timeout attempting to set port') + sys.stdout.write('Complete\n') + + +def set_vlan(s, vlan): + ovlan = vlan + if vlan == 'off': + vlan = b'\x00\x00' + else: + vlan = int(vlan) + if vlan: + vlan = vlan | 32768 + vlan = struct.pack('