Skip to content

Commit 78659e8

Browse files
committed
examples/resources: add cc ordering to studio_update.py
- to handle parallel studio update executions, execute CCs in order of workspace submission. - client script based solution to BUG1784422 Change-Id: I3b792b96846141a148589846b436992508034c2e
1 parent 2b1aeef commit 78659e8

1 file changed

Lines changed: 138 additions & 7 deletions

File tree

examples/resources/studio/studio_update.py

Lines changed: 138 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@
3636
# --insecure
3737
# --operation=get
3838
# --studio-id=studio-interface-v2-pkg
39+
#
40+
# This script can be invoked from a multi-threaded program. It has the
41+
# ability to rebase and order CCs appropriately to correctly process
42+
# parallel executions.
43+
#
3944

4045
import argparse
4146
import asyncio
@@ -52,9 +57,14 @@
5257
from cloudvision.api.arista.studio import v1 as studio
5358
from cloudvision.api.arista.changecontrol import v1 as changecontrol
5459
from cloudvision.api.arista.action import v1 as action
60+
from cloudvision.cvlib.constants import MAINLINE_WS_ID
5561

5662
logger = logging.getLogger(__name__)
5763

64+
# CHANGE_SIGNATURE
65+
# - substring used in workspace and change control names,
66+
# used to identify changes automated by this script
67+
CHANGE_SIGNATURE = "studio_update.py config push"
5868
# RPC_TIMEOUT
5969
# - used for quick requests (in seconds)
6070
RPC_TIMEOUT = 30
@@ -64,7 +74,6 @@
6474
BUILD_TIMEOUT = 300
6575
# SYNC_TIMEOUT
6676
# - set to max expected synchronization time (in seconds)
67-
# - synchronization can take as long as a build since it triggers a rebuild
6877
SYNC_TIMEOUT = 300
6978
# CC_EXECUTION_TIMEOUT
7079
# - set to max expected CC time (in seconds)
@@ -74,9 +83,16 @@
7483
# - set at minimum to max number parallel workspace requests
7584
# - since submits are serial, Nth workspace will need N-1 syncs
7685
MAX_SYNC_RETRIES = 10
77-
# MAINLINE_ID
78-
# - mainline to which workspaces submit
79-
MAINLINE_ID = ""
86+
# CC_ORDERING_ENABLED
87+
# - when True, CCs will execute in creation order (waits for earlier CCs)
88+
# - when False, CCs execute immediately after submission
89+
CC_ORDERING_ENABLED = True
90+
# MAX_CC_WAIT_ITERATIONS
91+
# - maximum iterations to wait for earlier CCs to complete
92+
MAX_CC_WAIT_ITERATIONS = 120
93+
# CC_POLL_INTERVAL
94+
# - seconds to wait between polling for earlier CCs
95+
CC_POLL_INTERVAL = 5
8096
# assign_studio
8197
# - whether to modify studio device selection
8298
assign_studio = False
@@ -187,7 +203,7 @@ async def get_inputs(channel, filename):
187203
'''
188204
sid = studio_id
189205
key = studio.InputsKey(studio_id=sid,
190-
workspace_id=MAINLINE_ID)
206+
workspace_id=MAINLINE_WS_ID)
191207
pfilter = studio.Inputs(key=key)
192208
req = studio.InputsStreamRequest()
193209
req.partial_eq_filter.append(pfilter)
@@ -718,20 +734,135 @@ async def submit_workspace(channel, ws_id):
718734
return None, False, False
719735

720736

737+
async def get_earlier_change_controls(channel, my_timestamp):
738+
'''
739+
Query all CCs that:
740+
1. Were created before my_timestamp
741+
2. Are still pending, approved, or running (not completed/cancelled)
742+
3. Were created by studio_update.py (name contains CHANGE_SIGNATURE)
743+
744+
Returns a list of earlier CCs that are still active.
745+
'''
746+
stub = changecontrol.ChangeControlServiceStub(channel)
747+
748+
# Query CCs with active statuses (server-side filter to reduce data transfer)
749+
# We still need to filter by timestamp and name client-side
750+
req = changecontrol.ChangeControlStreamRequest(
751+
partial_eq_filter=[
752+
changecontrol.ChangeControl(
753+
status=changecontrol.ChangeControlStatus.NOT_STARTED
754+
),
755+
changecontrol.ChangeControl(
756+
status=changecontrol.ChangeControlStatus.SCHEDULED
757+
),
758+
changecontrol.ChangeControl(
759+
status=changecontrol.ChangeControlStatus.RUNNING
760+
)
761+
]
762+
)
763+
764+
earlier_ccs = []
765+
766+
try:
767+
async for resp in stub.get_all(req, timeout=RPC_TIMEOUT):
768+
cc_data = resp.value
769+
cc_timestamp = cc_data.creation.time
770+
771+
# Check if this CC was created before mine
772+
# aristaproto converts Timestamp to datetime, so we can compare directly
773+
is_earlier = cc_timestamp < my_timestamp
774+
775+
if not is_earlier:
776+
continue
777+
778+
# Only consider CCs created by studio_update.py
779+
# These have CHANGE_SIGNATURE in their name
780+
cc_name = cc_data.change.name if cc_data.change and cc_data.change.name else ""
781+
if CHANGE_SIGNATURE not in cc_name:
782+
continue
783+
784+
# Status already filtered server-side, so we can add directly
785+
earlier_ccs.append(cc_data)
786+
except Exception as e:
787+
logger.warning('\tError querying earlier CCs: %s', e)
788+
return []
789+
790+
return earlier_ccs
791+
792+
793+
async def wait_for_earlier_change_controls(channel, my_timestamp):
794+
'''
795+
Wait until all CCs created before this one have completed.
796+
This ensures CCs execute in creation order.
797+
798+
This is best-effort: on timeout, proceed anyway to avoid deadlock.
799+
'''
800+
if not CC_ORDERING_ENABLED:
801+
return
802+
803+
logger.info('\tChecking for earlier change controls...')
804+
805+
for iteration in range(MAX_CC_WAIT_ITERATIONS):
806+
# Get all pending and running CCs created before mine
807+
earlier_ccs = await get_earlier_change_controls(channel, my_timestamp)
808+
809+
if not earlier_ccs:
810+
# No earlier CCs pending/running, safe to proceed
811+
if iteration == 0:
812+
logger.info('\tNo earlier CCs blocking execution')
813+
else:
814+
logger.info('\tAll earlier CCs completed, proceeding with execution')
815+
return
816+
817+
# Log waiting status with CC names
818+
earlier_cc_info = [(cc.key.id, cc.change.name if cc.change else 'unknown')
819+
for cc in earlier_ccs]
820+
if len(earlier_cc_info) <= 3:
821+
logger.info('\tWaiting for %d earlier CC(s): %s',
822+
len(earlier_ccs), [name for _, name in earlier_cc_info])
823+
else:
824+
logger.info('\tWaiting for %d earlier CC(s): %s ... and %d more',
825+
len(earlier_ccs), [name for _, name in earlier_cc_info[:3]],
826+
len(earlier_ccs) - 3)
827+
828+
# Wait before next check
829+
await asyncio.sleep(CC_POLL_INTERVAL)
830+
831+
# Timeout waiting for earlier CCs
832+
max_wait_time = MAX_CC_WAIT_ITERATIONS * CC_POLL_INTERVAL
833+
logger.warning('Timeout waiting for earlier CCs after %ds, proceeding anyway',
834+
max_wait_time)
835+
836+
721837
async def run_change_control(channel, cc_id):
722838
'''
723839
Approves and starts a change control, waits for it to finish,
724840
and reports the result. Returns True if execution was successful
725841
and False otherwise.
842+
843+
If CC_ORDERING_ENABLED is True, waits for all earlier CCs to complete
844+
before executing this one.
726845
'''
727846
logger.info('Executing change control %s', cc_id)
728847
key = changecontrol.ChangeControlKey(
729848
id=cc_id
730849
)
731-
# Approve the change control.
850+
851+
# Get this CC's creation timestamp for ordering
732852
req = changecontrol.ChangeControlRequest(key=key)
733853
stub = changecontrol.ChangeControlServiceStub(channel)
734854
res = await stub.get_one(req)
855+
856+
my_timestamp = res.value.creation.time
857+
my_cc_name = res.value.change.name if res.value.change else "unknown"
858+
logger.info('\tCC "%s" created at: %s', my_cc_name, my_timestamp)
859+
860+
# Best-effort wait for all earlier CCs to complete,
861+
# if ordering is enabled
862+
await wait_for_earlier_change_controls(channel, my_timestamp)
863+
864+
# Now safe to proceed with approval and execution
865+
# Approve the change control
735866
req = changecontrol.ApproveConfigSetRequest(
736867
value=changecontrol.ApproveConfig(
737868
key=key,
@@ -785,7 +916,7 @@ async def main(args, client):
785916
return
786917
# Set Inputs in Multiple Steps
787918
# Create a workspace.
788-
workspace_name = f'{studio_id} config push'
919+
workspace_name = f'{studio_id} {CHANGE_SIGNATURE}'
789920
if args.wsid:
790921
ws_id = args.wsid
791922
else:

0 commit comments

Comments
 (0)