コード例 #1
0
ファイル: ophandlers.php プロジェクト: xtha/salt
function resolve8021QConflicts()
{
    global $sic, $dbxlink;
    assertUIntArg('mutex_rev', TRUE);
    // counts from 0
    assertUIntArg('nrows');
    // Divide submitted radio buttons into 3 groups:
    // left (saved version wins)
    // asis (ignore)
    // right (running version wins)
    $F = array();
    for ($i = 0; $i < $sic['nrows']; $i++) {
        if (!array_key_exists("i_{$i}", $sic)) {
            continue;
        }
        // let's hope other inputs are in place
        switch ($sic["i_{$i}"]) {
            case 'left':
            case 'right':
                $F[$sic["pn_{$i}"]] = array('mode' => $sic["rm_{$i}"], 'allowed' => array_fetch($sic, "ra_{$i}", array()), 'native' => $sic["rn_{$i}"], 'decision' => $sic["i_{$i}"]);
                break;
            default:
                // don't care
        }
    }
    $dbxlink->beginTransaction();
    try {
        if (NULL === ($vswitch = getVLANSwitchInfo($sic['object_id'], 'FOR UPDATE'))) {
            throw new InvalidArgException('object_id', $sic['object_id'], 'VLAN domain is not set for this object');
        }
        if ($vswitch['mutex_rev'] != $sic['mutex_rev']) {
            throw new InvalidRequestArgException('mutex_rev', $sic['mutex_rev'], 'expired form (table data has changed)');
        }
        $D = getStored8021QConfig($vswitch['object_id'], 'desired');
        $C = getStored8021QConfig($vswitch['object_id'], 'cached');
        $R = getRunning8021QConfig($vswitch['object_id']);
        $plan = get8021QSyncOptions($vswitch, $D, $C, $R['portdata']);
        $ndone = 0;
        foreach ($F as $port_name => $port) {
            if (!array_key_exists($port_name, $plan)) {
                continue;
            } elseif ($plan[$port_name]['status'] == 'merge_conflict') {
                // for R neither mutex nor revisions can be emulated, but revision change can be
                if (!same8021QConfigs($port, $R['portdata'][$port_name])) {
                    throw new InvalidRequestArgException("port {$port_name}", '(hidden)', 'expired form (switch data has changed)');
                }
                if ($port['decision'] == 'right') {
                    // D wins, frame R by writing value of R to C
                    $ndone += upd8021QPort('cached', $vswitch['object_id'], $port_name, $port);
                } elseif ($port['decision'] == 'left') {
                    // R wins, cross D up
                    $ndone += upd8021QPort('cached', $vswitch['object_id'], $port_name, $D[$port_name]);
                }
                // otherwise there was no decision made
            } elseif ($plan[$port_name]['status'] == 'delete_conflict' or $plan[$port_name]['status'] == 'martian_conflict') {
                if ($port['decision'] == 'left') {
                    // confirm deletion of local copy
                    $ndone += del8021QPort($vswitch['object_id'], $port_name);
                }
            }
            // otherwise ignore a decision that doesn't address a conflict
        }
    } catch (InvalidRequestArgException $e) {
        $dbxlink->rollBack();
        showFuncMessage(__FUNCTION__, 'ERR1');
        return;
    } catch (Exception $e) {
        $dbxlink->rollBack();
        showFuncMessage(__FUNCTION__, 'ERR2');
        return;
    }
    $dbxlink->commit();
    showFuncMessage(__FUNCTION__, 'OK', array($ndone));
}
コード例 #2
0
ファイル: interface.php プロジェクト: xtha/salt
function renderObject8021QSync($object_id)
{
    $vswitch = getVLANSwitchInfo($object_id);
    $object = spotEntity('object', $object_id);
    amplifyCell($object);
    $maxdecisions = 0;
    $D = getStored8021QConfig($vswitch['object_id'], 'desired');
    $C = getStored8021QConfig($vswitch['object_id'], 'cached');
    try {
        $R = getRunning8021QConfig($object_id);
        $plan = apply8021QOrder($vswitch, get8021QSyncOptions($vswitch, $D, $C, $R['portdata']));
        foreach ($plan as $port) {
            if ($port['status'] == 'delete_conflict' or $port['status'] == 'merge_conflict' or $port['status'] == 'add_conflict' or $port['status'] == 'martian_conflict') {
                $maxdecisions++;
            }
        }
    } catch (RTGatewayError $re) {
        $error = $re->getMessage();
        $R = NULL;
    }
    echo '<table border=0 class=objectview cellspacing=0 cellpadding=0>';
    echo '<tr><td class=pcleft width="50%">';
    startPortlet('schedule');
    renderObject8021QSyncSchedule($object, $vswitch, $maxdecisions);
    finishPortlet();
    startPortlet('preview legend');
    echo '<table cellspacing=0 cellpadding=5 align=center class=widetable>';
    echo '<tr><th>status</th><th width="50%">color code</th></tr>';
    echo '<tr><td class=tdright>with template role:</td><td class=trbusy>&nbsp;</td></tr>';
    echo '<tr><td class=tdright>without template role:</td><td>&nbsp;</td></tr>';
    echo '<tr><td class=tdright>new data:</td><td class=trok>&nbsp;</td></tr>';
    echo '<tr><td class=tdright>warnings in new data:</td><td class=trwarning>&nbsp;</td></tr>';
    echo '<tr><td class=tdright>fatal errors in new data:</td><td class=trerror>&nbsp;</td></tr>';
    echo '<tr><td class=tdright>deleted data:</td><td class=trnull>&nbsp;</td></tr>';
    echo '</table>';
    finishPortlet();
    if (considerConfiguredConstraint($object, '8021Q_EXTSYNC_LISTSRC')) {
        startPortlet('add/remove 802.1Q ports');
        renderObject8021QSyncPorts($object, $D);
        finishPortlet();
    }
    echo '</td><td class=pcright>';
    startPortlet('sync plan live preview');
    if ($R !== NULL) {
        renderObject8021QSyncPreview($object, $vswitch, $plan, $C, $R, $maxdecisions);
    } else {
        echo "<p class=row_error>gateway error: {$error}</p>";
    }
    finishPortlet();
    echo '</td></tr></table>';
}
コード例 #3
0
ファイル: syncdomain.php プロジェクト: ivladdalvi/racktables
        if ($current_time < $pidfile_mtime + 15 * 60) {
            exit(0);
        }
        print_message_line("Failed to lock {$filename}, already locked by PID " . trim(fgets($fp, 10)));
        exit(1);
    }
    ftruncate($fp, 0);
    fwrite($fp, getmypid() . "\n");
    // don't close $fp yet: we need to keep an flock
}
// fetch all the needed data from DB (preparing for DB connection loss)
$switch_queue = array();
foreach ($switch_list as $object_id) {
    $cell = spotEntity('object', $object_id);
    $new_disabled = !considerConfiguredConstraint($cell, 'SYNC_802Q_LISTSRC');
    $queue = detectVLANSwitchQueue(getVLANSwitchInfo($object_id));
    if ($queue == 'disabled' xor $new_disabled) {
        usePreparedExecuteBlade('UPDATE VLANSwitch SET out_of_sync="yes", last_error_ts=NOW(), last_errno=? WHERE object_id=?', array($new_disabled ? E_8021Q_SYNC_DISABLED : E_8021Q_NOERROR, $object_id));
    } elseif (in_array($queue, $todo[$options['mode']])) {
        $switch_queue[] = $cell;
    }
}
// YOU SHOULD NOT USE DB FUNCTIONS BELOW IN THE PARENT PROCESS
// THE PARENT'S DB CONNECTION IS LOST DUE TO RECONNECTING IN THE CHILD
$fork_slots = getConfigVar('SYNCDOMAIN_MAX_PROCESSES');
$do_fork = $fork_slots > 1 and extension_loaded('pcntl');
if ($fork_slots > 1 and !$do_fork) {
    throw new RackTablesError('PHP extension \'pcntl\' not found, can not use childs', RackTablesError::MISCONFIGURED);
}
$switches_working = 0;
$switchesdone = 0;
コード例 #4
0
ファイル: interface.php プロジェクト: rhysm/racktables
function renderObject8021QSync($object_id)
{
    $vswitch = getVLANSwitchInfo($object_id);
    $object = spotEntity('object', $object_id);
    try {
        $R = getRunning8021QConfig($object_id);
    } catch (Exception $re) {
        showWarning('Device configuration unavailable:<br>' . $re->getMessage());
        return;
    }
    $D = getStored8021QConfig($vswitch['object_id'], 'desired');
    $C = getStored8021QConfig($vswitch['object_id'], 'cached');
    $plan = apply8021QOrder($vswitch['template_id'], get8021QSyncOptions($vswitch, $D, $C, $R['portdata']));
    $maxdecisions = 0;
    foreach ($plan as $port) {
        if ($port['status'] == 'delete_conflict' or $port['status'] == 'merge_conflict' or $port['status'] == 'add_conflict' or $port['status'] == 'martian_conflict') {
            $maxdecisions++;
        }
    }
    if (isset($_REQUEST['hl_port_id'])) {
        assertUIntArg('hl_port_id');
        $hl_port_id = intval($_REQUEST['hl_port_id']);
        $hl_port_name = NULL;
        addAutoScrollScript("port-{$hl_port_id}");
        amplifyCell($object);
        foreach ($object['ports'] as $port) {
            if (mb_strlen($port['name']) && $port['id'] == $hl_port_id) {
                $hl_port_name = $port['name'];
                break;
            }
        }
    }
    echo '<table border=0 class=objectview cellspacing=0 cellpadding=0>';
    echo '<tr><td class=pcleft width="50%">';
    startPortlet('schedule');
    echo '<table border=0 cellspacing=0 cellpadding=3 align=center>';
    // FIXME: sort rows newest event last
    $rows = array();
    if (!considerConfiguredConstraint($object, 'SYNC_802Q_LISTSRC')) {
        $rows['auto sync'] = '<span class="trerror">disabled by operator</span>';
    }
    $rows['last local change'] = $vswitch['last_change'] . ' (' . $vswitch['last_change_age'] . ' ago)';
    $rows['device out of sync'] = $vswitch['out_of_sync'];
    if ($vswitch['out_of_sync'] == 'no') {
        $rows['last sync session with device'] = $vswitch['last_push_finished'] . ' (' . $vswitch['last_push_age'] . ' ago, lasted ' . $vswitch['last_push_lasted'] . ')';
    }
    if ($vswitch['last_errno']) {
        $rows['failed'] = $vswitch['last_error_ts'] . ' (' . strerror8021Q($vswitch['last_errno']) . ')';
    }
    if (NULL !== ($new_rows = callHook('alter8021qSyncSummaryItems', $rows))) {
        $rows = $new_rows;
    }
    foreach ($rows as $th => $td) {
        echo "<tr><th width='50%' class=tdright>{$th}:</th><td class=tdleft colspan=2>{$td}</td></tr>";
    }
    echo '<tr><th class=tdright>run now:</th><td class=tdcenter>';
    printOpFormIntro('exec8021QPull');
    echo getImageHREF('prev', 'pull remote changes in', TRUE, 101) . '</form></td><td class=tdcenter>';
    if ($maxdecisions) {
        echo getImageHREF('COMMIT gray', 'cannot push due to version conflict(s)');
    } else {
        printOpFormIntro('exec8021QPush');
        echo getImageHREF('COMMIT', 'push local changes out', TRUE, 102) . '</form>';
    }
    echo '</td></tr>';
    echo '</table>';
    finishPortlet();
    startPortlet('preview legend');
    echo '<table cellspacing=0 cellpadding=5 align=center class=widetable>';
    echo '<tr><th>status</th><th width="50%">color code</th></tr>';
    echo '<tr><td class=tdright>with template role:</td><td class=trbusy>&nbsp;</td></tr>';
    echo '<tr><td class=tdright>without template role:</td><td>&nbsp;</td></tr>';
    echo '<tr><td class=tdright>new data:</td><td class=trok>&nbsp;</td></tr>';
    echo '<tr><td class=tdright>warnings in new data:</td><td class=trwarning>&nbsp;</td></tr>';
    echo '<tr><td class=tdright>fatal errors in new data:</td><td class=trerror>&nbsp;</td></tr>';
    echo '<tr><td class=tdright>deleted data:</td><td class=trnull>&nbsp;</td></tr>';
    echo '</table>';
    finishPortlet();
    echo '</td><td class=pcright>';
    startPortlet('preview/resolve');
    switchportInfoJS($object_id);
    // load JS code to make portnames interactive
    // initialize one of three popups: we've got data already
    $port_config = addslashes(json_encode(formatPortConfigHints($object_id, $R)));
    addJS(<<<END
\$(document).ready(function(){
\tvar confData = \$.parseJSON('{$port_config}');
\tapplyConfData(confData);
\tvar menuItem = \$('.context-menu-item.itemname-conf');
\tmenuItem.addClass(\$.contextMenu.disabledItemClassName);
\tsetItemIcon(menuItem[0], 'ok');
});
END
, TRUE);
    echo '<table cellspacing=0 cellpadding=5 align=center class=widetable width="100%">';
    if ($maxdecisions) {
        echo '<tr><th colspan=2>&nbsp;</th><th colspan=3>discard</th><th>&nbsp;</th></tr>';
    }
    echo '<tr valign=top><th>port</th><th width="40%">last&nbsp;saved&nbsp;version</th>';
    if ($maxdecisions) {
        addJS('js/racktables.js');
        printOpFormIntro('resolve8021QConflicts', array('mutex_rev' => $vswitch['mutex_rev']));
        foreach (array('left', 'asis', 'right') as $pos) {
            echo "<th class=tdcenter><input type=radio name=column_radio value={$pos} " . "onclick=\"checkColumnOfRadios('i_', {$maxdecisions}, '_{$pos}')\"></th>";
        }
    }
    echo '<th width="40%">running&nbsp;version</th></tr>';
    $rownum = 0;
    $plan = sortPortList($plan);
    $domvlans = array_keys(getDomainVLANs($vswitch['domain_id']));
    $default_port = array('mode' => 'access', 'allowed' => array(VLAN_DFL_ID), 'native' => VLAN_DFL_ID);
    foreach ($plan as $port_name => $item) {
        $trclass = $left_extra = $right_extra = $left_text = $right_text = '';
        $radio_attrs = array();
        switch ($item['status']) {
            case 'ok_to_delete':
                $left_text = serializeVLANPack($item['left']);
                $right_text = 'none';
                $left_extra = ' trnull';
                $right_extra = ' trok';
                // no confirmation is necessary
                break;
            case 'delete_conflict':
                $trclass = 'trbusy';
                $left_extra = ' trerror';
                // can be fixed on request
                $right_extra = ' trnull';
                $left_text = formatVLANPackDiff($item['lastseen'], $item['left']);
                $right_text = '&nbsp;';
                $radio_attrs = array('left' => '', 'asis' => ' checked', 'right' => ' disabled');
                // dummy setting to suppress warnings in resolve8021QConflicts()
                $item['right'] = $default_port;
                break;
            case 'add_conflict':
                $trclass = 'trbusy';
                $right_extra = ' trerror';
                $left_text = '&nbsp;';
                $right_text = serializeVLANPack($item['right']);
                break;
            case 'ok_to_add':
                $trclass = 'trbusy';
                $right_extra = ' trok';
                $left_text = '&nbsp;';
                $right_text = serializeVLANPack($item['right']);
                break;
            case 'ok_to_merge':
                $trclass = 'trbusy';
                $left_extra = ' trok';
                $right_extra = ' trok';
                // fall through
            // fall through
            case 'in_sync':
                $trclass = 'trbusy';
                $left_text = $right_text = serializeVLANPack($item['both']);
                break;
            case 'ok_to_pull':
                // at least one of the sides is not in the default state
                $trclass = 'trbusy';
                $right_extra = ' trok';
                $left_text = serializeVLANPack($item['left']);
                $right_text = serializeVLANPack($item['right']);
                break;
            case 'ok_to_push':
                $trclass = ' trbusy';
                $left_extra = ' trok';
                $left_text = formatVLANPackDiff($C[$port_name], $item['left']);
                $right_text = serializeVLANPack($item['right']);
                break;
            case 'merge_conflict':
                $trclass = 'trbusy';
                $left_extra = ' trerror';
                $right_extra = ' trerror';
                $left_text = formatVLANPackDiff($C[$port_name], $item['left']);
                $right_text = serializeVLANPack($item['right']);
                // enable, but consider each option independently
                // Don't accept running VLANs not in domain, and
                // don't offer anything, that VST will deny.
                // Consider domain and template constraints.
                $radio_attrs = array('left' => '', 'asis' => ' checked', 'right' => '');
                if (!acceptable8021QConfig($item['right']) or count(array_diff($item['right']['allowed'], $domvlans)) or !goodModeForVSTRole($item['right']['mode'], $item['vst_role'])) {
                    $radio_attrs['left'] = ' disabled';
                }
                break;
            case 'ok_to_push_with_merge':
                $trclass = 'trbusy';
                $left_extra = ' trok';
                $right_extra = ' trwarning';
                $left_text = formatVLANPackDiff($C[$port_name], $item['left']);
                $right_text = serializeVLANPack($item['right']);
                break;
            case 'none':
                $left_text = '&nbsp;';
                $right_text = '&nbsp;';
                break;
            case 'martian_conflict':
                if ($item['right']['mode'] == 'none') {
                    $right_text = '&nbsp;';
                } else {
                    $right_text = serializeVLANPack($item['right']);
                    $right_extra = ' trerror';
                }
                if ($item['left']['mode'] == 'none') {
                    $left_text = '&nbsp;';
                } else {
                    $left_text = serializeVLANPack($item['left']);
                    $left_extra = ' trerror';
                    $radio_attrs = array('left' => '', 'asis' => ' checked', 'right' => ' disabled');
                    // idem, see above
                    $item['right'] = $default_port;
                }
                break;
            default:
                $trclass = 'trerror';
                $left_text = $right_text = 'internal rendering error';
                break;
        }
        $ancor = '';
        $td_class = '';
        if (isset($hl_port_name) and $hl_port_name == $port_name) {
            $ancor = "name='port-{$hl_port_id}'";
            $td_class = ' border_highlight';
        }
        echo "<tr class='{$trclass}'><td class='tdleft{$td_class}' NOWRAP><a class='interactive-portname port-menu nolink' {$ancor}>{$port_name}</a></td>";
        if (!count($radio_attrs)) {
            echo "<td class='tdleft{$left_extra}'>{$left_text}</td>";
            if ($maxdecisions) {
                echo '<td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td>';
            }
            echo "<td class='tdleft{$right_extra}'>{$right_text}</td>";
        } else {
            echo "<td class='tdleft{$left_extra}'><label for=i_{$rownum}_left>{$left_text}</label></td>";
            foreach ($radio_attrs as $pos => $attrs) {
                echo "<td><input id=i_{$rownum}_{$pos} name=i_{$rownum} type=radio value={$pos}{$attrs}></td>";
            }
            echo "<td class='tdleft{$right_extra}'><label for=i_{$rownum}_right>{$right_text}</label></td>";
        }
        echo '</tr>';
        if (count($radio_attrs)) {
            echo "<input type=hidden name=rm_{$rownum} value=" . $item['right']['mode'] . '>';
            echo "<input type=hidden name=rn_{$rownum} value=" . $item['right']['native'] . '>';
            foreach ($item['right']['allowed'] as $a) {
                echo "<input type=hidden name=ra_{$rownum}[] value={$a}>";
            }
            echo "<input type=hidden name=pn_{$rownum} value='" . htmlspecialchars($port_name) . "'>";
        }
        $rownum += count($radio_attrs) ? 1 : 0;
    }
    if ($rownum) {
        echo "<input type=hidden name=nrows value={$rownum}>";
        echo '<tr><td colspan=2>&nbsp;</td><td colspan=3 align=center class=tdcenter>';
        printImageHREF('UNLOCK', 'resolve conflicts', TRUE);
        echo '</td><td>&nbsp;</td></tr>';
    }
    echo '</table>';
    echo '</form>';
    finishPortlet();
    echo '</td></tr></table>';
}
コード例 #5
0
ファイル: triggers.php プロジェクト: rhysm/racktables
function trigger_object_8021qsync()
{
    global $sic;
    if (NULL === ($vswitch = getVLANSwitchInfo($sic['object_id']))) {
        return '';
    }
    return $vswitch['out_of_sync'] == 'yes' ? 'attn' : 'std';
}
コード例 #6
0
ファイル: syncdomain.php プロジェクト: ehironymous/racktables
        exit(1);
    }
    // don't indicate failure unless the pidfile is 15 minutes or more old
    if ($current_time < $pidfile_mtime + 15 * 60) {
        exit(0);
    }
    print_message_line("Failed to lock {$filename}, already locked by PID " . mb_substr(file_get_contents($filename), 0, 6));
    exit(1);
}
ftruncate($fp, 0);
fwrite($fp, getmypid() . "\n");
fclose($fp);
// fetch all the needed data from DB (preparing for DB connection loss)
$switch_queue = array();
foreach ($switch_list as $object_id) {
    if (in_array(detectVLANSwitchQueue(getVLANSwitchInfo($object_id)), $todo[$options['mode']])) {
        $cell = spotEntity('object', $object_id);
        if (considerConfiguredConstraint($cell, 'SYNC_802Q_LISTSRC')) {
            $switch_queue[] = $cell;
        }
    }
}
// YOU SHOULD NOT USE DB FUNCTIONS BELOW IN THE PARENT PROCESS
// THE PARENT'S DB CONNECTION IS LOST DUE TO RECONNECTING IN THE CHILD
$fork_slots = getConfigVar('SYNCDOMAIN_MAX_PROCESSES');
$do_fork = $fork_slots > 1 and extension_loaded('pcntl');
if ($fork_slots > 1 and !$do_fork) {
    throw new RackTablesError('PHP extension \'pcntl\' not found, can not use childs', RackTablesError::MISCONFIGURED);
}
$switches_working = 0;
$switchesdone = 0;
コード例 #7
0
ファイル: triggers.php プロジェクト: peter-volkov/RackTrack
function trigger_object_8021qsync()
{
    if (NULL === ($vswitch = getVLANSwitchInfo(getBypassValue()))) {
        return '';
    }
    return $vswitch['out_of_sync'] == 'yes' ? 'attn' : 'std';
}
コード例 #8
0
ファイル: functions.php プロジェクト: rhysm/racktables
function apply8021qChangeRequest($switch_id, $changes, $verbose = TRUE, $mutex_rev = NULL)
{
    global $dbxlink;
    $dbxlink->beginTransaction();
    try {
        if (NULL === ($vswitch = getVLANSwitchInfo($switch_id, 'FOR UPDATE'))) {
            throw new InvalidArgException('object_id', $switch_id, 'VLAN domain is not set for this object');
        }
        if (isset($mutex_rev) and $vswitch['mutex_rev'] != $mutex_rev) {
            throw new InvalidRequestArgException('mutex_rev', $mutex_rev, 'expired form data');
        }
        $after = $before = apply8021QOrder($vswitch['template_id'], getStored8021QConfig($vswitch['object_id'], 'desired'));
        $domain_vlanlist = getDomainVLANs($vswitch['domain_id']);
        $changes = filter8021QChangeRequests($domain_vlanlist, $before, apply8021QOrder($vswitch['template_id'], $changes));
        $desired_ports_count = count($changes);
        $changes = authorize8021QChangeRequests($before, $changes);
        if (count($changes) < $desired_ports_count) {
            showWarning(sprintf("Permission denied to change %d ports", $desired_ports_count - count($changes)));
        }
        foreach ($changes as $port_name => $port) {
            $after[$port_name] = $port;
        }
        $new_uplinks = filter8021QChangeRequests($domain_vlanlist, $after, produceUplinkPorts($domain_vlanlist, $after, $vswitch['object_id']));
        $npulled = replace8021QPorts('desired', $vswitch['object_id'], $before, $changes);
        $nsaved_uplinks = replace8021QPorts('desired', $vswitch['object_id'], $before, $new_uplinks);
        if ($npulled + $nsaved_uplinks) {
            touchVLANSwitch($vswitch['object_id']);
        }
        $dbxlink->commit();
    } catch (Exception $e) {
        $dbxlink->rollBack();
        showError(sprintf("Failed to update switchports: %s", $e->getMessage()));
        return 0;
    }
    $nsaved_downlinks = 0;
    if ($nsaved_uplinks) {
        $nsaved_downlinks = initiateUplinksReverb($vswitch['object_id'], $new_uplinks);
    }
    // instant deploy to that switch if configured
    $done = 0;
    if ($npulled + $nsaved_uplinks > 0 and getConfigVar('8021Q_INSTANT_DEPLOY') == 'yes') {
        try {
            if (FALSE === ($done = exec8021QDeploy($vswitch['object_id'], TRUE))) {
                showError("deploy was blocked due to conflicting configuration versions");
            } elseif ($verbose) {
                showSuccess(sprintf("Configuration for %u port(s) have been deployed", $done));
            }
        } catch (Exception $e) {
            showError(sprintf("Failed to deploy changes to switch: %s", $e->getMessage()));
        }
    }
    // report number of changed ports
    $total = $npulled + $nsaved_uplinks + $nsaved_downlinks;
    if ($verbose) {
        $message = sprintf('%u port(s) have been changed', $total);
        if ($total > 0) {
            showSuccess($message);
        } else {
            showNotice($message);
        }
    }
    return $total;
}