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)); }
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> </td></tr>'; echo '<tr><td class=tdright>without template role:</td><td> </td></tr>'; echo '<tr><td class=tdright>new data:</td><td class=trok> </td></tr>'; echo '<tr><td class=tdright>warnings in new data:</td><td class=trwarning> </td></tr>'; echo '<tr><td class=tdright>fatal errors in new data:</td><td class=trerror> </td></tr>'; echo '<tr><td class=tdright>deleted data:</td><td class=trnull> </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> </th><th colspan=3>discard</th><th> </th></tr>'; } echo '<tr valign=top><th>port</th><th width="40%">last saved 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 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 = ' '; $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 = ' '; $right_text = serializeVLANPack($item['right']); break; case 'ok_to_add': $trclass = 'trbusy'; $right_extra = ' trok'; $left_text = ' '; $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 = ' '; $right_text = ' '; break; case 'martian_conflict': if ($item['right']['mode'] == 'none') { $right_text = ' '; } else { $right_text = serializeVLANPack($item['right']); $right_extra = ' trerror'; } if ($item['left']['mode'] == 'none') { $left_text = ' '; } 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> </td><td> </td><td> </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> </td><td colspan=3 align=center class=tdcenter>'; printImageHREF('UNLOCK', 'resolve conflicts', TRUE); echo '</td><td> </td></tr>'; } echo '</table>'; echo '</form>'; finishPortlet(); echo '</td></tr></table>'; }
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> </td></tr>'; echo '<tr><td class=tdright>without template role:</td><td> </td></tr>'; echo '<tr><td class=tdright>new data:</td><td class=trok> </td></tr>'; echo '<tr><td class=tdright>warnings in new data:</td><td class=trwarning> </td></tr>'; echo '<tr><td class=tdright>fatal errors in new data:</td><td class=trerror> </td></tr>'; echo '<tr><td class=tdright>deleted data:</td><td class=trnull> </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>'; }
function exec8021QDeploy($object_id, $do_push) { global $dbxlink; $nsaved = $npushed = $nsaved_uplinks = 0; $dbxlink->beginTransaction(); if (NULL === ($vswitch = getVLANSwitchInfo($object_id, 'FOR UPDATE'))) { throw new InvalidArgException('object_id', $object_id, 'VLAN domain is not set for this object'); } $D = getStored8021QConfig($vswitch['object_id'], 'desired'); $C = getStored8021QConfig($vswitch['object_id'], 'cached'); try { $R = getRunning8021QConfig($vswitch['object_id']); } catch (RTGatewayError $e) { usePreparedExecuteBlade('UPDATE VLANSwitch SET last_errno=?, last_error_ts=NOW() WHERE object_id=?', array(E_8021Q_PULL_REMOTE_ERROR, $vswitch['object_id'])); $dbxlink->commit(); return 0; } $conflict = FALSE; $ok_to_push = array(); foreach (get8021QSyncOptions($vswitch, $D, $C, $R['portdata']) as $pn => $port) { // always update cache with new data from switch switch ($port['status']) { case 'ok_to_merge': // FIXME: this can be logged upd8021QPort('cached', $vswitch['object_id'], $pn, $port['both']); break; case 'ok_to_delete': del8021QPort($vswitch['object_id'], $pn); $nsaved++; break; case 'ok_to_add': add8021QPort($vswitch['object_id'], $pn, $port['right']); $nsaved++; break; case 'delete_conflict': case 'merge_conflict': case 'add_conflict': case 'martian_conflict': $conflict = TRUE; break; case 'ok_to_pull': // FIXME: this can be logged upd8021QPort('desired', $vswitch['object_id'], $pn, $port['right']); upd8021QPort('cached', $vswitch['object_id'], $pn, $port['right']); $nsaved++; break; case 'ok_to_push_with_merge': upd8021QPort('cached', $vswitch['object_id'], $pn, $port['right']); // fall through // fall through case 'ok_to_push': $ok_to_push[$pn] = $port['left']; break; } } // redo uplinks unconditionally $domain_vlanlist = getDomainVLANs($vswitch['domain_id']); $Dnew = apply8021QOrder($vswitch['template_id'], getStored8021QConfig($vswitch['object_id'], 'desired')); // Take new "desired" configuration and derive uplink port configuration // from it. Then cancel changes to immune VLANs and save resulting // changes (if any left). $new_uplinks = filter8021QChangeRequests($domain_vlanlist, $Dnew, produceUplinkPorts($domain_vlanlist, $Dnew, $vswitch['object_id'])); $nsaved_uplinks += replace8021QPorts('desired', $vswitch['object_id'], $Dnew, $new_uplinks); if ($nsaved + $nsaved_uplinks) { // saved configuration has changed (either "user" ports have changed, // or uplinks, or both), so bump revision number up) touchVLANSwitch($vswitch['object_id']); } if ($conflict) { usePreparedExecuteBlade('UPDATE VLANSwitch SET out_of_sync="yes", last_errno=?, last_error_ts=NOW() WHERE object_id=?', array(E_8021Q_VERSION_CONFLICT, $vswitch['object_id'])); } else { usePreparedExecuteBlade('UPDATE VLANSwitch SET last_errno=?, last_error_ts=NOW() WHERE object_id=?', array(E_8021Q_NOERROR, $vswitch['object_id'])); // Modified uplinks are very likely to differ from those in R-copy, // so don't mark device as clean, if this happened. This can cost // us an additional, empty round of sync, but at least out_of_sync // won't be mistakenly set to 'no'. // FIXME: A cleaner way of coupling pull and push operations would // be to split this function into two. if (!count($ok_to_push) and !$nsaved_uplinks) { usePreparedExecuteBlade('UPDATE VLANSwitch SET out_of_sync="no" WHERE object_id=?', array($vswitch['object_id'])); } elseif ($do_push) { usePreparedExecuteBlade('UPDATE VLANSwitch SET last_push_started=NOW() WHERE object_id=?', array($vswitch['object_id'])); try { $vlan_names = isset($R['vlannames']) ? $R['vlannames'] : array(); $npushed += exportSwitch8021QConfig($vswitch, $R['vlanlist'], $R['portdata'], $ok_to_push, $vlan_names); // update cache for ports deployed replace8021QPorts('cached', $vswitch['object_id'], $R['portdata'], $ok_to_push); usePreparedExecuteBlade('UPDATE VLANSwitch SET last_push_finished=NOW(), out_of_sync="no", last_errno=? WHERE object_id=?', array(E_8021Q_NOERROR, $vswitch['object_id'])); } catch (RTGatewayError $r) { usePreparedExecuteBlade('UPDATE VLANSwitch SET out_of_sync="yes", last_error_ts=NOW(), last_errno=? WHERE object_id=?', array(E_8021Q_PUSH_REMOTE_ERROR, $vswitch['object_id'])); callHook('pushErrorHandler', $object_id, $r); } } } $dbxlink->commit(); // start downlink work only after unlocking current object to make deadlocks less likely to happen // TODO: only process changed uplink ports if ($nsaved_uplinks) { initiateUplinksReverb($vswitch['object_id'], $new_uplinks); } return $nsaved + $npushed + $nsaved_uplinks; }