diff --git a/proto/context.proto b/proto/context.proto index 3550fb1808d7c0e2c8aab78d369ab2b0e29d5e09..7d6ba1bc14bf66b8018fec421af49c54e124494b 100644 --- a/proto/context.proto +++ b/proto/context.proto @@ -260,6 +260,7 @@ enum DeviceDriverEnum { DEVICEDRIVER_GNMI_NOKIA_SRLINUX = 19; DEVICEDRIVER_OPENROADM = 20; DEVICEDRIVER_RESTCONF_OPENCONFIG = 21; + DEVICEDRIVER_CUSTOM_IPOWDM = 22; } enum DeviceOperationalStatusEnum { diff --git a/src/common/type_checkers/Assertions.py b/src/common/type_checkers/Assertions.py index 2fa279de593e4edc09d93627219e24c49b7d5fd0..51ca9f7aadf65f9e93715e0f48ce9ff7151a16a5 100644 --- a/src/common/type_checkers/Assertions.py +++ b/src/common/type_checkers/Assertions.py @@ -59,7 +59,8 @@ def validate_device_driver_enum(message): 'DEVICEDRIVER_RYU', 'DEVICEDRIVER_GNMI_NOKIA_SRLINUX', 'DEVICEDRIVER_OPENROADM', - 'DEVICEDRIVER_GNMI_OPENCONFIG', + 'DEVICEDRIVER_RESTCONF_OPENCONFIG', + 'DEVICEDRIVER_CUSTOM_IPOWDM', ] def validate_device_operational_status_enum(message): diff --git a/src/context/service/database/models/enums/DeviceDriver.py b/src/context/service/database/models/enums/DeviceDriver.py index 5c228c87ebe2ce5db8140d59716bc960c0184a81..d029af443eabf315819dd13f479dee8c158c02a0 100644 --- a/src/context/service/database/models/enums/DeviceDriver.py +++ b/src/context/service/database/models/enums/DeviceDriver.py @@ -44,6 +44,7 @@ class ORM_DeviceDriverEnum(enum.Enum): GNMI_NOKIA_SRLINUX = DeviceDriverEnum.DEVICEDRIVER_GNMI_NOKIA_SRLINUX OPENROADM = DeviceDriverEnum.DEVICEDRIVER_OPENROADM RESTCONF_OPENCONFIG = DeviceDriverEnum.DEVICEDRIVER_RESTCONF_OPENCONFIG + CUSTOM_IPOWDM = DeviceDriverEnum.DEVICEDRIVER_CUSTOM_IPOWDM grpc_to_enum__device_driver = functools.partial( grpc_to_enum, DeviceDriverEnum, ORM_DeviceDriverEnum) diff --git a/src/device/service/drivers/__init__.py b/src/device/service/drivers/__init__.py index 85ea4bef06a1e3facaaa583d4b9be63efac3da34..7c4338162ef59c8958c4127fea5d75ce66c07b1c 100644 --- a/src/device/service/drivers/__init__.py +++ b/src/device/service/drivers/__init__.py @@ -270,3 +270,14 @@ if LOAD_ALL_DEVICE_DRIVERS: FilterFieldEnum.DRIVER : DeviceDriverEnum.DEVICEDRIVER_RESTCONF_OPENCONFIG, } ])) +if LOAD_ALL_DEVICE_DRIVERS: + from .custom_ipowdm.CustomIpowdmDriver import CustomIpowdmDriver # pylint: disable=wrong-import-position + DRIVERS.append( + (CustomIpowdmDriver, [ + { + FilterFieldEnum.DEVICE_TYPE: [ + DeviceTypeEnum.PACKET_ROUTER + ], + FilterFieldEnum.DRIVER : DeviceDriverEnum.DEVICEDRIVER_CUSTOM_IPOWDM, + } + ])) diff --git a/src/device/service/drivers/custom_ipowdm/Constants.py b/src/device/service/drivers/custom_ipowdm/Constants.py new file mode 100644 index 0000000000000000000000000000000000000000..51ac1c483436d8daafed474b4c239f980ae9d496 --- /dev/null +++ b/src/device/service/drivers/custom_ipowdm/Constants.py @@ -0,0 +1,21 @@ +# Copyright 2022-2026 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from device.service.driver_api._Driver import RESOURCE_ENDPOINTS, RESOURCE_INTERFACES, RESOURCE_NETWORK_INSTANCES + +SPECIAL_RESOURCE_MAPPINGS = { + RESOURCE_ENDPOINTS : '/endpoints', + RESOURCE_INTERFACES : '/interfaces', + RESOURCE_NETWORK_INSTANCES: '/net-instances', +} diff --git a/src/device/service/drivers/custom_ipowdm/CustomIpowdmDriver.py b/src/device/service/drivers/custom_ipowdm/CustomIpowdmDriver.py new file mode 100644 index 0000000000000000000000000000000000000000..f1f583382fc67d3dd875e6dbcfe84dd1087fee16 --- /dev/null +++ b/src/device/service/drivers/custom_ipowdm/CustomIpowdmDriver.py @@ -0,0 +1,334 @@ +# Copyright 2022-2026 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import anytree, json, logging, pytz, queue, re, threading +from datetime import datetime, timedelta +from typing import Any, Iterator, List, Optional, Tuple, Union +from apscheduler.executors.pool import ThreadPoolExecutor +from apscheduler.job import Job +from apscheduler.jobstores.memory import MemoryJobStore +from apscheduler.schedulers.background import BackgroundScheduler +from common.method_wrappers.Decorator import MetricsPool, metered_subclass_method +from common.type_checkers.Checkers import chk_float, chk_length, chk_string, chk_type +from device.service.driver_api._Driver import _Driver +from device.service.driver_api.AnyTreeTools import TreeNode, dump_subtree, get_subnode, set_subnode_value +from .Constants import SPECIAL_RESOURCE_MAPPINGS +from .SyntheticSamplingParameters import SyntheticSamplingParameters, do_sampling +from .Tools import compose_resource_endpoint, connect_to_xr_agent +import requests + + +LOGGER = logging.getLogger(__name__) + +RE_GET_ENDPOINT_FROM_INTERFACE = re.compile(r'^\/interface\[([^\]]+)\].*') + +DRIVER_NAME = 'custom_ipowdm' +METRICS_POOL = MetricsPool('Device', 'Driver', labels={'driver': DRIVER_NAME}) + +class CustomIpowdmDriver(_Driver): + def __init__(self, address : str, port : int, **settings) -> None: + super().__init__(DRIVER_NAME, address, port, **settings) + self.__lock = threading.Lock() + self.__address = address + self.__initial = TreeNode('.') + self.__running = TreeNode('.') + self.__subscriptions = TreeNode('.') + + endpoints = self.settings.get('endpoints', []) + endpoint_resources = [] + for endpoint in endpoints: + endpoint_resource = compose_resource_endpoint(endpoint) + if endpoint_resource is None: continue + endpoint_resources.append(endpoint_resource) + self.SetConfig(endpoint_resources) + + self.__started = threading.Event() + self.__terminate = threading.Event() + self.__scheduler = BackgroundScheduler(daemon=True) # scheduler used to emulate sampling events + self.__scheduler.configure( + jobstores = {'default': MemoryJobStore()}, + executors = {'default': ThreadPoolExecutor(max_workers=1)}, + job_defaults = {'coalesce': False, 'max_instances': 3}, + timezone=pytz.utc) + self.__out_samples = queue.Queue() + self.__synthetic_sampling_parameters = SyntheticSamplingParameters() + + def Connect(self) -> bool: + # If started, assume it is already connected + if self.__started.is_set(): return True + + # Connect triggers activation of sampling events that will be scheduled based on subscriptions + self.__scheduler.start() + + # Indicate the driver is now connected to the device + self.__started.set() + return True + + def Disconnect(self) -> bool: + # Trigger termination of loops and processes + self.__terminate.set() + + # If not started, assume it is already disconnected + if not self.__started.is_set(): return True + + # Disconnect triggers deactivation of sampling events + self.__scheduler.shutdown() + return True + + @metered_subclass_method(METRICS_POOL) + def GetInitialConfig(self) -> List[Tuple[str, Any]]: + with self.__lock: + return dump_subtree(self.__initial) + + @metered_subclass_method(METRICS_POOL) + def GetConfig(self, resource_keys : List[str] = []) -> List[Tuple[str, Union[Any, None, Exception]]]: + chk_type('resources', resource_keys, list) + with self.__lock: + if len(resource_keys) == 0: return dump_subtree(self.__running) + results = [] + resolver = anytree.Resolver(pathattr='name') + for i,resource_key in enumerate(resource_keys): + str_resource_name = 'resource_key[#{:d}]'.format(i) + try: + chk_string(str_resource_name, resource_key, allow_empty=False) + resource_key = SPECIAL_RESOURCE_MAPPINGS.get(resource_key, resource_key) + resource_path = resource_key.split('/') + except Exception as e: # pylint: disable=broad-except + LOGGER.exception('Exception validating {:s}: {:s}'.format(str_resource_name, str(resource_key))) + results.append((resource_key, e)) # if validation fails, store the exception + continue + + resource_node = get_subnode(resolver, self.__running, resource_path, default=None) + # if not found, resource_node is None + if resource_node is None: continue + results.extend(dump_subtree(resource_node)) + return results + + @metered_subclass_method(METRICS_POOL) + def SetConfig(self, resources : List[Tuple[str, Any]]) -> List[Union[bool, Exception]]: + chk_type('resources', resources, list) + if len(resources) == 0: return [] + results = [] + if 'ipowdm_ruleset' in str(resources): + connect_to_xr_agent(resources) + results.append(True) + else: + resolver = anytree.Resolver(pathattr='name') + with self.__lock: + for i,resource in enumerate(resources): + str_resource_name = 'resources[#{:d}]'.format(i) + try: + chk_type(str_resource_name, resource, (list, tuple)) + chk_length(str_resource_name, resource, min_length=2, max_length=2) + resource_key,resource_value = resource + chk_string(str_resource_name, resource_key, allow_empty=False) + resource_path = resource_key.split('/') + except Exception as e: # pylint: disable=broad-except + LOGGER.exception('Exception validating {:s}: {:s}'.format(str_resource_name, str(resource_key))) + results.append(e) # if validation fails, store the exception + continue + try: + resource_value = json.loads(resource_value) + except: # pylint: disable=bare-except + pass + + if resource_key.startswith('/ipowdm/service/'): + LOGGER.info('[%s] Legacy IPoWDM Service Provisioning: Payload=%s', self.__address, str(resource_value)) + elif resource_key.startswith('/ipowdm/l3nm/'): + LOGGER.info('[%s] L3NM Service Provisioning: Payload=%s', self.__address, str(resource_value)) + elif resource_key.startswith('/ipowdm/pluggables/'): + LOGGER.info('[%s] Pluggables Service Provisioning: Payload=%s', self.__address, str(resource_value)) + try: + key_parts = resource_key.split('/') + if len(key_parts) >= 5: + service_uuid_raw = key_parts[3] + # Normalize UUID: if serviceId contains '-pluggable-', take the part before it + if "-pluggable-" in service_uuid_raw: + service_uuid = service_uuid_raw.split("-pluggable-")[0] + else: + service_uuid = service_uuid_raw + + if not hasattr(EmulatedDriver, 'pluggables_pending'): + EmulatedDriver.pluggables_pending = {} + + if service_uuid in EmulatedDriver.pluggables_pending: + stored_payload = EmulatedDriver.pluggables_pending.pop(service_uuid) + current_payload = resource_value + + def format_entry(payload): + config = payload.get('config', {}) + return { + 'uuid': payload.get('device'), + 'power': config.get('target-output-power', 0.0), + 'frequency': config.get('frequency', 0.0) + } + combined_data = [ + {'src': format_entry(stored_payload)}, + {'dst': format_entry(current_payload)} + ] + LOGGER.info('[%s] Pluggables Service Provisioning Aggregated: %s', self.__address, json.dumps(combined_data, indent=2)) + # TODO Dynamic IP + url = "http://192.168.88.17:9849/api-v0/transponders" + headers = {'Content-Type': 'application/json'} + response = requests.post(url, json=combined_data, headers=headers) + LOGGER.info('[%s] Pluggables Service Provisioning Response: %s', self.__address, str(response.text)) + else: + EmulatedDriver.pluggables_pending[service_uuid] = resource_value + LOGGER.debug('[%s] Pluggables Service Partial Provisioning stored for %s', self.__address, service_uuid) + except Exception as e: + LOGGER.warning("Error processing Pluggables aggregation: %s", str(e)) + LOGGER.info('[%s] Pluggables Service Provisioning: Payload=%s', self.__address, str(resource_value)) + + set_subnode_value(resolver, self.__running, resource_path, resource_value) + + match = RE_GET_ENDPOINT_FROM_INTERFACE.match(resource_key) + if match is not None: + endpoint_uuid = match.group(1) + if '.' in endpoint_uuid: endpoint_uuid = endpoint_uuid.split('.')[0] + self.__synthetic_sampling_parameters.set_endpoint_configured(endpoint_uuid) + + results.append(True) + return results + + @metered_subclass_method(METRICS_POOL) + def DeleteConfig(self, resources : List[Tuple[str, Any]]) -> List[Union[bool, Exception]]: + chk_type('resources', resources, list) + if len(resources) == 0: return [] + results = [] + resolver = anytree.Resolver(pathattr='name') + with self.__lock: + for i,resource in enumerate(resources): + str_resource_name = 'resources[#{:d}]'.format(i) + try: + chk_type(str_resource_name, resource, (list, tuple)) + chk_length(str_resource_name, resource, min_length=2, max_length=2) + resource_key,_ = resource + chk_string(str_resource_name, resource_key, allow_empty=False) + resource_path = resource_key.split('/') + except Exception as e: # pylint: disable=broad-except + LOGGER.exception('Exception validating {:s}: {:s}'.format(str_resource_name, str(resource_key))) + results.append(e) # if validation fails, store the exception + continue + + resource_node = get_subnode(resolver, self.__running, resource_path, default=None) + # if not found, resource_node is None + if resource_node is None: + results.append(False) + continue + + match = RE_GET_ENDPOINT_FROM_INTERFACE.match(resource_key) + if match is not None: + endpoint_uuid = match.group(1) + if '.' in endpoint_uuid: endpoint_uuid = endpoint_uuid.split('.')[0] + self.__synthetic_sampling_parameters.unset_endpoint_configured(endpoint_uuid) + + parent = resource_node.parent + children = list(parent.children) + children.remove(resource_node) + parent.children = tuple(children) + results.append(True) + return results + + @metered_subclass_method(METRICS_POOL) + def SubscribeState(self, subscriptions : List[Tuple[str, float, float]]) -> List[Union[bool, Exception]]: + chk_type('subscriptions', subscriptions, list) + if len(subscriptions) == 0: return [] + results = [] + resolver = anytree.Resolver(pathattr='name') + with self.__lock: + for i,subscription in enumerate(subscriptions): + str_subscription_name = 'subscriptions[#{:d}]'.format(i) + try: + chk_type(str_subscription_name, subscription, (list, tuple)) + chk_length(str_subscription_name, subscription, min_length=3, max_length=3) + resource_key,sampling_duration,sampling_interval = subscription + chk_string(str_subscription_name + '.resource_key', resource_key, allow_empty=False) + resource_path = resource_key.split('/') + chk_float(str_subscription_name + '.sampling_duration', sampling_duration, min_value=0) + chk_float(str_subscription_name + '.sampling_interval', sampling_interval, min_value=0) + except Exception as e: # pylint: disable=broad-except + LOGGER.exception('Exception validating {:s}: {:s}'.format(str_subscription_name, str(resource_key))) + results.append(e) # if validation fails, store the exception + continue + + start_date,end_date = None,None + if sampling_duration <= 1.e-12: + start_date = datetime.utcnow() + end_date = start_date + timedelta(seconds=sampling_duration) + + job_id = 'k={:s}/d={:f}/i={:f}'.format(resource_key, sampling_duration, sampling_interval) + job = self.__scheduler.add_job( + do_sampling, args=(self.__synthetic_sampling_parameters, resource_key, self.__out_samples), + kwargs={}, id=job_id, trigger='interval', seconds=sampling_interval, start_date=start_date, + end_date=end_date, timezone=pytz.utc) + + subscription_path = resource_path + ['{:.3f}:{:.3f}'.format(sampling_duration, sampling_interval)] + set_subnode_value(resolver, self.__subscriptions, subscription_path, job) + results.append(True) + return results + + @metered_subclass_method(METRICS_POOL) + def UnsubscribeState(self, subscriptions : List[Tuple[str, float, float]]) -> List[Union[bool, Exception]]: + chk_type('subscriptions', subscriptions, list) + if len(subscriptions) == 0: return [] + results = [] + resolver = anytree.Resolver(pathattr='name') + with self.__lock: + for i,resource in enumerate(subscriptions): + str_subscription_name = 'resources[#{:d}]'.format(i) + try: + chk_type(str_subscription_name, resource, (list, tuple)) + chk_length(str_subscription_name, resource, min_length=3, max_length=3) + resource_key,sampling_duration,sampling_interval = resource + chk_string(str_subscription_name + '.resource_key', resource_key, allow_empty=False) + resource_path = resource_key.split('/') + chk_float(str_subscription_name + '.sampling_duration', sampling_duration, min_value=0) + chk_float(str_subscription_name + '.sampling_interval', sampling_interval, min_value=0) + except Exception as e: # pylint: disable=broad-except + LOGGER.exception('Exception validating {:s}: {:s}'.format(str_subscription_name, str(resource_key))) + results.append(e) # if validation fails, store the exception + continue + + subscription_path = resource_path + ['{:.3f}:{:.3f}'.format(sampling_duration, sampling_interval)] + subscription_node = get_subnode(resolver, self.__subscriptions, subscription_path) + + # if not found, resource_node is None + if subscription_node is None: + results.append(False) + continue + + job : Job = getattr(subscription_node, 'value', None) + if job is None or not isinstance(job, Job): + raise Exception('Malformed subscription node or wrong resource key: {:s}'.format(str(resource))) + job.remove() + + parent = subscription_node.parent + children = list(parent.children) + children.remove(subscription_node) + parent.children = tuple(children) + + results.append(True) + return results + + def GetState(self, blocking=False, terminate : Optional[threading.Event] = None) -> Iterator[Tuple[str, Any]]: + while True: + if self.__terminate.is_set(): break + if terminate is not None and terminate.is_set(): break + try: + sample = self.__out_samples.get(block=blocking, timeout=0.1) + except queue.Empty: + if blocking: continue + return + if sample is None: continue + yield sample diff --git a/src/device/service/drivers/custom_ipowdm/SyntheticSamplingParameters.py b/src/device/service/drivers/custom_ipowdm/SyntheticSamplingParameters.py new file mode 100644 index 0000000000000000000000000000000000000000..52584a3454e1d7134e5f79c3d765a130041f664e --- /dev/null +++ b/src/device/service/drivers/custom_ipowdm/SyntheticSamplingParameters.py @@ -0,0 +1,86 @@ +# Copyright 2022-2026 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging, math, queue, random, re, threading +from datetime import datetime +from typing import Optional, Tuple + +LOGGER = logging.getLogger(__name__) + +RE_GET_ENDPOINT_METRIC = re.compile(r'.*\/endpoint\[([^\]]+)\]\/state\/(.*)') + +MSG_ERROR_PARSE = '[get] unable to extract endpoint-metric from monitoring_resource_key "{:s}"' +MSG_INFO = '[get] monitoring_resource_key={:s}, endpoint_uuid={:s}, metric={:s}, metric_sense={:s}' + +class SyntheticSamplingParameters: + def __init__(self) -> None: + self.__lock = threading.Lock() + self.__data = {} + self.__configured_endpoints = set() + + def set_endpoint_configured(self, endpoint_uuid : str): + with self.__lock: + self.__configured_endpoints.add(endpoint_uuid) + + def unset_endpoint_configured(self, endpoint_uuid : str): + with self.__lock: + self.__configured_endpoints.discard(endpoint_uuid) + + def get(self, monitoring_resource_key : str) -> Optional[Tuple[float, float, float, float, float]]: + with self.__lock: + match = RE_GET_ENDPOINT_METRIC.match(monitoring_resource_key) + if match is None: + LOGGER.error(MSG_ERROR_PARSE.format(monitoring_resource_key)) + return None + endpoint_uuid = match.group(1) + + # If endpoint is not configured, generate a flat synthetic traffic aligned at 0 + if endpoint_uuid not in self.__configured_endpoints: return (0, 0, 1, 0, 0) + + metric = match.group(2) + metric_sense = metric.lower().replace('packets_', '').replace('bytes_', '') + + LOGGER.debug(MSG_INFO.format(monitoring_resource_key, endpoint_uuid, metric, metric_sense)) + + parameters_key = '{:s}-{:s}'.format(endpoint_uuid, metric_sense) + parameters = self.__data.get(parameters_key) + if parameters is not None: return parameters + + # assume packets + amplitude = 1.e7 * random.random() + phase = 60 * random.random() + period = 3600 * random.random() + offset = 1.e8 * random.random() + amplitude + avg_bytes_per_packet = random.randint(500, 1500) + parameters = (amplitude, phase, period, offset, avg_bytes_per_packet) + return self.__data.setdefault(parameters_key, parameters) + +def do_sampling( + synthetic_sampling_parameters : SyntheticSamplingParameters, monitoring_resource_key : str, + out_samples : queue.Queue +) -> None: + parameters = synthetic_sampling_parameters.get(monitoring_resource_key) + if parameters is None: return + amplitude, phase, period, offset, avg_bytes_per_packet = parameters + + if 'bytes' in monitoring_resource_key.lower(): + # convert to bytes + amplitude = avg_bytes_per_packet * amplitude + offset = avg_bytes_per_packet * offset + + timestamp = datetime.timestamp(datetime.utcnow()) + waveform = amplitude * math.sin(2 * math.pi * timestamp / period + phase) + offset + noise = amplitude * random.random() + value = abs(0.95 * waveform + 0.05 * noise) + out_samples.put_nowait((timestamp, monitoring_resource_key, value)) diff --git a/src/device/service/drivers/custom_ipowdm/Tools.py b/src/device/service/drivers/custom_ipowdm/Tools.py new file mode 100644 index 0000000000000000000000000000000000000000..8cfef9c323d1fcd717e47fd0e0ea7d6f126b8f17 --- /dev/null +++ b/src/device/service/drivers/custom_ipowdm/Tools.py @@ -0,0 +1,134 @@ +# Copyright 2022-2026 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +import logging +import requests +from common.proto.kpi_sample_types_pb2 import KpiSampleType +from common.type_checkers.Checkers import chk_attribute, chk_string, chk_type +from device.service.driver_api._Driver import RESOURCE_ENDPOINTS +from .Constants import SPECIAL_RESOURCE_MAPPINGS +from typing import Any, Dict, Optional, Tuple + +LOGGER = logging.getLogger(__name__) + +def process_optional_string_field( + endpoint_data : Dict[str, Any], field_name : str, endpoint_resource_value : Dict[str, Any] +) -> None: + field_value = chk_attribute(field_name, endpoint_data, 'endpoint_data', default=None) + if field_value is None: return + chk_string('endpoint_data.{:s}'.format(field_name), field_value) + if len(field_value) > 0: endpoint_resource_value[field_name] = field_value + +def compose_resource_endpoint(endpoint_data : Dict[str, Any]) -> Optional[Tuple[str, Dict]]: + try: + # Check type of endpoint_data + chk_type('endpoint_data', endpoint_data, dict) + + # Check endpoint UUID (mandatory) + endpoint_uuid = chk_attribute('uuid', endpoint_data, 'endpoint_data') + chk_string('endpoint_data.uuid', endpoint_uuid, min_length=1) + endpoint_resource_path = SPECIAL_RESOURCE_MAPPINGS.get(RESOURCE_ENDPOINTS) + endpoint_resource_key = '{:s}/endpoint[{:s}]'.format(endpoint_resource_path, endpoint_uuid) + endpoint_resource_value = {'uuid': endpoint_uuid} + + # Check endpoint optional string fields + process_optional_string_field(endpoint_data, 'name', endpoint_resource_value) + process_optional_string_field(endpoint_data, 'type', endpoint_resource_value) + process_optional_string_field(endpoint_data, 'context_uuid', endpoint_resource_value) + process_optional_string_field(endpoint_data, 'topology_uuid', endpoint_resource_value) + + # Check endpoint sample types (optional) + endpoint_sample_types = chk_attribute('sample_types', endpoint_data, 'endpoint_data', default=[]) + chk_type('endpoint_data.sample_types', endpoint_sample_types, list) + sample_types = {} + sample_type_errors = [] + for i,endpoint_sample_type in enumerate(endpoint_sample_types): + field_name = 'endpoint_data.sample_types[{:d}]'.format(i) + try: + chk_type(field_name, endpoint_sample_type, (int, str)) + if isinstance(endpoint_sample_type, int): + metric_name = KpiSampleType.Name(endpoint_sample_type) + metric_id = endpoint_sample_type + elif isinstance(endpoint_sample_type, str): + metric_id = KpiSampleType.Value(endpoint_sample_type) + metric_name = endpoint_sample_type + else: + str_type = str(type(endpoint_sample_type)) + raise Exception('Bad format: {:s}'.format(str_type)) # pylint: disable=broad-exception-raised + except Exception as e: # pylint: disable=broad-exception-caught + MSG = 'Unsupported {:s}({:s}) : {:s}' + sample_type_errors.append(MSG.format(field_name, str(endpoint_sample_type), str(e))) + + metric_name = metric_name.lower().replace('kpisampletype_', '') + monitoring_resource_key = '{:s}/state/{:s}'.format(endpoint_resource_key, metric_name) + sample_types[metric_id] = monitoring_resource_key + + if len(sample_type_errors) > 0: + # pylint: disable=broad-exception-raised + raise Exception('Malformed Sample Types:\n{:s}'.format('\n'.join(sample_type_errors))) + + if len(sample_types) > 0: + endpoint_resource_value['sample_types'] = sample_types + + if 'location' in endpoint_data: + endpoint_resource_value['location'] = endpoint_data['location'] + + if "site_location" in endpoint_data: + endpoint_resource_value["site_location"] = endpoint_data["site_location"] + + if "ce-ip" in endpoint_data: + endpoint_resource_value["ce-ip"] = endpoint_data["ce-ip"] + + if "address_ip" in endpoint_data: + endpoint_resource_value["address_ip"] = endpoint_data["address_ip"] + + if "address_prefix" in endpoint_data: + endpoint_resource_value["address_prefix"] = endpoint_data["address_prefix"] + + if "mtu" in endpoint_data: + endpoint_resource_value["mtu"] = endpoint_data["mtu"] + + if "ipv4_lan_prefixes" in endpoint_data: + endpoint_resource_value["ipv4_lan_prefixes"] = endpoint_data[ + "ipv4_lan_prefixes" + ] + + return endpoint_resource_key, endpoint_resource_value + except: # pylint: disable=bare-except + LOGGER.exception('Problem composing endpoint({:s})'.format(str(endpoint_data))) + return None + +# TODO Dynamic IP +def connect_to_xr_agent(resources): + rule_set = resources[0][1]['rule_set'] + nodes = [ + {'src': { + 'uuid': rule_set['src'][0]['uuid'], + 'power': rule_set['src'][0]['power'], + 'frequency': rule_set['src'][0]['frequency'] + }}, + {'dst': { + 'uuid': rule_set['dst'][0]['uuid'], + 'power': rule_set['dst'][0]['power'], + 'frequency': rule_set['dst'][0]['frequency'] + }} + ] + url = "http://192.168.88.17:9849/api-v0/transponders" + headers = { + "Content-Type": "application/json", + "Expect": "" + } + json_data = json.dumps(nodes) + requests.post(url, data=json_data, headers=headers, timeout=10) diff --git a/src/device/service/drivers/custom_ipowdm/__init__.py b/src/device/service/drivers/custom_ipowdm/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..b53987a4eae1aed245eba5c7ddd8cd10e35919c2 --- /dev/null +++ b/src/device/service/drivers/custom_ipowdm/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2022-2026 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/src/device/service/drivers/ietf_l2vpn/TfsApiClient.py b/src/device/service/drivers/ietf_l2vpn/TfsApiClient.py index aeb4132bd0229c8e2129908a952425d74f95ce33..88ec07a09bc9427b18b2a913914b03b1028e4e4b 100644 --- a/src/device/service/drivers/ietf_l2vpn/TfsApiClient.py +++ b/src/device/service/drivers/ietf_l2vpn/TfsApiClient.py @@ -51,6 +51,7 @@ MAPPING_DRIVER = { 'DEVICEDRIVER_GNMI_NOKIA_SRLINUX' : 19, 'DEVICEDRIVER_OPENROADM' : 20, 'DEVICEDRIVER_RESTCONF_OPENCONFIG' : 21, + 'DEVICEDRIVER_CUSTOM_IPOWDM' : 22, } diff --git a/src/device/service/drivers/ietf_l3vpn/TfsApiClient.py b/src/device/service/drivers/ietf_l3vpn/TfsApiClient.py index 1ada5ddef3d92afe7de7fbd0600bd3fbf23eb371..947b3ca5c3c41aa4f928044fe834f76fd778f136 100644 --- a/src/device/service/drivers/ietf_l3vpn/TfsApiClient.py +++ b/src/device/service/drivers/ietf_l3vpn/TfsApiClient.py @@ -56,6 +56,7 @@ MAPPING_DRIVER = { 'DEVICEDRIVER_GNMI_NOKIA_SRLINUX' : 19, 'DEVICEDRIVER_OPENROADM' : 20, 'DEVICEDRIVER_RESTCONF_OPENCONFIG' : 21, + 'DEVICEDRIVER_CUSTOM_IPOWDM' : 22, } diff --git a/src/device/service/drivers/ietf_slice/TfsApiClient.py b/src/device/service/drivers/ietf_slice/TfsApiClient.py index 9cee9a03bd717c743b0984714d6ce5278db2a4ee..3a0261901c2fd176d09f44e7540ddb1b273f9f1a 100644 --- a/src/device/service/drivers/ietf_slice/TfsApiClient.py +++ b/src/device/service/drivers/ietf_slice/TfsApiClient.py @@ -57,6 +57,7 @@ MAPPING_DRIVER = { 'DEVICEDRIVER_GNMI_NOKIA_SRLINUX' : 19, 'DEVICEDRIVER_OPENROADM' : 20, 'DEVICEDRIVER_RESTCONF_OPENCONFIG' : 21, + 'DEVICEDRIVER_CUSTOM_IPOWDM' : 22, } diff --git a/src/device/service/drivers/optical_tfs/TfsApiClient.py b/src/device/service/drivers/optical_tfs/TfsApiClient.py index 4cf97f73c353690e08ede8f0fe215bdc7c0f20f4..5e1ece17f9645492a38463cada82dc9c38f043cf 100644 --- a/src/device/service/drivers/optical_tfs/TfsApiClient.py +++ b/src/device/service/drivers/optical_tfs/TfsApiClient.py @@ -60,6 +60,7 @@ MAPPING_DRIVER = { 'DEVICEDRIVER_GNMI_NOKIA_SRLINUX' : 19, 'DEVICEDRIVER_OPENROADM' : 20, 'DEVICEDRIVER_RESTCONF_OPENCONFIG' : 21, + 'DEVICEDRIVER_CUSTOM_IPOWDM' : 22, } diff --git a/src/device/service/drivers/transport_api/TfsApiClient.py b/src/device/service/drivers/transport_api/TfsApiClient.py index e8962516e787b9f1382beb651f27115bb66119ec..76cacb98649d41c8e3c0dff5df50e5aa556abbc4 100644 --- a/src/device/service/drivers/transport_api/TfsApiClient.py +++ b/src/device/service/drivers/transport_api/TfsApiClient.py @@ -47,6 +47,10 @@ MAPPING_DRIVER = { 'DEVICEDRIVER_SMARTNIC' : 16, 'DEVICEDRIVER_MORPHEUS' : 17, 'DEVICEDRIVER_RYU' : 18, + 'DEVICEDRIVER_GNMI_NOKIA_SRLINUX' : 19, + 'DEVICEDRIVER_OPENROADM' : 20, + 'DEVICEDRIVER_RESTCONF_OPENCONFIG' : 21, + 'DEVICEDRIVER_CUSTOM_IPOWDM' : 22, } LOGGER = logging.getLogger(__name__) diff --git a/src/nbi/service/ipowdm/Resources.py b/src/nbi/service/ipowdm/Resources.py index dbe1dd3ccb5ca60d0825284492a4a5b044eb382e..945c6ab6b7b7a31b17b2dc5cd6efe03efc3338a4 100644 --- a/src/nbi/service/ipowdm/Resources.py +++ b/src/nbi/service/ipowdm/Resources.py @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/src/nbi/service/ipowdm/__init__.py b/src/nbi/service/ipowdm/__init__.py index 39cadfb1464e3b02fe0075f4230ae739610aa05d..3bec4e132f7a9a398e74091629256428dc5075c8 100644 --- a/src/nbi/service/ipowdm/__init__.py +++ b/src/nbi/service/ipowdm/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/src/policy/src/main/java/org/etsi/tfs/policy/Serializer.java b/src/policy/src/main/java/org/etsi/tfs/policy/Serializer.java index 1dce54e76307f0819e88e7fc87fd3d0eecf2650f..5639a2059235791127826a114b5f47842ce78e08 100644 --- a/src/policy/src/main/java/org/etsi/tfs/policy/Serializer.java +++ b/src/policy/src/main/java/org/etsi/tfs/policy/Serializer.java @@ -1752,6 +1752,8 @@ public class Serializer { return ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_OPENROADM; case RESTCONF_OPENCONFIG: return ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_RESTCONF_OPENCONFIG; + case CUSTOM_IPOWDM: + return ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_CUSTOM_IPOWDM; case UNDEFINED: default: return ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_UNDEFINED; @@ -1803,6 +1805,8 @@ public class Serializer { return DeviceDriverEnum.OPENROADM; case DEVICEDRIVER_RESTCONF_OPENCONFIG: return DeviceDriverEnum.RESTCONF_OPENCONFIG; + case DEVICEDRIVER_CUSTOM_IPOWDM: + return DeviceDriverEnum.CUSTOM_IPOWDM; case DEVICEDRIVER_UNDEFINED: case UNRECOGNIZED: default: diff --git a/src/policy/src/main/java/org/etsi/tfs/policy/context/model/DeviceDriverEnum.java b/src/policy/src/main/java/org/etsi/tfs/policy/context/model/DeviceDriverEnum.java index 3d45ee265341211b7c7f56158f731bc7263188e4..3e1ae08e888d39fbe964a58b0fa711e00d5e2211 100644 --- a/src/policy/src/main/java/org/etsi/tfs/policy/context/model/DeviceDriverEnum.java +++ b/src/policy/src/main/java/org/etsi/tfs/policy/context/model/DeviceDriverEnum.java @@ -38,5 +38,6 @@ public enum DeviceDriverEnum { RYU, GNMI_NOKIA_SRLINUX, OPENROADM, - RESTCONF_OPENCONFIG + RESTCONF_OPENCONFIG, + CUSTOM_IPOWDM } diff --git a/src/policy/src/test/java/org/etsi/tfs/policy/SerializerTest.java b/src/policy/src/test/java/org/etsi/tfs/policy/SerializerTest.java index 72c6cfffdd134458c39c1f9d4cfbda82c03ce831..8ae266365013e2950efc5fbf01485d6346ba2661 100644 --- a/src/policy/src/test/java/org/etsi/tfs/policy/SerializerTest.java +++ b/src/policy/src/test/java/org/etsi/tfs/policy/SerializerTest.java @@ -2641,6 +2641,12 @@ class SerializerTest { ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_OPTICAL_TFS), Arguments.of( DeviceDriverEnum.IETF_ACTN, ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_IETF_ACTN), + Arguments.of( + DeviceDriverEnum.RESTCONF_OPENCONFIG, + ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_RESTCONF_OPENCONFIG), + Arguments.of( + DeviceDriverEnum.CUSTOM_IPOWDM, + ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_CUSTOM_IPOWDM), Arguments.of( DeviceDriverEnum.UNDEFINED, ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_UNDEFINED)); } diff --git a/src/policy/target/generated-sources/grpc/context/ContextOuterClass.java b/src/policy/target/generated-sources/grpc/context/ContextOuterClass.java index 27cde66fb4d01c41c0ae39bba4f4da897b2cd0c5..7ab7bca1180dfffef6fcaf89412251ed7612caf5 100644 --- a/src/policy/target/generated-sources/grpc/context/ContextOuterClass.java +++ b/src/policy/target/generated-sources/grpc/context/ContextOuterClass.java @@ -235,6 +235,10 @@ public final class ContextOuterClass { * DEVICEDRIVER_RESTCONF_OPENCONFIG = 21; */ DEVICEDRIVER_RESTCONF_OPENCONFIG(21), + /** + * DEVICEDRIVER_CUSTOM_IPOWDM = 22; + */ + DEVICEDRIVER_CUSTOM_IPOWDM(22), UNRECOGNIZED(-1); /** @@ -351,6 +355,11 @@ public final class ContextOuterClass { */ public static final int DEVICEDRIVER_RESTCONF_OPENCONFIG_VALUE = 21; + /** + * DEVICEDRIVER_CUSTOM_IPOWDM = 22; + */ + public static final int DEVICEDRIVER_CUSTOM_IPOWDM_VALUE = 22; + public final int getNumber() { if (this == UNRECOGNIZED) { throw new java.lang.IllegalArgumentException("Can't get the number of an unknown enum value."); @@ -418,6 +427,8 @@ public final class ContextOuterClass { return DEVICEDRIVER_OPENROADM; case 21: return DEVICEDRIVER_RESTCONF_OPENCONFIG; + case 22: + return DEVICEDRIVER_CUSTOM_IPOWDM; default: return null; } diff --git a/src/webui/service/device/routes.py b/src/webui/service/device/routes.py index e1cfb000a97e46e6ccda2ea4eb5c16dac2b8ea08..3eac49150e2e9ecf91b9a3df26924b204155baeb 100644 --- a/src/webui/service/device/routes.py +++ b/src/webui/service/device/routes.py @@ -155,6 +155,8 @@ def add(): device_drivers.append(DeviceDriverEnum.DEVICEDRIVER_OPENROADM) if form.device_drivers_restconf_openconfig.data: device_drivers.append(DeviceDriverEnum.DEVICEDRIVER_RESTCONF_OPENCONFIG) + if form.device_drivers_custom_ipowdm.data: + device_drivers.append(DeviceDriverEnum.DEVICEDRIVER_CUSTOM_IPOWDM) device_obj.device_drivers.extend(device_drivers) # pylint: disable=no-member try: diff --git a/src/ztp/src/main/java/org/etsi/tfs/ztp/Serializer.java b/src/ztp/src/main/java/org/etsi/tfs/ztp/Serializer.java index 18677f88fb4ba7cf4692746d7a58d99fcf33a0d5..8c6ee944d066987ce19de4038f399b5ad7d54b90 100644 --- a/src/ztp/src/main/java/org/etsi/tfs/ztp/Serializer.java +++ b/src/ztp/src/main/java/org/etsi/tfs/ztp/Serializer.java @@ -891,6 +891,8 @@ public class Serializer { return ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_OPENROADM; case RESTCONF_OPENCONFIG: return ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_RESTCONF_OPENCONFIG; + case CUSTOM_IPOWDM: + return ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_CUSTOM_IPOWDM; case UNDEFINED: default: return ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_UNDEFINED; @@ -942,6 +944,8 @@ public class Serializer { return DeviceDriverEnum.OPENROADM; case DEVICEDRIVER_RESTCONF_OPENCONFIG: return DeviceDriverEnum.RESTCONF_OPENCONFIG; + case DEVICEDRIVER_CUSTOM_IPOWDM: + return DeviceDriverEnum.CUSTOM_IPOWDM; case DEVICEDRIVER_UNDEFINED: case UNRECOGNIZED: default: diff --git a/src/ztp/src/main/java/org/etsi/tfs/ztp/context/model/DeviceDriverEnum.java b/src/ztp/src/main/java/org/etsi/tfs/ztp/context/model/DeviceDriverEnum.java index 9874e521d199735ef80d24b7efa1b90ff0d1bea9..5bb0fe04874af8a9e0c21009fdeee6e2e1fcaa2c 100644 --- a/src/ztp/src/main/java/org/etsi/tfs/ztp/context/model/DeviceDriverEnum.java +++ b/src/ztp/src/main/java/org/etsi/tfs/ztp/context/model/DeviceDriverEnum.java @@ -1,18 +1,18 @@ /* -* Copyright 2022-2026 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) -* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -*/ + * Copyright 2022-2026 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.etsi.tfs.ztp.context.model; @@ -38,5 +38,6 @@ public enum DeviceDriverEnum { RYU, GNMI_NOKIA_SRLINUX, OPENROADM, - RESTCONF_OPENCONFIG + RESTCONF_OPENCONFIG, + CUSTOM_IPOWDM } diff --git a/src/ztp/src/test/java/org/etsi/tfs/ztp/SerializerTest.java b/src/ztp/src/test/java/org/etsi/tfs/ztp/SerializerTest.java index eab65460c2333bdbb595db1b4656753b59a7ca8e..e93114406b2ccf5c2d117a01614a373d4f7f1be2 100644 --- a/src/ztp/src/test/java/org/etsi/tfs/ztp/SerializerTest.java +++ b/src/ztp/src/test/java/org/etsi/tfs/ztp/SerializerTest.java @@ -1230,9 +1230,17 @@ class SerializerTest { DeviceDriverEnum.OPTICAL_TFS, ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_OPTICAL_TFS), Arguments.of( - DeviceDriverEnum.IETF_ACTN, ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_IETF_ACTN), + DeviceDriverEnum.IETF_ACTN, + ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_IETF_ACTN), Arguments.of( - DeviceDriverEnum.UNDEFINED, ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_UNDEFINED)); + DeviceDriverEnum.RESTCONF_OPENCONFIG, + ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_RESTCONF_OPENCONFIG), + Arguments.of( + DeviceDriverEnum.CUSTOM_IPOWDM, + ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_CUSTOM_IPOWDM), + Arguments.of( + DeviceDriverEnum.UNDEFINED, + ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_UNDEFINED)); } @ParameterizedTest diff --git a/src/ztp/target/generated-sources/grpc/context/ContextOuterClass.java b/src/ztp/target/generated-sources/grpc/context/ContextOuterClass.java index 27cde66fb4d01c41c0ae39bba4f4da897b2cd0c5..b9438459054b3cc769f705b92464a54049951487 100644 --- a/src/ztp/target/generated-sources/grpc/context/ContextOuterClass.java +++ b/src/ztp/target/generated-sources/grpc/context/ContextOuterClass.java @@ -235,6 +235,10 @@ public final class ContextOuterClass { * DEVICEDRIVER_RESTCONF_OPENCONFIG = 21; */ DEVICEDRIVER_RESTCONF_OPENCONFIG(21), + /** + * DEVICEDRIVER_CUSTOM_IPOWDM = 22; + */ + DEVICEDRIVER_CUSTOM_IPOWDM(22), UNRECOGNIZED(-1); /** @@ -351,6 +355,11 @@ public final class ContextOuterClass { */ public static final int DEVICEDRIVER_RESTCONF_OPENCONFIG_VALUE = 21; + /** + * DEVICEDRIVER_CUSTOM_IPOWDM = 22; + */ + public static final int DEVICEDRIVER_CUSTOM_IPOWDM_VALUE = 22; + public final int getNumber() { if (this == UNRECOGNIZED) { throw new java.lang.IllegalArgumentException("Can't get the number of an unknown enum value."); @@ -418,6 +427,8 @@ public final class ContextOuterClass { return DEVICEDRIVER_OPENROADM; case 21: return DEVICEDRIVER_RESTCONF_OPENCONFIG; + case 22: + return DEVICEDRIVER_CUSTOM_IPOWDM; default: return null; }