From a251a538b0708de4bc9c69f626148ce687463ca8 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Fri, 26 Jul 2019 09:25:19 -0400 Subject: [PATCH] Improve SMM discovery SMM discovery behavior has seemingly gotten more picky with time. First switch to an IPMI-free if the user has custom password. The web based approach is much less problematic than SMM IPMI stack in this context. If user specifies they want to use default credentials, we have no choice but to use IPMI. Omit things and shuffle order of operations to mitigate problems. It isn't perfect, but it does work eventually. --- .../confluent/discovery/handlers/bmc.py | 109 ++++++++------- .../confluent/discovery/handlers/smm.py | 127 ++++++++++++++++-- 2 files changed, 179 insertions(+), 57 deletions(-) diff --git a/confluent_server/confluent/discovery/handlers/bmc.py b/confluent_server/confluent/discovery/handlers/bmc.py index dc783f8b..4985f795 100644 --- a/confluent_server/confluent/discovery/handlers/bmc.py +++ b/confluent_server/confluent/discovery/handlers/bmc.py @@ -56,27 +56,27 @@ class NodeHandler(generic.NodeHandler): def config(self, nodename, reset=False): self._bmcconfig(nodename, reset) - def _bmcconfig(self, nodename, reset=False, customconfig=None): + def _bmcconfig(self, nodename, reset=False, customconfig=None, vc=None): # TODO(jjohnson2): set ip parameters, user/pass, alert cfg maybe # In general, try to use https automation, to make it consistent # between hypothetical secure path and today. + creds = self.configmanager.get_node_attributes( + nodename, + ['secret.hardwaremanagementuser', + 'secret.hardwaremanagementpassword'], decrypt=True) + user = creds.get(nodename, {}).get( + 'secret.hardwaremanagementuser', {}).get('value', None) + passwd = creds.get(nodename, {}).get( + 'secret.hardwaremanagementpassword', {}).get('value', None) try: ic = self._get_ipmicmd() passwd = DEFAULT_PASS except pygexc.IpmiException as pi: - creds = self.configmanager.get_node_attributes( - nodename, - ['secret.hardwaremanagementuser', - 'secret.hardwaremanagementpassword'], decrypt=True) - user = creds.get(nodename, {}).get( - 'secret.hardwaremanagementuser', {}).get('value', None) havecustomcreds = False if user is not None and user != DEFAULT_USER: havecustomcreds = True else: user = DEFAULT_USER - passwd = creds.get(nodename, {}).get( - 'secret.hardwaremanagementpassword', {}).get('value', None) if passwd is not None and passwd != DEFAULT_PASS: havecustomcreds = True else: @@ -85,8 +85,8 @@ class NodeHandler(generic.NodeHandler): ic = self._get_ipmicmd(user, passwd) else: raise - if customconfig: - customconfig(ic) + if vc: + ic.register_key_handler(vc) currusers = ic.get_users() lanchan = ic.get_network_channel() userdata = ic.xraw_command(netfn=6, command=0x44, data=(lanchan, @@ -106,6 +106,55 @@ class NodeHandler(generic.NodeHandler): raise exc.TargetEndpointBadCredentials( 'secret.hardwaremanagementuser and/or ' 'secret.hardwaremanagementpassword was not configured') + newuser = cd['secret.hardwaremanagementuser']['value'] + newpass = cd['secret.hardwaremanagementpassword']['value'] + for uid in currusers: + if currusers[uid]['name'] == newuser: + # Use existing account that has been created + newuserslot = uid + if newpass != passwd: # don't mess with existing if no change + ic.set_user_password(newuserslot, password=newpass) + ic = self._get_ipmicmd(user, passwd) + if vc: + ic.register_key_handler(vc) + break + else: + newuserslot = lockedusers + 1 + if newuserslot < 2: + newuserslot = 2 + if newpass != passwd: # don't mess with existing if no change + ic.set_user_password(newuserslot, password=newpass) + ic.set_user_name(newuserslot, newuser) + if havecustomcreds: + ic = self._get_ipmicmd(user, passwd) + if vc: + ic.register_key_handler(vc) + #We are remote operating on the account we are + #using, no need to try to set user access + #ic.set_user_access(newuserslot, lanchan, + # privilege_level='administrator') + # Now to zap others + for uid in currusers: + if uid != newuserslot: + if uid <= lockedusers: # we cannot delete, settle for disable + ic.disable_user(uid, 'disable') + else: + # lead with the most critical thing, removing user access + ic.set_user_access(uid, channel=None, callback=False, + link_auth=False, ipmi_msg=False, + privilege_level='no_access') + # next, try to disable the password + ic.set_user_password(uid, mode='disable', password=None) + # ok, now we can be less paranoid + try: + ic.user_delete(uid) + except pygexc.IpmiException as ie: + if ie.ipmicode != 0xd5: # some response to the 0xff + # name... + # the user will remain, but that is life + raise + if customconfig: + customconfig(ic) if ('hardwaremanagement.manager' in cd and cd['hardwaremanagement.manager']['value'] and not cd['hardwaremanagement.manager']['value'].startswith( @@ -134,44 +183,6 @@ class NodeHandler(generic.NodeHandler): else: raise exc.TargetEndpointUnreachable( 'hardwaremanagement.manager must be set to desired address') - newuser = cd['secret.hardwaremanagementuser']['value'] - newpass = cd['secret.hardwaremanagementpassword']['value'] - for uid in currusers: - if currusers[uid]['name'] == newuser: - # Use existing account that has been created - newuserslot = uid - if newpass != passwd: # don't mess with existing if no change - ic.set_user_password(newuserslot, password=newpass) - break - else: - newuserslot = lockedusers + 1 - if newuserslot < 2: - newuserslot = 2 - if newpass != passwd: # don't mess with existing if no change - ic.set_user_password(newuserslot, password=newpass) - ic.set_user_name(newuserslot, newuser) - ic.set_user_access(newuserslot, lanchan, - privilege_level='administrator') - # Now to zap others - for uid in currusers: - if uid != newuserslot: - if uid <= lockedusers: # we cannot delete, settle for disable - ic.disable_user(uid, 'disable') - else: - # lead with the most critical thing, removing user access - ic.set_user_access(uid, channel=None, callback=False, - link_auth=False, ipmi_msg=False, - privilege_level='no_access') - # next, try to disable the password - ic.set_user_password(uid, mode='disable', password=None) - # ok, now we can be less paranoid - try: - ic.user_delete(uid) - except pygexc.IpmiException as ie: - if ie.ipmicode != 0xd5: # some response to the 0xff - # name... - # the user will remain, but that is life - raise if reset: ic.reset_bmc() return ic diff --git a/confluent_server/confluent/discovery/handlers/smm.py b/confluent_server/confluent/discovery/handlers/smm.py index 6b426aab..82149588 100644 --- a/confluent_server/confluent/discovery/handlers/smm.py +++ b/confluent_server/confluent/discovery/handlers/smm.py @@ -13,7 +13,15 @@ # limitations under the License. import confluent.discovery.handlers.bmc as bmchandler +import confluent.exceptions as exc +import pyghmi.util.webclient as webclient import struct +import urllib +import eventlet.support.greendns +import confluent.netutil as netutil +getaddrinfo = eventlet.support.greendns.getaddrinfo + +from xml.etree.ElementTree import fromstring def fixuuid(baduuid): # SMM dumps it out in hex @@ -26,7 +34,7 @@ def fixuuid(baduuid): class NodeHandler(bmchandler.NodeHandler): is_enclosure = True devname = 'SMM' - maxmacs = 5 # support an enclosure, but try to avoid catching daisy chain + maxmacs = 6 # support an enclosure, but try to avoid catching daisy chain def scan(self): # the UUID is in a weird order, fix it up to match @@ -44,7 +52,7 @@ class NodeHandler(bmchandler.NodeHandler): self._fp = certificate return certificate == self._fp - def set_password_policy(self, ic): + def _webconfigrules(self, wc): rules = [] for rule in self.ruleset.split(','): if '=' not in rule: @@ -62,10 +70,95 @@ class NodeHandler(bmchandler.NodeHandler): rules.append('passwordReuseCheckNum:' + value) if rules: apirequest = 'set={0}'.format(','.join(rules)) - ic.register_key_handler(self._validate_cert) - ic.oem_init() - ic._oem.smmhandler.wc.request('POST', '/data', apirequest) - ic._oem.smmhandler.wc.getresponse().read() + wc.request('POST', '/data', apirequest) + wc.getresponse().read() + + def _webconfignet(self, wc, nodename): + cfg = self.configmanager + cd = cfg.get_node_attributes( + nodename, ['hardwaremanagement.manager']) + smmip = cd.get(nodename, {}).get('hardwaremanagement.manager', {}).get('value', None) + if smmip and ':' not in smmip: + smmip = getaddrinfo(smmip, 0)[0] + smmip = smmip[-1][0] + if ':' in smmip: + raise exc.NotImplementedException('IPv6 not supported') + netconfig = netutil.get_nic_config(cfg, nodename, ip=smmip) + netmask = netutil.cidr_to_mask(netconfig['prefix']) + setdata = 'set=ifIndex:0,v4DHCPEnabled:0,v4IPAddr:{0},v4NetMask:{1}'.format(smmip, netmask) + gateway = netconfig.get('ipv4_gateway', None) + if gateway: + setdata += ',v4Gateway:{0}'.format(gateway) + wc.request('POST', '/data', setdata) + rsp = wc.getresponse() + rspdata = rsp.read() + if '0' not in rspdata: + raise Exception("Error configuring SMM Network") + return + if ':' in smmip and not smmip.startswith('fe80::'): + raise exc.NotImplementedException('IPv6 configuration TODO') + if self.ipaddr.startswith('fe80::'): + cfg.set_node_attributes( + {nodename: {'hardwaremanagement.manager': self.ipaddr}}) + + def _webconfigcreds(self, username, password): + wc = webclient.SecureHTTPConnection(self.ipaddr, 443, verifycallback=self._validate_cert) + wc.connect() + authdata = { # start by trying factory defaults + 'user': 'USERID', + 'password': 'PASSW0RD', + } + headers = {'Connection': 'keep-alive', 'Content-Type': 'application/x-www-form-urlencoded'} + wc.request('POST', '/data/login', urllib.urlencode(authdata), headers) + rsp = wc.getresponse() + rspdata = rsp.read() + if 'authResult>0' not in rspdata: + # default credentials are refused, try with the actual + authdata['user'] = username + authdata['password'] = password + wc.request('POST', '/data/login', urllib.urlencode(authdata), headers) + rsp = wc.getresponse() + rspdata = rsp.read() + if 'renew_account' in rspdata: + raise Exception('Configured password has expired') + if 'authResult>0' not in rspdata: + raise Exception('Unknown username/password on SMM') + tokens = fromstring(rspdata) + st2 = tokens.findall('st2')[0].text + wc.set_header('ST2', st2) + return wc + if 'renew_account' in rspdata: + passwdchange = {'oripwd': 'PASSW0RD', 'newpwd': password} + tokens = fromstring(rspdata) + st2 = tokens.findall('st2')[0].text + wc.set_header('ST2', st2) + wc.request('POST', '/data/changepwd', urllib.urlencode(passwdchange)) + rsp = wc.getresponse() + rspdata = rsp.read() + authdata['password'] = password + wc.request('POST', '/data/login', urllib.urlencode(authdata), headers) + rsp = wc.getresponse() + rspdata = rsp.read() + if 'authResult>0' in rspdata: + tokens = fromstring(rspdata) + st2 = tokens.findall('st2')[0].text + wc.set_header('ST2', st2) + if username == 'USERID': + return wc + wc.request('POST', '/data', 'set=user(2,1,{0},511,,4,15,0)'.format(username)) + rsp = wc.getresponse() + rspdata = rsp.read() + wc.request('POST', '/data/logout') + rsp = wc.getresponse() + rspdata = rsp.read() + authdata['user'] = username + wc.request('POST', '/data/login', urllib.urlencode(authdata, headers)) + rsp = wc.getresponse() + rspdata = rsp.read() + tokens = fromstring(rspdata) + st2 = tokens.findall('st2')[0].text + wc.set_header('ST2', st2) + return wc def config(self, nodename): # SMM for now has to reset to assure configuration applies @@ -73,7 +166,25 @@ class NodeHandler(bmchandler.NodeHandler): nodename, 'discovery.passwordrules') self.ruleset = dpp.get(nodename, {}).get( 'discovery.passwordrules', {}).get('value', '') - ic = self._bmcconfig(nodename, customconfig=self.set_password_policy) + creds = self.configmanager.get_node_attributes( + nodename, + ['secret.hardwaremanagementuser', + 'secret.hardwaremanagementpassword'], decrypt=True) + username = creds.get(nodename, {}).get( + 'secret.hardwaremanagementuser', {}).get('value', 'USERID') + passwd = creds.get(nodename, {}).get( + 'secret.hardwaremanagementpassword', {}).get('value', 'PASSW0RD') + if passwd == 'PASSW0RD' and self.ruleset: + raise Exception('Cannot support default password and setting password rules at same time') + if passwd == 'PASSW0RD': + # We must avoid hitting the web interface due to forced password change, best effert + self._bmcconfig(nodename) + else: + # Switch to full web based configuration, to mitigate risks with the SMM + wc = self._webconfigcreds(username, passwd) + self._webconfigrules(wc) + self._webconfignet(wc, nodename) + # notes for smm: # POST to: @@ -88,4 +199,4 @@ class NodeHandler(bmchandler.NodeHandler): # with body user=USERID&password=Passw0rd!4321 # yields: # ok 0 index.html -# note forwardUrl, if password change needed, will indicate something else \ No newline at end of file +# note forwardUrl, if password change needed, will indicate something else