def arg_check(cls, device_id: int, json_data: dict) -> dict: parsed_args = {'device_id': device_id} if not isinstance(device_id, int): raise ValueError("'device_id' must be an integer") if 'hostname' not in json_data: raise ValueError("POST data must include new 'hostname'") else: if not Device.valid_hostname(json_data['hostname']): raise ValueError("Provided hostname is not valid") else: parsed_args['new_hostname'] = json_data['hostname'] if 'device_type' not in json_data: raise ValueError("POST data must include 'device_type'") else: try: device_type = str(json_data['device_type']).upper() except Exception: raise ValueError("'device_type' must be a string") if DeviceType.has_name(device_type): parsed_args['device_type'] = device_type else: raise ValueError("Invalid 'device_type' provided") if 'mlag_peer_id' in json_data or 'mlag_peer_hostname' in json_data: if 'mlag_peer_id' not in json_data or 'mlag_peer_hostname' not in json_data: raise ValueError( "Both 'mlag_peer_id' and 'mlag_peer_hostname' must be specified" ) if not isinstance(json_data['mlag_peer_id'], int): raise ValueError("'mlag_peer_id' must be an integer") if not Device.valid_hostname(json_data['mlag_peer_hostname']): raise ValueError("Provided 'mlag_peer_hostname' is not valid") parsed_args['mlag_peer_id'] = json_data['mlag_peer_id'] parsed_args['mlag_peer_new_hostname'] = json_data[ 'mlag_peer_hostname'] if 'neighbors' in json_data and json_data['neighbors'] is not None: if isinstance(json_data['neighbors'], list): for neighbor in json_data['neighbors']: if not Device.valid_hostname(neighbor): raise ValueError( "Invalid hostname specified in neighbor list") parsed_args['neighbors'] = json_data['neighbors'] else: raise ValueError( "Neighbors must be specified as either a list of hostnames," "an empty list, or not specified at all") else: parsed_args['neighbors'] = None return parsed_args
def find_mgmtdomain(session, hostnames: List[str]) -> Optional[Mgmtdomain]: """Find the corresponding management domain for a pair of distribution switches. Args: hostnames: A list of two hostnames for the distribution switches Raises: ValueError: On invalid hostnames etc """ if not isinstance(hostnames, list) or not len(hostnames) == 2: raise ValueError( "hostnames argument must be a list with two device hostnames") for hostname in hostnames: if not Device.valid_hostname(hostname): raise ValueError(f"Argument {hostname} is not a valid hostname") try: device0 = session.query(Device).filter( Device.hostname == hostnames[0]).one() except NoResultFound: raise ValueError( f"hostname {hostnames[0]} not found in device database") try: device1 = session.query(Device).filter( Device.hostname == hostnames[1]).one() except NoResultFound: raise ValueError( f"hostname {hostnames[1]} not found in device database") mgmtdomain = session.query(Mgmtdomain).\ filter( ((Mgmtdomain.device_a == device0) & (Mgmtdomain.device_b == device1)) | ((Mgmtdomain.device_a == device1) & (Mgmtdomain.device_b == device0)) ).one_or_none() return mgmtdomain
def get(self): """ Get settings """ args = request.args hostname = None device_type = None model = None if 'hostname' in args: if Device.valid_hostname(args['hostname']): hostname = args['hostname'] else: return empty_result('error', "Invalid hostname specified"), 400 with sqla_session() as session: dev: Device = session.query(Device).\ filter(Device.hostname == hostname).one_or_none() if dev: device_type = dev.device_type model = dev.model else: return empty_result('error', "Hostname not found in database"), 400 if 'device_type' in args: if DeviceType.has_name(args['device_type'].upper()): device_type = DeviceType[args['device_type'].upper()] else: return empty_result('error', "Invalid device type specified"), 400 try: settings, settings_origin = get_settings(hostname, device_type, model) except Exception as e: return empty_result('error', "Error getting settings: {}".format(str(e))), 400 return empty_result(data={'settings': settings, 'settings_origin': settings_origin})
def settings_syncstatus( updated_settings: set) -> Tuple[Set[DeviceType], Set[str]]: """Determine what devices has become unsynchronized after updating the settings repository.""" unsynced_devtypes = set() unsynced_hostnames = set() filename: str for filename in updated_settings: basedir = filename.split(os.path.sep)[0] if basedir not in DIR_STRUCTURE: continue if basedir.startswith('global'): return {DeviceType.ACCESS, DeviceType.DIST, DeviceType.CORE}, set() elif basedir.startswith('fabric'): unsynced_devtypes.update({DeviceType.DIST, DeviceType.CORE}) elif basedir.startswith('access'): unsynced_devtypes.add(DeviceType.ACCESS) elif basedir.startswith('dist'): unsynced_devtypes.add(DeviceType.DIST) elif basedir.startswith('core'): unsynced_devtypes.add(DeviceType.CORE) elif basedir.startswith('devices'): try: hostname = filename.split(os.path.sep)[1] if Device.valid_hostname(hostname): unsynced_hostnames.add(hostname) except Exception as e: logger.exception( "Error in settings devices directory: {}".format(str(e))) else: logger.warn( "Unhandled settings file found {}, syncstatus not updated". format(filename)) return (unsynced_devtypes, unsynced_hostnames)
def get(self, hostname: str): """ Get device configuration """ result = empty_result() result['data'] = {'config': None} if not Device.valid_hostname(hostname): return empty_result(status='error', data=f"Invalid hostname specified"), 400 try: config, template_vars = cnaas_nms.confpush.sync_devices.generate_only( hostname) result['data']['config'] = { 'hostname': hostname, 'generated_config': config, 'available_variables': template_vars } except Exception as e: logger.exception( f"Exception while generating config for device {hostname}") return empty_result( status='error', data="Exception while generating config for device {}: {} {}". format(hostname, type(e), str(e))), 500 return result
def post(self, hostname: str): """Restore configuration to previous version""" json_data = request.get_json() apply_kwargs = {'hostname': hostname} config = None if not Device.valid_hostname(hostname): return empty_result(status='error', data=f"Invalid hostname specified"), 400 if 'job_id' in json_data: try: job_id = int(json_data['job_id']) except Exception: return empty_result('error', "job_id must be an integer"), 400 else: return empty_result('error', "job_id must be specified"), 400 with sqla_session() as session: try: prev_config_result = Job.get_previous_config(session, hostname, job_id=job_id) failed = prev_config_result['failed'] if not failed and 'config' in prev_config_result: config = prev_config_result['config'] except JobNotFoundError as e: return empty_result('error', str(e)), 404 except InvalidJobError as e: return empty_result('error', str(e)), 500 except Exception as e: return empty_result('error', "Unhandled exception: {}".format(e)), 500 if failed: return empty_result( 'error', "The specified job_id has a failed status"), 400 if not config: return empty_result('error', "No config found in this job"), 500 if 'dry_run' in json_data and isinstance(json_data['dry_run'], bool) \ and not json_data['dry_run']: apply_kwargs['dry_run'] = False else: apply_kwargs['dry_run'] = True apply_kwargs['config'] = config scheduler = Scheduler() job_id = scheduler.add_onetime_job( 'cnaas_nms.confpush.sync_devices:apply_config', when=1, scheduled_by=get_jwt_identity(), kwargs=apply_kwargs, ) res = empty_result(data=f"Scheduled job to restore {hostname}") res['job_id'] = job_id return res, 200
def verify_dir_structure(path: str, dir_structure: dict): """Verify that given path complies to given directory structure. Raises: VerifyPathException """ for item, subitem in dir_structure.items(): if isinstance(subitem, str) and subitem == 'file': filename = os.path.join(path, item) if not os.path.isfile(filename): if os.path.exists(filename): raise VerifyPathException( f"{filename} is not a regular file") else: raise VerifyPathException(f"File {filename} not found") elif item is Device: for hostname in os.listdir(path): hostname_path = os.path.join(path, hostname) if not os.path.isdir(hostname_path) or hostname.startswith( '.'): continue if not Device.valid_hostname(hostname): continue verify_dir_structure(hostname_path, subitem) else: dirname = os.path.join(path, item) if not os.path.isdir(dirname): if os.path.exists(dirname): raise VerifyPathException(f"{dirname} is not a directory") else: raise VerifyPathException(f"Directory {dirname} not found") if subitem: verify_dir_structure(os.path.join(path, item), dir_structure[item])
def post(self): json_data = request.get_json() kwargs: dict = {} if 'hostname' in json_data: hostname = str(json_data['hostname']) if not Device.valid_hostname(hostname): return empty_result( status='error', data=f"Hostname '{hostname}' is not a valid hostname"), 400 with sqla_session() as session: dev: Device = session.query(Device).\ filter(Device.hostname == hostname).one_or_none() if not dev or dev.state != DeviceState.MANAGED: return empty_result( status='error', data= f"Hostname '{hostname}' not found or is not a managed device" ), 400 kwargs['hostname'] = hostname what = hostname elif 'device_type' in json_data: if DeviceType.has_name(str(json_data['device_type']).upper()): kwargs['device_type'] = str(json_data['device_type']).upper() else: return empty_result( status='error', data= f"Invalid device type '{json_data['device_type']}' specified" ), 400 what = f"{json_data['device_type']} devices" elif 'all' in json_data and isinstance(json_data['all'], bool) and json_data['all']: what = "all devices" else: return empty_result( status='error', data=f"No devices to synchronize was specified"), 400 if 'dry_run' in json_data and isinstance(json_data['dry_run'], bool) \ and not json_data['dry_run']: kwargs['dry_run'] = False if 'force' in json_data and isinstance(json_data['force'], bool): kwargs['force'] = json_data['force'] scheduler = Scheduler() job_id = scheduler.add_onetime_job( 'cnaas_nms.confpush.sync_devices:sync_devices', when=1, kwargs=kwargs) res = empty_result(data=f"Scheduled job to synchronize {what}") res['job_id'] = job_id return res
def post(self): """ Add a new linknet """ json_data = request.get_json() data = {} errors = [] if 'device_a' in json_data: if not Device.valid_hostname(json_data['device_a']): errors.append("Invalid hostname specified for 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: 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") if errors: return empty_result(status='error', data=errors), 400 with sqla_session() as session: 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.add(new_linknet) session.commit() data = new_linknet.as_dict() except Exception as e: session.rollback() return empty_result(status='error', data=str(e)), 500 return empty_result(status='success', data=data), 201
def post(self, device_id: int): if not isinstance(device_id, int): return empty_result(status='error', data="'device_id' must be an integer"), 400 json_data = request.get_json() if 'hostname' not in json_data: return empty_result( status='error', data="POST data must include new 'hostname'"), 400 else: if not Device.valid_hostname(json_data['hostname']): return empty_result(status='error', data='Provided hostname is not valid'), 400 else: new_hostname = json_data['hostname'] if 'device_type' not in json_data: return empty_result( status='error', data="POST data must include 'device_type'"), 400 else: try: device_type = str(json_data['device_type']).upper() except: return empty_result(status='error', data="'device_type' must be a string"), 400 if not DeviceType.has_name(device_type): return empty_result(status='error', data="Invalid 'device_type' provided"), 400 if device_type == DeviceType.ACCESS.name: scheduler = Scheduler() job_id = scheduler.add_onetime_job( 'cnaas_nms.confpush.init_device:init_access_device_step1', when=1, kwargs={ 'device_id': device_id, 'new_hostname': new_hostname }) res = empty_result( data=f"Scheduled job to initialize device_id { device_id }") res['job_id'] = job_id return res
def post(self): """ Start update facts of device(s) """ json_data = request.get_json() kwargs: dict = {} total_count: Optional[int] = None if 'hostname' in json_data: hostname = str(json_data['hostname']) if not Device.valid_hostname(hostname): return empty_result( status='error', data=f"Hostname '{hostname}' is not a valid hostname"), 400 with sqla_session() as session: dev: Device = session.query(Device). \ filter(Device.hostname == hostname).one_or_none() if not dev or (dev.state != DeviceState.MANAGED and dev.state != DeviceState.UNMANAGED): return empty_result( status='error', data= f"Hostname '{hostname}' not found or is in invalid state" ), 400 kwargs['hostname'] = hostname total_count = 1 else: return empty_result( status='error', data="No target to be updated was specified"), 400 scheduler = Scheduler() job_id = scheduler.add_onetime_job( 'cnaas_nms.confpush.update:update_facts', when=1, scheduled_by=get_jwt_identity(), kwargs=kwargs) res = empty_result( data=f"Scheduled job to update facts for {hostname}") res['job_id'] = job_id resp = make_response(json.dumps(res), 200) if total_count: resp.headers['X-Total-Count'] = total_count resp.headers['Content-Type'] = "application/json" return resp
def get_evpn_spines(session, settings: dict): logger = get_logger() device_hostnames = [] for entry in settings['evpn_peers']: if 'hostname' in entry and Device.valid_hostname(entry['hostname']): device_hostnames.append(entry['hostname']) else: logger.error( "Invalid entry specified in settings->evpn_peers, ignoring: {}" .format(entry)) ret = [] for hostname in device_hostnames: dev = session.query(Device).filter( Device.hostname == hostname).one_or_none() if dev: ret.append(dev) return ret
def get_evpn_peers(session, settings: dict): logger = get_logger() device_hostnames = [] for entry in settings['evpn_peers']: if 'hostname' in entry and Device.valid_hostname(entry['hostname']): device_hostnames.append(entry['hostname']) else: logger.error("Invalid entry specified in settings->evpn_peers, ignoring: {}".format(entry)) ret = [] for hostname in device_hostnames: dev = session.query(Device).filter(Device.hostname == hostname).one_or_none() if dev: ret.append(dev) # If no evpn_peers were specified return a list of all CORE devices instead if not ret: core_devs = session.query(Device).filter(Device.device_type == DeviceType.CORE).all() for dev in core_devs: ret.append(dev) return ret
def get(self, hostname: str): args = request.args result = empty_result() result['data'] = {'config': None} if not Device.valid_hostname(hostname): return empty_result(status='error', data=f"Invalid hostname specified"), 400 kwargs = {} if 'job_id' in args: try: kwargs['job_id'] = int(args['job_id']) except Exception: return empty_result('error', "job_id must be an integer"), 400 elif 'previous' in args: try: kwargs['previous'] = int(args['previous']) except Exception: return empty_result('error', "previous must be an integer"), 400 elif 'before' in args: try: kwargs['before'] = datetime.datetime.fromisoformat( args['before']) except Exception: return empty_result( 'error', "before must be a valid ISO format date time string"), 400 with sqla_session() as session: try: result['data'] = Job.get_previous_config( session, hostname, **kwargs) except JobNotFoundError as e: return empty_result('error', str(e)), 404 except InvalidJobError as e: return empty_result('error', str(e)), 500 except Exception as e: return empty_result('error', "Unhandled exception: {}".format(e)), 500 return result
def get_all_mgmtdomains(session, hostname: str) -> List[Mgmtdomain]: """ Get all mgmtdomains for a specific distribution switch. Args: session: sqla session hostname: hostname of distribution switch Raises: ValueError: on invalid hostname etc """ if not Device.valid_hostname(hostname): raise ValueError(f"Argument {hostname} is not a valid hostname") try: dev = session.query(Device).filter(Device.hostname == hostname).one() except NoResultFound: raise ValueError(f"hostname {hostname} not found in device database") mgmtdomains = session.query(Mgmtdomain). \ filter((Mgmtdomain.device_a == dev) | (Mgmtdomain.device_b == dev)).all() return mgmtdomains
def post(self, hostname: str): """Apply exact specified configuration to device without using templates""" json_data = request.get_json() apply_kwargs = {'hostname': hostname} allow_live_run = get_apidata()['allow_apply_config_liverun'] if not Device.valid_hostname(hostname): return empty_result( status='error', data=f"Invalid hostname specified" ), 400 if 'full_config' not in json_data: return empty_result('error', "full_config must be specified"), 400 if 'dry_run' in json_data and isinstance(json_data['dry_run'], bool) \ and not json_data['dry_run']: if allow_live_run: apply_kwargs['dry_run'] = False else: return empty_result('error', "Apply config live_run is not allowed"), 400 else: apply_kwargs['dry_run'] = True apply_kwargs['config'] = json_data['full_config'] scheduler = Scheduler() job_id = scheduler.add_onetime_job( 'cnaas_nms.confpush.sync_devices:apply_config', when=1, scheduled_by=get_jwt_identity(), kwargs=apply_kwargs, ) res = empty_result(data=f"Scheduled job to apply config {hostname}") res['job_id'] = job_id return res, 200
def post(self): """Execute certificate related actions on device""" json_data = request.get_json() # default args kwargs: dict = {} if 'action' in json_data and isinstance(json_data['action'], str): action = json_data['action'].upper() else: return empty_result( status='error', data=f"Required field 'action' was not specified"), 400 if 'comment' in json_data and isinstance(json_data['comment'], str): kwargs['job_comment'] = json_data['comment'] if 'ticket_ref' in json_data and isinstance(json_data['ticket_ref'], str): kwargs['job_ticket_ref'] = json_data['ticket_ref'] total_count: Optional[int] = None nr = cnaas_init() if 'hostname' in json_data: hostname = str(json_data['hostname']) if not Device.valid_hostname(hostname): return empty_result( status='error', data=f"Hostname '{hostname}' is not a valid hostname"), 400 _, total_count, _ = inventory_selector(nr, hostname=hostname) if total_count != 1: return empty_result( status='error', data= f"Hostname '{hostname}' not found or is not a managed device" ), 400 kwargs['hostname'] = hostname elif 'group' in json_data: group_name = str(json_data['group']) if group_name not in get_groups(): return empty_result( status='error', data='Could not find a group with name {}'.format( group_name)) kwargs['group'] = group_name _, total_count, _ = inventory_selector(nr, group=group_name) else: return empty_result(status='error', data=f"No devices were specified"), 400 if action == 'RENEW': scheduler = Scheduler() job_id = scheduler.add_onetime_job( 'cnaas_nms.confpush.cert:renew_cert', when=1, scheduled_by=get_jwt_identity(), kwargs=kwargs) res = empty_result(data=f"Scheduled job to renew certificates") res['job_id'] = job_id resp = make_response(json.dumps(res), 200) if total_count: resp.headers['X-Total-Count'] = total_count resp.headers['Content-Type'] = "application/json" return resp else: return empty_result( status='error', data=f"Unknown action specified: {action}"), 400
def post(self): json_data = request.get_json() data = {} errors = [] with sqla_session() as session: if 'device_a' in json_data: hostname_a = str(json_data['device_a']) if not Device.valid_hostname(hostname_a): errors.append( f"Invalid hostname for device_a: {hostname_a}") else: device_a = session.query(Device).\ filter(Device.hostname == hostname_a).one_or_none() if not device_a: errors.append( f"Device with hostname {hostname_a} not found") else: data['device_a'] = device_a if 'device_b' in json_data: hostname_b = str(json_data['device_b']) if not Device.valid_hostname(hostname_b): errors.append( f"Invalid hostname for device_b: {hostname_b}") else: device_b = session.query(Device).\ filter(Device.hostname == hostname_b).one_or_none() if not device_b: errors.append( f"Device with hostname {hostname_b} not found") else: data['device_b'] = device_b if 'vlan' in json_data: try: vlan_id_int = int(json_data['vlan']) except: errors.append('Invalid VLAN received.') else: data['vlan'] = vlan_id_int if 'ipv4_gw' in json_data: try: addr = IPv4Interface(json_data['ipv4_gw']) prefix_len = int(addr.network.prefixlen) except: errors.append(('Invalid ipv4_gw received. ' 'Must be correct IPv4 address with mask')) else: if prefix_len <= 31 and prefix_len >= 16: data['ipv4_gw'] = str(addr) else: errors.append( "Bad prefix length for management network: {}". format(prefix_len)) required_keys = ['device_a', 'device_b', 'vlan', 'ipv4_gw'] if all([key in data for key in required_keys]): new_mgmtd = Mgmtdomain() new_mgmtd.device_a = data['device_a'] new_mgmtd.device_b = data['device_b'] new_mgmtd.ipv4_gw = data['ipv4_gw'] new_mgmtd.vlan = data['vlan'] result = session.add(new_mgmtd) return empty_result(result, 200) else: errors.append("Not all required inputs were found: {}".\ format(', '.join(required_keys))) return empty_result('error', errors), 400
def post(self, device_id: int): """ Init a device """ if not isinstance(device_id, int): return empty_result(status='error', data="'device_id' must be an integer"), 400 json_data = request.get_json() if 'hostname' not in json_data: return empty_result(status='error', data="POST data must include new 'hostname'"), 400 else: if not Device.valid_hostname(json_data['hostname']): return empty_result( status='error', data='Provided hostname is not valid'), 400 else: new_hostname = json_data['hostname'] if 'device_type' not in json_data: return empty_result(status='error', data="POST data must include 'device_type'"), 400 else: try: device_type = str(json_data['device_type']).upper() except Exception: return empty_result(status='error', data="'device_type' must be a string"), 400 if not DeviceType.has_name(device_type): return empty_result(status='error', data="Invalid 'device_type' provided"), 400 job_kwargs = { 'device_id': device_id, 'new_hostname': new_hostname } if 'mlag_peer_id' in json_data or 'mlag_peer_hostname' in json_data: if 'mlag_peer_id' not in json_data or 'mlag_peer_hostname' not in json_data: return empty_result( status='error', data="Both 'mlag_peer_id' and 'mlag_peer_hostname' must be specified"), 400 if not isinstance(json_data['mlag_peer_id'], int): return empty_result(status='error', data="'mlag_peer_id' must be an integer"), 400 if not Device.valid_hostname(json_data['mlag_peer_hostname']): return empty_result( status='error', data="Provided 'mlag_peer_hostname' is not valid"), 400 job_kwargs['mlag_peer_id'] = json_data['mlag_peer_id'] job_kwargs['mlag_peer_new_hostname'] = json_data['mlag_peer_hostname'] if device_type == DeviceType.ACCESS.name: scheduler = Scheduler() job_id = scheduler.add_onetime_job( 'cnaas_nms.confpush.init_device:init_access_device_step1', when=1, scheduled_by=get_jwt_identity(), kwargs=job_kwargs) else: return empty_result(status='error', data="Unsupported 'device_type' provided"), 400 res = empty_result(data=f"Scheduled job to initialize device_id { device_id }") res['job_id'] = job_id return res
def post(self): """Update/scan interfaces of device""" json_data = request.get_json() kwargs: dict = { "replace": False, "delete_all": False, "mlag_peer_hostname": None } total_count: Optional[int] = None if 'hostname' in json_data: hostname = str(json_data['hostname']) if not Device.valid_hostname(hostname): return empty_result( status='error', data=f"Hostname '{hostname}' is not a valid hostname"), 400 with sqla_session() as session: dev: Device = session.query(Device). \ filter(Device.hostname == hostname).one_or_none() if not dev or (dev.state != DeviceState.MANAGED and dev.state != DeviceState.UNMANAGED): return empty_result( status='error', data= f"Hostname '{hostname}' not found or is in invalid state" ), 400 if dev.device_type != DeviceType.ACCESS: return empty_result( status='error', data= f"Only devices of type ACCESS has interface database to update" ), 400 kwargs['hostname'] = hostname total_count = 1 else: return empty_result( status='error', data="No target to be updated was specified"), 400 if 'mlag_peer_hostname' in json_data: mlag_peer_hostname = str(json_data['mlag_peer_hostname']) if not Device.valid_hostname(mlag_peer_hostname): return empty_result( status='error', data= f"Hostname '{mlag_peer_hostname}' is not a valid hostname" ), 400 with sqla_session() as session: dev: Device = session.query(Device). \ filter(Device.hostname == mlag_peer_hostname).one_or_none() if not dev or (dev.state != DeviceState.MANAGED and dev.state != DeviceState.UNMANAGED): return empty_result( status='error', data= f"Hostname '{mlag_peer_hostname}' not found or is in invalid state" ), 400 if dev.device_type != DeviceType.ACCESS: return empty_result( status='error', data= f"Only devices of type ACCESS has interface database to update" ), 400 kwargs['mlag_peer_hostname'] = mlag_peer_hostname if 'replace' in json_data and isinstance(json_data['replace'], bool) \ and json_data['replace']: kwargs['replace'] = True if 'delete_all' in json_data and isinstance(json_data['delete_all'], bool) \ and json_data['delete_all']: kwargs['delete_all'] = True scheduler = Scheduler() job_id = scheduler.add_onetime_job( 'cnaas_nms.confpush.update:update_interfacedb', when=1, scheduled_by=get_jwt_identity(), kwargs=kwargs) res = empty_result( data=f"Scheduled job to update interfaces for {hostname}") res['job_id'] = job_id resp = make_response(json.dumps(res), 200) if total_count: resp.headers['X-Total-Count'] = total_count resp.headers['Content-Type'] = "application/json" return resp
def find_mgmtdomain(session, hostnames: List[str]) -> Optional[Mgmtdomain]: """Find the corresponding management domain for a pair of distribution switches. Args: hostnames: A list of two hostnames for the distribution switches Raises: ValueError: On invalid hostnames etc """ if not isinstance(hostnames, list) or not len(hostnames) == 2: raise ValueError( "Two uplink devices are required to find a compatible mgmtdomain, got: {}" .format(hostnames)) for hostname in hostnames: if not Device.valid_hostname(hostname): raise ValueError(f"Argument {hostname} is not a valid hostname") try: device0: Device = session.query(Device).filter( Device.hostname == hostnames[0]).one() except NoResultFound: raise ValueError( f"hostname {hostnames[0]} not found in device database") try: device1: Device = session.query(Device).filter( Device.hostname == hostnames[1]).one() except NoResultFound: raise ValueError( f"hostname {hostnames[1]} not found in device database") if device0.device_type == DeviceType.DIST or device1.device_type == DeviceType.DIST: if device0.device_type != DeviceType.DIST or device1.device_type != DeviceType.DIST: raise ValueError( "Both uplink devices must be of same device type: {}, {}". format(device0.hostname, device1.hostname)) try: mgmtdomain: Mgmtdomain = session.query(Mgmtdomain).\ filter( ((Mgmtdomain.device_a == device0) & (Mgmtdomain.device_b == device1)) | ((Mgmtdomain.device_a == device1) & (Mgmtdomain.device_b == device0)) ).one_or_none() if not mgmtdomain: mgmtdomain: Mgmtdomain = session.query(Mgmtdomain).filter( (Mgmtdomain.device_a.has( Device.device_type == DeviceType.CORE)) | (Mgmtdomain.device_b.has( Device.device_type == DeviceType.CORE))).one_or_none() except MultipleResultsFound: raise Exception( "Found multiple possible mgmtdomains, please remove any redundant mgmtdomains" ) elif device0.device_type == DeviceType.ACCESS or device1.device_type == DeviceType.ACCESS: if device0.device_type != DeviceType.ACCESS or device1.device_type != DeviceType.ACCESS: raise ValueError( "Both uplink devices must be of same device type: {}, {}". format(device0.hostname, device1.hostname)) mgmtdomain: Mgmtdomain = find_mgmtdomain_by_ip(session, device0.management_ip) if mgmtdomain.id != find_mgmtdomain_by_ip(session, device1.management_ip).id: raise Exception( "Uplink access devices have different mgmtdomains: {}, {}". format(device0.hostname, device1.hostname)) else: raise Exception("Unknown uplink device type") return mgmtdomain
def post(self): """ Upgrade firmware on device """ json_data = request.get_json() kwargs = dict() seconds = 1 date_format = "%Y-%m-%d %H:%M:%S" url = self.firmware_url() if 'url' not in json_data and url == '': return empty_result(status='error', data='No external address configured for ' 'HTTPD, please specify one with "url"') if 'url' not in json_data: kwargs['url'] = url else: if isinstance(json_data['url'], str): kwargs['url'] = json_data['url'] else: return empty_result(status='error', data='url should be a string') if 'activate' in json_data: if isinstance(json_data['activate'], bool): kwargs['activate'] = json_data['activate'] else: return empty_result(status='error', data='activate should be a boolean') if 'download' in json_data: if isinstance(json_data['download'], bool): kwargs['download'] = json_data['download'] else: return empty_result(status='error', data='download should be a boolean') if 'reboot' in json_data: if isinstance(json_data['reboot'], bool): kwargs['reboot'] = json_data['reboot'] else: return empty_result(status='error', data='reboot should be a boolean') if 'pre_flight' in json_data: if isinstance(json_data['pre_flight'], bool): kwargs['pre_flight'] = json_data['pre_flight'] else: return empty_result(status='error', data='pre_flight should be a boolean') if 'post_flight' in json_data: if isinstance(json_data['post_flight'], bool): kwargs['post_flight'] = json_data['post_flight'] else: return empty_result(status='error', data='post_flight should be a boolean') if 'post_waittime' in json_data: if isinstance(json_data['post_waittime'], int): kwargs['post_waittime'] = json_data['post_waittime'] else: return empty_result(status='error', data='post_waittime should be an integer') if 'filename' in json_data: if isinstance(json_data['filename'], str): kwargs['filename'] = json_data['filename'] else: return empty_result(status='error', data='filename should be a string') total_count: Optional[int] = None nr = cnaas_init() if 'hostname' in json_data: hostname = str(json_data['hostname']) if not Device.valid_hostname(hostname): return empty_result( status='error', data=f"Hostname '{hostname}' is not a valid hostname" ), 400 _, total_count, _ = inventory_selector(nr, hostname=hostname) if total_count != 1: return empty_result( status='error', data=f"Hostname '{hostname}' not found or is not a managed device" ), 400 kwargs['hostname'] = hostname elif 'group' in json_data: group_name = str(json_data['group']) if group_name not in get_groups(): return empty_result(status='error', data='Could not find a group with name {}'.format(group_name)) kwargs['group'] = group_name _, total_count, _ = inventory_selector(nr, group=group_name) kwargs['group'] = group_name else: return empty_result( status='error', data=f"No devices to upgrade were specified" ), 400 if 'comment' in json_data and isinstance(json_data['comment'], str): kwargs['job_comment'] = json_data['comment'] if 'ticket_ref' in json_data and isinstance(json_data['ticket_ref'], str): kwargs['job_ticket_ref'] = json_data['ticket_ref'] if 'start_at' in json_data: try: time_start = datetime.strptime(json_data['start_at'], date_format) time_now = datetime.utcnow() if time_start < time_now: return empty_result(status='error', data='start_at must be in the future') time_diff = time_start - time_now seconds = int(time_diff.total_seconds()) except Exception as e: logger.exception(f'Exception when scheduling job: {e}') return empty_result(status='error', data=f'Invalid date format, should be: {date_format}') scheduler = Scheduler() job_id = scheduler.add_onetime_job( 'cnaas_nms.confpush.firmware:device_upgrade', when=seconds, scheduled_by=get_jwt_identity(), kwargs=kwargs) res = empty_result(data='Scheduled job to upgrade devices') res['job_id'] = job_id resp = make_response(json.dumps(res), 200) if total_count: resp.headers['X-Total-Count'] = total_count resp.headers['Content-Type'] = "application/json" return resp
def _refresh_repo_task(repo_type: RepoType = RepoType.TEMPLATES) -> str: """Should only be called by refresh_repo function.""" with open('/etc/cnaas-nms/repository.yml', 'r') as db_file: repo_config = yaml.safe_load(db_file) if repo_type == RepoType.TEMPLATES: local_repo_path = repo_config['templates_local'] remote_repo_path = repo_config['templates_remote'] elif repo_type == RepoType.SETTINGS: local_repo_path = repo_config['settings_local'] remote_repo_path = repo_config['settings_remote'] else: raise ValueError("Invalid repository") ret = '' changed_files: Set[str] = set() try: local_repo = Repo(local_repo_path) prev_commit = local_repo.commit().hexsha diff = local_repo.remotes.origin.pull() for item in diff: ret += 'Commit {} by {} at {}\n'.format( item.commit.name_rev, item.commit.committer, item.commit.committed_datetime) diff_files = local_repo.git.diff('{}..{}'.format( prev_commit, item.commit.hexsha), name_only=True).split() changed_files.update(diff_files) prev_commit = item.commit.hexsha except (InvalidGitRepositoryError, NoSuchPathError) as e: logger.info("Local repository {} not found, cloning from remote".\ format(local_repo_path)) try: local_repo = Repo.clone_from(remote_repo_path, local_repo_path) except NoSuchPathError as e: raise ConfigException("Invalid remote repository {}: {}".format( remote_repo_path, str(e))) except GitCommandError as e: raise ConfigException( "Error cloning remote repository {}: {}".format( remote_repo_path, str(e))) ret = 'Cloned new from remote. Last commit {} by {} at {}'.format( local_repo.head.commit.name_rev, local_repo.head.commit.committer, local_repo.head.commit.committed_datetime) if repo_type == RepoType.SETTINGS: try: logger.debug("Clearing redis-lru cache for settings") with redis_session() as redis_db: cache = RedisLRU(redis_db) cache.clear_all_cache() get_settings() test_devtypes = [ DeviceType.ACCESS, DeviceType.DIST, DeviceType.CORE ] for devtype in test_devtypes: get_settings(device_type=devtype) for hostname in os.listdir(os.path.join(local_repo_path, 'devices')): hostname_path = os.path.join(local_repo_path, 'devices', hostname) if not os.path.isdir(hostname_path) or hostname.startswith( '.'): continue if not Device.valid_hostname(hostname): continue get_settings(hostname) check_settings_collisions() except SettingsSyntaxError as e: logger.exception("Error in settings repo configuration: {}".format( str(e))) raise e except VlanConflictError as e: logger.exception("VLAN conflict in repo configuration: {}".format( str(e))) raise e logger.debug( "Files changed in settings repository: {}".format(changed_files)) updated_devtypes, updated_hostnames = settings_syncstatus( updated_settings=changed_files) logger.debug( "Devicestypes to be marked unsynced after repo refresh: {}".format( ', '.join([dt.name for dt in updated_devtypes]))) logger.debug( "Devices to be marked unsynced after repo refresh: {}".format( ', '.join(updated_hostnames))) with sqla_session() as session: devtype: DeviceType for devtype in updated_devtypes: Device.set_devtype_syncstatus(session, devtype, syncstatus=False) for hostname in updated_hostnames: dev: Device = session.query(Device).\ filter(Device.hostname == hostname).one_or_none() if dev: dev.synchronized = False else: logger.warn( "Settings updated for unknown device: {}".format( hostname)) if repo_type == RepoType.TEMPLATES: logger.debug( "Files changed in template repository: {}".format(changed_files)) updated_devtypes = template_syncstatus(updated_templates=changed_files) updated_list = [ '{}:{}'.format(platform, dt.name) for dt, platform in updated_devtypes ] logger.debug( "Devicestypes to be marked unsynced after repo refresh: {}".format( ', '.join(updated_list))) with sqla_session() as session: devtype: DeviceType for devtype, platform in updated_devtypes: Device.set_devtype_syncstatus(session, devtype, platform, syncstatus=False) return ret
def put(self, hostname): """Take a map of interfaces and associated values to update. Example: {"interfaces": {"Ethernet1": {"configtype": "ACCESS_AUTO"}}} """ json_data = request.get_json() data = {} errors = [] device_settings = None with sqla_session() as session: dev: Device = session.query(Device).filter(Device.hostname == hostname).one_or_none() if not dev: return empty_result('error', "Device not found"), 404 updated = False if 'interfaces' in json_data and isinstance(json_data['interfaces'], dict): for if_name, if_dict in json_data['interfaces'].items(): if not isinstance(if_dict, dict): errors.append("Each interface must have a dict with data to update") continue intf: Interface = session.query(Interface).filter(Interface.device == dev).\ filter(Interface.name == if_name).one_or_none() if not intf: errors.append(f"Interface {if_name} not found") continue if intf.data and isinstance(intf.data, dict): intfdata_original = dict(intf.data) intfdata = dict(intf.data) else: intfdata_original = {} intfdata = {} if 'configtype' in if_dict: configtype = if_dict['configtype'].upper() if InterfaceConfigType.has_name(configtype): if intf.configtype != InterfaceConfigType[configtype]: intf.configtype = InterfaceConfigType[configtype] updated = True data[if_name] = {'configtype': configtype} else: errors.append(f"Invalid configtype received: {configtype}") if 'data' in if_dict: # TODO: maybe this validation should be done via # pydantic if it gets more complex if not device_settings: device_settings, _ = get_settings(hostname, dev.device_type) if 'vxlan' in if_dict['data']: if if_dict['data']['vxlan'] in device_settings['vxlans']: intfdata['vxlan'] = if_dict['data']['vxlan'] else: errors.append("Specified VXLAN {} is not present in {}".format( if_dict['data']['vxlan'], hostname )) if 'untagged_vlan' in if_dict['data']: vlan_id = resolve_vlanid(if_dict['data']['untagged_vlan'], device_settings['vxlans']) if vlan_id: intfdata['untagged_vlan'] = if_dict['data']['untagged_vlan'] else: errors.append("Specified VLAN name {} is not present in {}".format( if_dict['data']['untagged_vlan'], hostname )) if 'tagged_vlan_list' in if_dict['data']: if isinstance(if_dict['data']['tagged_vlan_list'], list): vlan_id_list = resolve_vlanid_list(if_dict['data']['tagged_vlan_list'], device_settings['vxlans']) if len(vlan_id_list) == len(if_dict['data']['tagged_vlan_list']): intfdata['tagged_vlan_list'] = if_dict['data']['tagged_vlan_list'] else: errors.append("Some VLAN names {} are not present in {}".format( ", ".join(if_dict['data']['tagged_vlan_list']), hostname )) else: errors.append("tagged_vlan_list should be of type list, found {}".format( type(if_dict['data']['tagged_vlan_list']) )) if 'neighbor' in if_dict['data']: if isinstance(if_dict['data']['neighbor'], str) and \ Device.valid_hostname(if_dict['data']['neighbor']): intfdata['neighbor'] = if_dict['data']['neighbor'] else: errors.append("Neighbor must be valid hostname, got: {}".format( if_dict['data']['neighbor'])) if 'description' in if_dict['data']: if isinstance(if_dict['data']['description'], str) and \ len(if_dict['data']['description']) <= 64: if if_dict['data']['description']: intfdata['description'] = if_dict['data']['description'] elif 'description' in intfdata: del intfdata['description'] elif if_dict['data']['description'] is None: if 'description' in intfdata: del intfdata['description'] else: errors.append( "Description must be a string of 0-64 characters for: {}". format(if_dict['data']['description'])) if 'enabled' in if_dict['data']: if type(if_dict['data']['enabled']) == bool: intfdata['enabled'] = if_dict['data']['enabled'] else: errors.append( "Enabled must be a bool, true or false, got: {}". format(if_dict['data']['enabled'])) if intfdata != intfdata_original: intf.data = intfdata updated = True if if_name in data: data[if_name]['data'] = intfdata else: data[if_name] = {'data': intfdata} if updated: dev.synchronized = False if errors: if data: ret = {'errors': errors, 'updated': data} else: ret = {'errors': errors} return empty_result(status='error', data=ret), 400 else: return empty_result(status='success', data={'updated': data})
def put(self, device_id): json_data = request.get_json() data = {} errors = [] if 'state' in json_data: try: state = str(json_data['state']).upper() except: errors.append('Invalid device state received.') else: if DeviceState.has_name(state): data['state'] = DeviceState[state] else: errors.append('Invalid device state received.') if 'device_type' in json_data: try: device_type = str(json_data['device_type']).upper() except: errors.append('Invalid device type received.') else: if DeviceType.has_name(device_type): data['device_type'] = DeviceType[device_type] else: errors.append('Invalid device type received.') if 'management_ip' in json_data: if json_data['management_ip'] == None: data['management_ip'] = None else: try: addr = IPv4Address(json_data['management_ip']) except: errors.append( 'Invalid management_ip received. Must be correct IPv4 address.' ) else: data['management_ip'] = addr if 'dhcp_ip' in json_data: if json_data['dhcp_ip'] == None: data['dhcp_ip'] = None else: try: addr = IPv4Address(json_data['dhcp_ip']) except: errors.append( 'Invalid dhcp_ip received. Must be correct IPv4 address.' ) else: data['dhcp_ip'] = addr if 'hostname' in json_data: if Device.valid_hostname(json_data['hostname']): data['hostname'] = json_data['hostname'] else: errors.append("Invalid hostname received") with sqla_session() as session: instance = session.query(Device).filter( Device.id == device_id).one_or_none() if instance: #TODO: auto loop through class members and match if 'state' in data: instance.state = data['state'] if 'device_type' in data: instance.device_type = data['device_type'] if 'management_ip' in data: instance.management_ip = data['management_ip'] if 'dhcp_ip' in data: instance.dhcp_ip = data['dhcp_ip'] if 'hostname' in data: instance.hostname = data['hostname'] else: errors.append('Device not found')
def post(self): """ Start sync of device(s) """ json_data = request.get_json() # default args kwargs: dict = { 'dry_run': True, 'auto_push': False, 'force': False, 'resync': False } if 'dry_run' in json_data and isinstance(json_data['dry_run'], bool) \ and not json_data['dry_run']: kwargs['dry_run'] = False if 'force' in json_data and isinstance(json_data['force'], bool): kwargs['force'] = json_data['force'] if 'auto_push' in json_data and isinstance(json_data['auto_push'], bool): kwargs['auto_push'] = json_data['auto_push'] if 'resync' in json_data and isinstance(json_data['resync'], bool): kwargs['resync'] = json_data['resync'] if 'comment' in json_data and isinstance(json_data['comment'], str): kwargs['job_comment'] = json_data['comment'] if 'ticket_ref' in json_data and isinstance(json_data['ticket_ref'], str): kwargs['job_ticket_ref'] = json_data['ticket_ref'] total_count: Optional[int] = None nr = cnaas_init() if 'hostname' in json_data: hostname = str(json_data['hostname']) if not Device.valid_hostname(hostname): return empty_result( status='error', data=f"Hostname '{hostname}' is not a valid hostname"), 400 _, total_count, _ = inventory_selector(nr, hostname=hostname) if total_count != 1: return empty_result( status='error', data= f"Hostname '{hostname}' not found or is not a managed device" ), 400 kwargs['hostnames'] = [hostname] what = hostname elif 'device_type' in json_data: devtype_str = str(json_data['device_type']).upper() if DeviceType.has_name(devtype_str): kwargs['device_type'] = devtype_str else: return empty_result( status='error', data= f"Invalid device type '{json_data['device_type']}' specified" ), 400 what = f"{json_data['device_type']} devices" _, total_count, _ = inventory_selector(nr, resync=kwargs['resync'], device_type=devtype_str) elif 'group' in json_data: group_name = str(json_data['group']) if group_name not in get_groups(): return empty_result( status='error', data='Could not find a group with name {}'.format( group_name)) kwargs['group'] = group_name what = 'group {}'.format(group_name) _, total_count, _ = inventory_selector(nr, resync=kwargs['resync'], group=group_name) elif 'all' in json_data and isinstance(json_data['all'], bool) and json_data['all']: what = "all devices" _, total_count, _ = inventory_selector(nr, resync=kwargs['resync']) else: return empty_result( status='error', data=f"No devices to synchronize were specified"), 400 scheduler = Scheduler() job_id = scheduler.add_onetime_job( 'cnaas_nms.confpush.sync_devices:sync_devices', when=1, scheduled_by=get_jwt_identity(), kwargs=kwargs) res = empty_result(data=f"Scheduled job to synchronize {what}") res['job_id'] = job_id resp = make_response(json.dumps(res), 200) if total_count: resp.headers['X-Total-Count'] = total_count resp.headers['Content-Type'] = "application/json" return resp
def post(self): """ Add a new linknet """ json_data = request.get_json() data = {} errors = [] 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_linknet = Linknet.create_linknet( 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() except Exception as e: session.rollback() return empty_result(status='error', data=str(e)), 500 return empty_result(status='success', data=data), 201