Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for initcheck of access devices #156

Merged
merged 20 commits into from
Jan 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
db076ed
Make some neighbor initchecks for access devices
indy-independence Jan 20, 2021
2824eed
Get linknets for mlag peer during initcheck
indy-independence Jan 20, 2021
c71bdec
Fix dev_mlag_peer undefined
indy-independence Jan 20, 2021
e7d5424
Only check access/dist devices as uplink candidates
indy-independence Jan 20, 2021
3cbde40
Fix merging of lists. Better error messages.
indy-independence Jan 20, 2021
21b0ad2
Accept linknets to mlag peer
indy-independence Jan 20, 2021
9638f1a
No further execution in loop if skipped neighbor
indy-independence Jan 20, 2021
8bca89a
Don't add loopback cables as neighbors
indy-independence Jan 20, 2021
4957b52
Check for mlag peer in neighbors
indy-independence Jan 20, 2021
221e09a
Add mlag_compatible to summarized compatability status
indy-independence Jan 20, 2021
d85aed7
Fix error message
indy-independence Jan 20, 2021
bad6578
Improve error message
indy-independence Jan 21, 2021
e8b8904
Merge branch 'develop' into feature.access_initcheck
indy-independence Jan 21, 2021
fd8cb63
Update init docs
indy-independence Jan 22, 2021
3aa35f0
Refactor update_linknets, move from get -> update module
indy-independence Jan 22, 2021
dabf83d
Normalize update_facts code
indy-independence Jan 22, 2021
297fa1f
Fix integration test for new getfacts output
indy-independence Jan 22, 2021
3c99e49
Allow adding of linknets between non-fabric devices via API (can help…
indy-independence Jan 22, 2021
6bbec9e
do device_initcheck before ztp in integrationtest
indy-independence Jan 28, 2021
67fb4da
Temp disable compatible=true check for initcheck
indy-independence Jan 28, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions docs/apiref/devices.rst
Original file line number Diff line number Diff line change
Expand Up @@ -230,10 +230,12 @@ This will schedule a job to send the configuration to the device.
Initialize check
----------------

Before initializing a new CORE or DIST device you can run a pre-check API call.
This will check that compatible LLDP neighbors are found and that the
interfaces facing these neighbors are set to the correct ifclass as well as
some basic device state checks.
Before initializing a new device you can run a pre-check API call. This will
perform some basic device state checks and check that compatible LLDP
neighbors are found. For access devices it will try and find a compatible
mgmtdomain and for core/dist devices it will check that interfaces facing
neighbors are set to the correct ifclass. It is possible that the init will
fail even if the initcheck passed.

To test if a device is compatible for DIST ZTP run:

Expand Down Expand Up @@ -280,7 +282,9 @@ actually not compatible for DIST ZTP at the moment. We did find a compatible
linknet, but there were not enough neighboring devices of the correct device
type found. If you want to perform some non-standard configuration like trying
ZTP with just one neighbor you can manually specify what neighbors you expect
to see instead.
to see instead ("neighbors": ["core1"]). Other arguments that can be passed
to device_init should also be valid here, like "mlag_peer_id" and
"mlag_peer_hostname" for access MLAG pairs.

If the checks can not be performed at all, like when the device is not found
or an invalid device type is specified the API call will return a 400 or 500
Expand Down
8 changes: 5 additions & 3 deletions docs/howto/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,11 @@ container at the initial steps of the process, and also logs from the API
container at later stages of the process. If the device gets stuck in the
DHCP_BOOT process for example, it probably means the API can not log in to the
device using the credentials and IP address saved in the database. The API
will retry connecting to the device 5 times with increasing delay between
each attempt. If you want to trigger more retries at a later point you can manually
call the discover_device API call and send the MAC and DHCP IP of the device.
will retry connecting to the device 3 times with increasing delay between
each attempt. If you want to trigger more retries at a later point you can
manually call the discover_device API call and send the MAC and DHCP IP of the
device. New attempts to discover the device will also be made when the DHCP
lease is renewed or reaquired.


Zero-touch provisioning of fabric switch
Expand Down
57 changes: 50 additions & 7 deletions src/cnaas_nms/api/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import cnaas_nms.confpush.sync_devices
import cnaas_nms.confpush.underlay
import cnaas_nms.confpush.get
import cnaas_nms.confpush.update
from cnaas_nms.confpush.nornir_helper import cnaas_init, inventory_selector
from cnaas_nms.api.generic import build_filter, empty_result
from cnaas_nms.db.device import Device, DeviceState, DeviceType
Expand Down Expand Up @@ -380,45 +381,87 @@ def post(self, device_id: int):
parsed_args = DeviceInitApi.arg_check(device_id, json_data)
target_devtype = DeviceType[parsed_args['device_type']]
target_hostname = parsed_args['new_hostname']
mlag_peer_target_hostname: Optional[str] = None
mlag_peer_id: Optional[int] = None
mlag_peer_dev: Optional[Device] = None
if 'mlag_peer_id' in parsed_args and 'mlag_peer_new_hostname' in parsed_args:
mlag_peer_target_hostname = parsed_args['mlag_peer_new_hostname']
mlag_peer_id = parsed_args['mlag_peer_id']
except ValueError as e:
return empty_result(status='error', data=str(e)), 400
return empty_result(status='error',
data="Error parsing arguments: {}".format(e)), 400

with sqla_session() as session:
try:
dev = cnaas_nms.confpush.init_device.pre_init_checks(session, device_id)
except ValueError as e:
return empty_result(status='error', data=str(e)), 400
return empty_result(status='error',
data="ValueError in pre_init_checks: {}".format(e)), 400
except Exception as e:
return empty_result(status='error', data=str(e)), 500
return empty_result(status='error',
data="Exception in pre_init_checks: {}".format(e)), 500

if mlag_peer_id:
try:
mlag_peer_dev = cnaas_nms.confpush.init_device.pre_init_checks(
session, mlag_peer_id)
except ValueError as e:
return empty_result(status='error',
data="ValueError in pre_init_checks: {}".format(e)), 400
except Exception as e:
return empty_result(status='error',
data="Exception in pre_init_checks: {}".format(e)), 500

try:
ret['linknets'] = cnaas_nms.confpush.get.update_linknets(
ret['linknets'] = cnaas_nms.confpush.update.update_linknets(
session,
hostname=dev.hostname,
devtype=target_devtype,
ztp_hostname=target_hostname,
dry_run=True
)
if mlag_peer_dev:
ret['linknets'] += cnaas_nms.confpush.update.update_linknets(
session,
hostname=mlag_peer_dev.hostname,
devtype=target_devtype,
ztp_hostname=mlag_peer_target_hostname,
dry_run=True
)
ret['linknets_compatible'] = True
except ValueError as e:
ret['linknets_compatible'] = False
ret['linknets_error'] = str(e)
except Exception as e:
return empty_result(status='error', data=str(e)), 500
return empty_result(status='error',
data="Exception in update_linknets: {}".format(e)), 500

try:
if 'linknets' in ret:
ret['neighbors'] = cnaas_nms.confpush.init_device.pre_init_check_neighbors(
session, dev, target_devtype,
ret['linknets'], parsed_args['neighbors'])
ret['linknets'], parsed_args['neighbors'], mlag_peer_dev)
ret['neighbors_compatible'] = True
else:
ret['neighbors_compatible'] = False
ret['neighbors_error'] = "No linknets found"
except (ValueError, cnaas_nms.confpush.init_device.InitVerificationError) as e:
ret['neighbors_compatible'] = False
ret['neighbors_error'] = str(e)
except Exception as e:
return empty_result(status='error', data=str(e)), 500
return empty_result(
status='error',
data="Exception in pre_init_check_neighbors: {}".format(e)), 500

if mlag_peer_dev:
try:
ret['mlag_compatible'] = mlag_peer_dev.hostname in ret['neighbors']
except Exception:
ret['mlag_compatible'] = False

ret['parsed_args'] = parsed_args
if mlag_peer_id and not ret['mlag_compatible']:
ret['compatible'] = False
if ret['linknets_compatible'] and ret['neighbors_compatible']:
ret['compatible'] = True
else:
Expand Down
48 changes: 44 additions & 4 deletions src/cnaas_nms/api/linknet.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from flask import request
from flask_restx import Resource, Namespace, fields
from flask_jwt_extended import jwt_required
from ipaddress import IPv4Network

from cnaas_nms.api.generic import empty_result
from cnaas_nms.db.session import sqla_session
from cnaas_nms.db.linknet import Linknet
from cnaas_nms.db.device import Device
from cnaas_nms.db.device import Device, DeviceType
from cnaas_nms.confpush.underlay import find_free_infra_linknet
from cnaas_nms.version import __api_version__

Expand Down Expand Up @@ -42,27 +43,66 @@ def post(self):
if 'device_a' in json_data:
if not Device.valid_hostname(json_data['device_a']):
errors.append("Invalid hostname specified for device_a")
else:
hostname_a = json_data['device_a']
else:
errors.append("Required field hostname_a not found")
if 'device_b' in json_data:
if not Device.valid_hostname(json_data['device_b']):
errors.append("Invalid hostname specified for device_b")
else:
hostname_b = json_data['device_b']
else:
errors.append("Required field hostname_b not found")
if 'device_a_port' not in json_data:
errors.append("Required field device_a_port not found")
if 'device_b_port' not in json_data:
errors.append("Required field device_b_port not found")

new_prefix = None
if 'prefix' in json_data:
if json_data['prefix']:
try:
new_prefix = IPv4Network(json_data['prefix'])
except Exception as e:
errors.append("Invalid prefix: {}".format(e))

if errors:
return empty_result(status='error', data=errors), 400

with sqla_session() as session:
dev_a: Device = session.query(Device).\
filter(Device.hostname == hostname_a).one_or_none()
if not dev_a:
return empty_result(
status='error',
data=f"Hostname '{hostname_a}' not found or is in invalid state"
), 400

dev_b: Device = session.query(Device). \
filter(Device.hostname == hostname_b).one_or_none()
if not dev_b:
return empty_result(
status='error',
data=f"Hostname '{hostname_b}' not found or is in invalid state"
), 400

# check if we need an ip prefix for the linknet
ip_linknet_devtypes = [DeviceType.CORE, DeviceType.DIST]
if dev_a.device_type in ip_linknet_devtypes and \
dev_b.device_type in ip_linknet_devtypes:
if not new_prefix:
new_prefix = find_free_infra_linknet(session)
if not new_prefix:
return empty_result(
status='error',
data="Device types requires IP linknets, but no prefix could be found"
), 400

try:
new_prefix = find_free_infra_linknet(session)
new_linknet = Linknet.create_linknet(
session, json_data['device_a'], json_data['device_a_port'],
json_data['device_b'], json_data['device_b_port'], new_prefix)
session, hostname_a, json_data['device_a_port'],
hostname_b, json_data['device_b_port'], new_prefix)
session.add(new_linknet)
session.commit()
data = new_linknet.as_dict()
Expand Down
Loading