From e533f5e0b83aa993b417682ffed85b4c35e3cf68 Mon Sep 17 00:00:00 2001 From: Juliana Cardozo Date: Tue, 14 Jul 2026 14:28:49 -0300 Subject: [PATCH 1/4] feat: last_deploy and last_undeploy attributes for portchannel --- ...add_last_deploy_undeploy_port_channel.migration | 14 ++++++++++++++ networkapi/api_interface/facade.py | 4 ++++ networkapi/interface/models.py | 2 ++ .../interface/resource/InterfaceChannelResource.py | 4 ++++ 4 files changed, 24 insertions(+) create mode 100644 dbmigrate/migrations/20260713120000_add_last_deploy_undeploy_port_channel.migration diff --git a/dbmigrate/migrations/20260713120000_add_last_deploy_undeploy_port_channel.migration b/dbmigrate/migrations/20260713120000_add_last_deploy_undeploy_port_channel.migration new file mode 100644 index 00000000..385fe088 --- /dev/null +++ b/dbmigrate/migrations/20260713120000_add_last_deploy_undeploy_port_channel.migration @@ -0,0 +1,14 @@ +#-*- coding:utf-8 -*- +SQL_UP = u""" +-- Adds last_deploy and last_undeploy columns to port_channel table +-- These columns track the datetime of the last deploy and undeploy operations +ALTER TABLE `port_channel` +ADD COLUMN `last_deploy` DATETIME NULL DEFAULT NULL, +ADD COLUMN `last_undeploy` DATETIME NULL DEFAULT NULL; +""" + +SQL_DOWN = u""" +ALTER TABLE `port_channel` +DROP COLUMN `last_deploy`, +DROP COLUMN `last_undeploy`; +""" diff --git a/networkapi/api_interface/facade.py b/networkapi/api_interface/facade.py index cd3c7990..0563d57a 100644 --- a/networkapi/api_interface/facade.py +++ b/networkapi/api_interface/facade.py @@ -17,6 +17,7 @@ import logging import os import re +from datetime import datetime from django.core.exceptions import FieldError from django.core.exceptions import ObjectDoesNotExist @@ -510,6 +511,9 @@ def generate_and_deploy_channel_config_sync(user, id_channel): equipamento = Equipamento.get_by_pk(equipment_id) status_deploy = deploy_config_in_equipment_synchronous( files_to_deploy[equipment_id], equipamento, lockvar) + + channel.last_deploy = datetime.now() + channel.save() return status_deploy diff --git a/networkapi/interface/models.py b/networkapi/interface/models.py index b2c8431a..67862dfc 100644 --- a/networkapi/interface/models.py +++ b/networkapi/interface/models.py @@ -165,6 +165,8 @@ class PortChannel(BaseModel): choices=StatusDeploy.list_type, default=StatusDeploy.pending[0] ) + last_deploy = models.DateTimeField(null=True, blank=True, db_column='last_deploy') + last_undeploy = models.DateTimeField(null=True, blank=True, db_column='last_undeploy') class Meta(BaseModel.Meta): db_table = u'port_channel' diff --git a/networkapi/interface/resource/InterfaceChannelResource.py b/networkapi/interface/resource/InterfaceChannelResource.py index 7cbabc1a..45ce28c7 100644 --- a/networkapi/interface/resource/InterfaceChannelResource.py +++ b/networkapi/interface/resource/InterfaceChannelResource.py @@ -14,6 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. import logging +from datetime import datetime from django.forms.models import model_to_dict @@ -362,6 +363,9 @@ def handle_delete(self, request, user, *args, **kwargs): if not keep_db: channel.delete(user) + else: + channel.last_undeploy = datetime.now() + channel.save(user) return self.response(dumps_networkapi({})) From 73be4e739535fa1dcf0bd959cdf5ca3ceb72447f Mon Sep 17 00:00:00 2001 From: Juliana Cardozo Date: Tue, 14 Jul 2026 14:30:26 -0300 Subject: [PATCH 2/4] feat: last_deploy and last_undeploy attributes for portchannel --- networkapi/api_interface/facade.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/networkapi/api_interface/facade.py b/networkapi/api_interface/facade.py index 0563d57a..f4dcd022 100644 --- a/networkapi/api_interface/facade.py +++ b/networkapi/api_interface/facade.py @@ -513,7 +513,7 @@ def generate_and_deploy_channel_config_sync(user, id_channel): files_to_deploy[equipment_id], equipamento, lockvar) channel.last_deploy = datetime.now() - channel.save() + channel.save(user) return status_deploy From dca2c8a1d825eefd1b93bc5f52f8c0108a7e73ed Mon Sep 17 00:00:00 2001 From: Thiago Guimaraes Rocha Date: Wed, 15 Jul 2026 09:28:52 -0300 Subject: [PATCH 3/4] PCEC-163: persist port_channel status_deploy on deploy/undeploy --- networkapi/api_interface/facade.py | 19 +++++-- networkapi/api_interface/serializers.py | 8 ++- .../resource/InterfaceChannelResource.py | 56 +++++++++++-------- 3 files changed, 52 insertions(+), 31 deletions(-) diff --git a/networkapi/api_interface/facade.py b/networkapi/api_interface/facade.py index f4dcd022..16f1aa25 100644 --- a/networkapi/api_interface/facade.py +++ b/networkapi/api_interface/facade.py @@ -40,6 +40,7 @@ from networkapi.interface.models import EnvironmentInterface from networkapi.interface.models import Interface from networkapi.interface.models import PortChannel +from networkapi.interface.models import StatusDeploy from networkapi.interface.models import TipoInterface from networkapi.interface import models from networkapi.system import exceptions as var_exceptions @@ -506,13 +507,19 @@ def generate_and_deploy_channel_config_sync(user, id_channel): # TODO Deploy config file # make separate threads - for equipment_id in files_to_deploy.keys(): - lockvar = LOCK_INTERFACE_DEPLOY_CONFIG % (equipment_id) - equipamento = Equipamento.get_by_pk(equipment_id) - status_deploy = deploy_config_in_equipment_synchronous( - files_to_deploy[equipment_id], equipamento, lockvar) - + try: + for equipment_id in files_to_deploy.keys(): + lockvar = LOCK_INTERFACE_DEPLOY_CONFIG % (equipment_id) + equipamento = Equipamento.get_by_pk(equipment_id) + status_deploy = deploy_config_in_equipment_synchronous( + files_to_deploy[equipment_id], equipamento, lockvar) + except Exception: + channel.status_deploy = StatusDeploy.error[0] + channel.save(user) + raise + channel.last_deploy = datetime.now() + channel.status_deploy = StatusDeploy.deployed[0] channel.save(user) return status_deploy diff --git a/networkapi/api_interface/serializers.py b/networkapi/api_interface/serializers.py index b566fc4f..6ffb619c 100644 --- a/networkapi/api_interface/serializers.py +++ b/networkapi/api_interface/serializers.py @@ -59,6 +59,9 @@ class Meta: 'id', 'name', 'lacp', + 'status_deploy', + 'last_deploy', + 'last_undeploy', ) default_fields = fields @@ -66,7 +69,10 @@ class Meta: basic_fields = ( 'id', 'name', - 'lacp' + 'lacp', + 'status_deploy', + 'last_deploy', + 'last_undeploy', ) details_fields = fields diff --git a/networkapi/interface/resource/InterfaceChannelResource.py b/networkapi/interface/resource/InterfaceChannelResource.py index 45ce28c7..bba6f1fd 100644 --- a/networkapi/interface/resource/InterfaceChannelResource.py +++ b/networkapi/interface/resource/InterfaceChannelResource.py @@ -32,6 +32,7 @@ from networkapi.interface.models import InterfaceError from networkapi.interface.models import InterfaceNotFoundError from networkapi.interface.models import PortChannel +from networkapi.interface.models import StatusDeploy from networkapi.interface.models import TipoInterface from networkapi.rest import RestResource from networkapi.rest import UserNotAuthorizedError @@ -336,35 +337,42 @@ def handle_delete(self, request, user, *args, **kwargs): tipo = tipo.get_by_name('access') # For each equipment (leaf), undeploy port channel config - for e in equip_dict: - if not keep_db: - for i in equip_dict.get(e): - try: - front = i.ligacao_front.id - except: - front = None - pass - try: - back = i.ligacao_back.id - except: - back = None - pass - i.update(user, - i.id, - interface=i.interface, - protegida=i.protegida, - descricao=i.descricao, - ligacao_front_id=front, - ligacao_back_id=back, - tipo=tipo, - vlan_nativa='1') - - api_interface_facade.delete_channel(user, e, equip_dict.get(e), channel) + try: + for e in equip_dict: + if not keep_db: + for i in equip_dict.get(e): + try: + front = i.ligacao_front.id + except: + front = None + pass + try: + back = i.ligacao_back.id + except: + back = None + pass + i.update(user, + i.id, + interface=i.interface, + protegida=i.protegida, + descricao=i.descricao, + ligacao_front_id=front, + ligacao_back_id=back, + tipo=tipo, + vlan_nativa='1') + + api_interface_facade.delete_channel(user, e, equip_dict.get(e), channel) + except Exception: + if keep_db: + channel.status_deploy = StatusDeploy.error[0] + channel.save(user) + raise if not keep_db: channel.delete(user) else: channel.last_undeploy = datetime.now() + channel.status_deploy = StatusDeploy.pending[0] channel.save(user) return self.response(dumps_networkapi({})) From 1c80b71165c6cc6cc7736f0670e358b37d6378b4 Mon Sep 17 00:00:00 2001 From: Thiago Guimaraes Rocha Date: Thu, 16 Jul 2026 12:15:20 -0300 Subject: [PATCH 4/4] PCEC-163: cover config generation failures under status_deploy=error --- networkapi/api_interface/facade.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/networkapi/api_interface/facade.py b/networkapi/api_interface/facade.py index 16f1aa25..17bfe2b3 100644 --- a/networkapi/api_interface/facade.py +++ b/networkapi/api_interface/facade.py @@ -499,15 +499,15 @@ def generate_and_deploy_channel_config_sync(user, id_channel): equipment_interfaces[interface.equipamento.id] = [] equipment_interfaces[interface.equipamento.id].append(interface) - files_to_deploy = {} - for equipment_id in equipment_interfaces.keys(): - grouped_interfaces = equipment_interfaces[equipment_id] - file_to_deploy = _generate_config_file(grouped_interfaces) - files_to_deploy[equipment_id] = file_to_deploy - - # TODO Deploy config file - # make separate threads try: + files_to_deploy = {} + for equipment_id in equipment_interfaces.keys(): + grouped_interfaces = equipment_interfaces[equipment_id] + file_to_deploy = _generate_config_file(grouped_interfaces) + files_to_deploy[equipment_id] = file_to_deploy + + # TODO Deploy config file + # make separate threads for equipment_id in files_to_deploy.keys(): lockvar = LOCK_INTERFACE_DEPLOY_CONFIG % (equipment_id) equipamento = Equipamento.get_by_pk(equipment_id)