|
36 | 36 | # --insecure |
37 | 37 | # --operation=get |
38 | 38 | # --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 | +# |
39 | 44 |
|
40 | 45 | import argparse |
41 | 46 | import asyncio |
|
52 | 57 | from cloudvision.api.arista.studio import v1 as studio |
53 | 58 | from cloudvision.api.arista.changecontrol import v1 as changecontrol |
54 | 59 | from cloudvision.api.arista.action import v1 as action |
| 60 | +from cloudvision.cvlib.constants import MAINLINE_WS_ID |
55 | 61 |
|
56 | 62 | logger = logging.getLogger(__name__) |
57 | 63 |
|
| 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" |
58 | 68 | # RPC_TIMEOUT |
59 | 69 | # - used for quick requests (in seconds) |
60 | 70 | RPC_TIMEOUT = 30 |
|
64 | 74 | BUILD_TIMEOUT = 300 |
65 | 75 | # SYNC_TIMEOUT |
66 | 76 | # - set to max expected synchronization time (in seconds) |
67 | | -# - synchronization can take as long as a build since it triggers a rebuild |
68 | 77 | SYNC_TIMEOUT = 300 |
69 | 78 | # CC_EXECUTION_TIMEOUT |
70 | 79 | # - set to max expected CC time (in seconds) |
|
74 | 83 | # - set at minimum to max number parallel workspace requests |
75 | 84 | # - since submits are serial, Nth workspace will need N-1 syncs |
76 | 85 | 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 |
80 | 96 | # assign_studio |
81 | 97 | # - whether to modify studio device selection |
82 | 98 | assign_studio = False |
@@ -187,7 +203,7 @@ async def get_inputs(channel, filename): |
187 | 203 | ''' |
188 | 204 | sid = studio_id |
189 | 205 | key = studio.InputsKey(studio_id=sid, |
190 | | - workspace_id=MAINLINE_ID) |
| 206 | + workspace_id=MAINLINE_WS_ID) |
191 | 207 | pfilter = studio.Inputs(key=key) |
192 | 208 | req = studio.InputsStreamRequest() |
193 | 209 | req.partial_eq_filter.append(pfilter) |
@@ -718,20 +734,135 @@ async def submit_workspace(channel, ws_id): |
718 | 734 | return None, False, False |
719 | 735 |
|
720 | 736 |
|
| 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 | + |
721 | 837 | async def run_change_control(channel, cc_id): |
722 | 838 | ''' |
723 | 839 | Approves and starts a change control, waits for it to finish, |
724 | 840 | and reports the result. Returns True if execution was successful |
725 | 841 | and False otherwise. |
| 842 | +
|
| 843 | + If CC_ORDERING_ENABLED is True, waits for all earlier CCs to complete |
| 844 | + before executing this one. |
726 | 845 | ''' |
727 | 846 | logger.info('Executing change control %s', cc_id) |
728 | 847 | key = changecontrol.ChangeControlKey( |
729 | 848 | id=cc_id |
730 | 849 | ) |
731 | | - # Approve the change control. |
| 850 | + |
| 851 | + # Get this CC's creation timestamp for ordering |
732 | 852 | req = changecontrol.ChangeControlRequest(key=key) |
733 | 853 | stub = changecontrol.ChangeControlServiceStub(channel) |
734 | 854 | 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 |
735 | 866 | req = changecontrol.ApproveConfigSetRequest( |
736 | 867 | value=changecontrol.ApproveConfig( |
737 | 868 | key=key, |
@@ -785,7 +916,7 @@ async def main(args, client): |
785 | 916 | return |
786 | 917 | # Set Inputs in Multiple Steps |
787 | 918 | # Create a workspace. |
788 | | - workspace_name = f'{studio_id} config push' |
| 919 | + workspace_name = f'{studio_id} {CHANGE_SIGNATURE}' |
789 | 920 | if args.wsid: |
790 | 921 | ws_id = args.wsid |
791 | 922 | else: |
|
0 commit comments