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