2
0
mirror of https://github.com/xcat2/confluent.git synced 2026-09-21 08:33:23 +00:00

Add nvos onboarding

This commit is contained in:
Jarrod Johnson
2026-09-18 16:37:35 -04:00
parent 043ede7464
commit 58e9a11f4c
4 changed files with 77 additions and 5 deletions
+2 -1
View File
@@ -79,6 +79,7 @@ import confluent.discovery.handlers.xcc as xcc
import confluent.discovery.handlers.xcc3 as xcc3
import confluent.discovery.handlers.smm3 as smm3
import confluent.discovery.handlers.megarac as megarac
import confluent.discovery.handlers.nvos as nvos
import confluent.discovery.handlers.eureka as eureka
import confluent.exceptions as exc
import confluent.log as log
@@ -134,7 +135,7 @@ nodehandlers = {
'generic-redfish': None,
'generic-https': None,
'generic-ssh': None,
'nvos-switch': None,
'nvos-switch': nvos,
#'openbmc': None,
'service:io-device.Lenovo:management-module': None,
'service:thinkagile-storage': cpstorage,
@@ -0,0 +1,53 @@
import asyncssh
import confluent.sshclient as sshclient
import confluent.discovery.handlers.generic as generic
import confluent.netutil as netutil
class NodeHandler(generic.NodeHandler):
devname = 'NVOS Switch'
def get_firmware_default_account_info(self):
return ('admin', 'admin')
async def config(self, nodename):
ncreds = self.configmanager.get_node_attributes(nodename, ['secret.adminuser', 'secret.adminpassword'], decrypt=True)
ncreds = ncreds.get(nodename, {})
adminuser = ncreds.get('secret.adminuser', {}).get('value', 'admin')
if isinstance(adminuser, bytes):
adminuser = adminuser.decode('utf-8')
if adminuser != 'admin':
raise ValueError("nvos support does not support renaming the admin user")
adminpass = ncreds.get('secret.adminpassword', {}).get('value')
if isinstance(adminpass, bytes):
adminpass = adminpass.decode('utf-8')
if not adminpass:
raise ValueError("secret.adminpassword must be specified for " + nodename)
myaddress = self.info.get('addresses', [[None]])[0][0]
if not myaddress:
raise ValueError("No address found for request")
hwaddr = self.info.get('hwaddr')
myaddress = self.info.get('addresses', [[None]])[0][0]
if myaddress:
attrset = {'deployment.client_ip': myaddress}
if hwaddr:
myip = await netutil.my_ip_facing(myaddress)
if myip:
niccfg = await netutil.get_nic_config(self.configmanager, nodename, serverip=myip)
cfgname = niccfg.get('config_name', None)
if cfgname:
attrname = f'net.{cfgname}.hwaddr'
else:
attrname = 'net.hwaddr'
attrset[attrname] = hwaddr
await self.configmanager.set_node_attributes({nodename: attrset})
try:
async with sshclient.connect(myaddress, username=adminuser, password=adminpass, nodename=nodename, configmanager=self.configmanager) as conn:
pass
except asyncssh.misc.PermissionDenied: # try admin, admin
async with sshclient.connect(myaddress, username='admin', password='admin', nodename=nodename, configmanager=self.configmanager) as conn:
res = await conn.run(f"nv set system aaa user admin password '{adminpass}'")
if res.exit_status == 0:
res = await conn.run(f'nv config apply')
if res.exit_status != 0:
raise RuntimeError("Failed to apply NVOS configuration")
+13
View File
@@ -140,6 +140,17 @@ def mac2lla(mac):
lla = f'fe80::{first_half[0:4]}:{first_half[4:]}ff:fe{second_half[0:2]}:{second_half[2:6]}'
return lla
async def my_ip_facing(target):
addrinfo = await asyncio.get_running_loop().getaddrinfo(
target, 0, type=socket.SOCK_DGRAM)
family, socktype, proto, _, destination = addrinfo[0]
myip = None
with socket.socket(family, socktype, proto) as sock:
# UDP connect selects the route and local address without
# transmitting a packet.
sock.connect(destination)
myip = sock.getsockname()[0]
return myip
async def mac2ip(mac):
lla = None
@@ -778,6 +789,7 @@ async def get_nic_config(configmanager, node, ip=None, mac=None, ifidx=None,
'ipv6_address': None,
'ipv6_method': None,
'lease_time': None,
'config_name': None,
}
myaddrs = []
if ifidx is not None:
@@ -876,6 +888,7 @@ async def get_nic_config(configmanager, node, ip=None, mac=None, ifidx=None,
if ((isremote and ipn_on_same_subnet(fam, clientipn, candipn, int(candprefix)))
or ipn_on_same_subnet(fam, bootsvrip, candipn, prefix)):
bestsrvbyfam[fam] = svrip
cfgdata['config_name'] = candidate
cfgdata['ipv{}_address'.format(nver)] = candip
cfgdata['ipv{}_method'.format(nver)] = ipmethod
cfgdata['ipv{}_gateway'.format(nver)] = cfgbyname[candidate].get(
+9 -4
View File
@@ -107,7 +107,7 @@ class _MyClient(asyncssh.SSHClient):
self.confluent_custom_ctx = ctx
def connect(target, context=None, disable_hostkey_validation=False, known_hosts=(), nodename=None, configmanager=None, keyattrib='pubkeys.ssh', **kwargs):
def connect(target, context=None, disable_hostkey_validation=False, known_hosts=(), nodename=None, configmanager=None, keyattrib='pubkeys.ssh', quickquit=False, **kwargs):
if context is None:
context = {}
def make_client():
@@ -120,19 +120,24 @@ def connect(target, context=None, disable_hostkey_validation=False, known_hosts=
client.confluent_set_context(context)
return client
login_timeout=30
connect_timeout=30
if quickquit:
login_timeout=2
connect_timeout=2
sco = asyncssh.SSHClientConnectionOptions(
client_factory=make_client,
x509_trusted_cert_paths=None,
known_hosts=known_hosts,
login_timeout=3,
connect_timeout=2)
login_timeout=login_timeout,
connect_timeout=connect_timeout)
return asyncssh.connect(target, options=sco, **kwargs)
async def get_ssh_banner(target):
mycontext = {'nologon': True}
try:
async with connect(target, disable_hostkey_validation=True, context=mycontext):
async with connect(target, disable_hostkey_validation=True, quickquit=True, context=mycontext):
pass
except _CancelSsh:
pass