From 7c539fd8736b0ff9acafe32d857b2a2021d778e6 Mon Sep 17 00:00:00 2001 From: Simon Rettberg Date: Wed, 31 Jul 2019 16:58:14 +0200 Subject: [locations] Add warnings/cleanup for bad machine to roomplan mappings --- modules-available/locations/pages/cleanup.inc.php | 93 +++++++ modules-available/locations/pages/details.inc.php | 310 +++++++++++++++++++++ .../locations/pages/locations.inc.php | 271 ++++++++++++++++++ modules-available/locations/pages/subnets.inc.php | 79 ++++++ 4 files changed, 753 insertions(+) create mode 100644 modules-available/locations/pages/cleanup.inc.php create mode 100644 modules-available/locations/pages/details.inc.php create mode 100644 modules-available/locations/pages/locations.inc.php create mode 100644 modules-available/locations/pages/subnets.inc.php (limited to 'modules-available/locations/pages') diff --git a/modules-available/locations/pages/cleanup.inc.php b/modules-available/locations/pages/cleanup.inc.php new file mode 100644 index 00000000..d10dbac0 --- /dev/null +++ b/modules-available/locations/pages/cleanup.inc.php @@ -0,0 +1,93 @@ + $delete]); + Message::addSuccess('reset-n-machines', $num); + } + + private static function moveMachines() + { + $move = self::getSelectedMachines(false); + if ($move === false) + return; + // Move to subnet's location, or NULL if position field was empty (Which should never be the case) + $num = Database::exec("UPDATE machine SET fixedlocationid = If(Length(position) > 0, subnetlocationid, NULL) WHERE machineuuid IN (:machines)", + ['machines' => $move]); + Message::addSuccess('moved-n-machines', $num); + } + + private static function getSelectedMachines($forDelete) + { + $list = self::loadForLocation(); + if ($list === false) + return false; + $machines = Request::post('machines', false, 'array'); + if ($machines === false) { + Message::addError('main.parameter-missing', 'machines'); + return false; + } + $valid = array_map(function($item) use ($forDelete) { + return $item['canmove'] || $forDelete ? $item['machineuuid'] : 'x'; + }, $list['clients']); + $retList = array_filter($machines, function($item) use ($valid) { + return in_array($item, $valid); + }); + if (empty($retList)) { + Message::addError('no-valid-machines-selected'); + return false; + } + return $retList; + } + + private static function loadForLocation() + { + $locationid = Request::any('locationid', false, 'int'); + if ($locationid === false) { + Message::addError('main.parameter-missing', 'locationid'); + return false; + } + $list = LocationUtil::getMachinesWithLocationMismatch($locationid, true); + if (empty($list)) { + Message::addInfo('no-mismatch-location'); + return false; + } + return $list; + } + +} \ No newline at end of file diff --git a/modules-available/locations/pages/details.inc.php b/modules-available/locations/pages/details.inc.php new file mode 100644 index 00000000..6acf31bf --- /dev/null +++ b/modules-available/locations/pages/details.inc.php @@ -0,0 +1,310 @@ + $locationId)); + if ($location === false) { + Message::addError('main.value-invalid', 'locationid', $locationId); + Util::redirect('?do=Locations'); + } + $change = false; + // Delete location? + if ($locationId === $del) { + User::assertPermission("location.delete", $locationId, '?do=locations'); + self::deleteLocation($location); + $change = true; + } + // Update subnets + $change |= self::updateLocationSubnets(); + // Insert subnets + $change |= self::addNewLocationSubnets($location); + // Update location! + $change |= self::updateLocationData($location); + + if ($change) { + // In case subnets or tree layout changed, recalc this + AutoLocation::rebuildAll(); + } + Util::redirect('?do=Locations'); + } + + private static function deleteLocation($location) + { + $locationId = (int)$location['locationid']; + if (Request::post('recursive', false) === 'on') { + $rows = Location::queryLocations(); + $rows = Location::buildTree($rows, $locationId); + $ids = Location::extractIds($rows); + } else { + $ids = [$locationId]; + } + $locs = Database::exec("DELETE FROM location WHERE locationid IN (:ids)", ['ids' => $ids]); + Database::exec('UPDATE location SET parentlocationid = :newparent WHERE parentlocationid = :oldparent', array( + 'newparent' => $location['parentlocationid'], + 'oldparent' => $location['locationid'] + )); + AutoLocation::rebuildAll($ids); + Message::addSuccess('location-deleted', $locs, implode(', ', $ids)); + Util::redirect('?do=Locations'); + } + + private static function updateLocationData($location) + { + $locationId = (int)$location['locationid']; + $newParent = Request::post('parentlocationid', false, 'integer'); + $newName = Request::post('locationname', false, 'string'); + if (!User::hasPermission('location.edit.name', $locationId)) { + $newName = $location['locationname']; + } elseif ($newName === false || preg_match('/^\s*$/', $newName)) { + if ($newName !== false) { + Message::addWarning('main.value-invalid', 'location name', $newName); + } + $newName = $location['locationname']; + } + if ($newParent === false || !User::hasPermission('location.edit.parent', $locationId) + || !User::hasPermission('location.edit.parent', $newParent) + || !User::hasPermission('location.edit.*', $location['parentlocationid'])) { + $newParent = $location['parentlocationid']; + } else if ($newParent !== 0) { + $rows = Location::queryLocations(); + $all = Location::extractIds(Location::buildTree($rows)); + if (!in_array($newParent, $all) || $newParent === $locationId) { + Message::addWarning('main.value-invalid', 'parent', $newParent); + $newParent = $location['parentlocationid']; + } else { + $rows = Location::extractIds(Location::buildTree($rows, $locationId)); + if (in_array($newParent, $rows)) { + Message::addWarning('main.value-invalid', 'parent', $newParent); + $newParent = $location['parentlocationid']; + } + } + } + // TODO: Check permissions for new parent (only if changed) + $ret = Database::exec('UPDATE location SET parentlocationid = :parent, locationname = :name' + . ' WHERE locationid = :lid', array( + 'lid' => $locationId, + 'parent' => $newParent, + 'name' => $newName + )); + if ($ret > 0) { + Message::addSuccess('location-updated', $newName); + } + return $newParent != $location['parentlocationid']; + } + + private static function updateLocationSubnets() + { + $locationId = Request::post('locationid', false, 'integer'); + if (!User::hasPermission('location.edit.subnets', $locationId)) + return false; + + $change = false; + + // Deletion first + $dels = Request::post('deletesubnet', false); + if (is_array($dels)) { + $count = 0; + $stmt = Database::prepare('DELETE FROM subnet WHERE subnetid = :id'); + foreach ($dels as $key => $value) { + if (!is_numeric($key) || $value !== 'on') + continue; + if ($stmt->execute(array('id' => $key))) { + $count += $stmt->rowCount(); + } + } + if ($count > 0) { + Message::addInfo('subnets-deleted', $count); + $change = true; + } + } + + // Now actual updates + $starts = Request::post('startaddr', false); + $ends = Request::post('endaddr', false); + if (!is_array($starts) || !is_array($ends)) { + return $change; + } + $count = 0; + $stmt = Database::prepare('UPDATE subnet SET startaddr = :start, endaddr = :end' + . ' WHERE subnetid = :id'); + foreach ($starts as $key => $start) { + if (!isset($ends[$key]) || !is_numeric($key)) + continue; + $end = $ends[$key]; + $range = LocationUtil::rangeToLongVerbose($start, $end); + if ($range === false) + continue; + list($startLong, $endLong) = $range; + if ($stmt->execute(array('id' => $key, 'start' => $startLong, 'end' => $endLong))) { + $count += $stmt->rowCount(); + } + } + if ($count > 0) { + Message::addInfo('subnets-updated', $count); + $change = true; + } + return $change; + } + + private static function addNewLocationSubnets($location) + { + $locationId = (int)$location['locationid']; + if (!User::hasPermission('location.edit.subnets', $locationId)) + return false; + + $change = false; + $starts = Request::post('newstartaddr', false); + $ends = Request::post('newendaddr', false); + if (!is_array($starts) || !is_array($ends)) { + return $change; + } + $count = 0; + $stmt = Database::prepare('INSERT INTO subnet SET startaddr = :start, endaddr = :end, locationid = :location'); + foreach ($starts as $key => $start) { + if (!isset($ends[$key]) || !is_numeric($key)) + continue; + $end = $ends[$key]; + list($startLong, $endLong) = LocationUtil::rangeToLong($start, $end); + if ($startLong === false) { + Message::addWarning('main.value-invalid', 'new start addr', $start); + } + if ($endLong === false) { + Message::addWarning('main.value-invalid', 'new end addr', $start); + } + if ($startLong === false || $endLong === false) + continue; + if ($startLong > $endLong) { + Message::addWarning('main.value-invalid', 'range', $start . ' - ' . $end); + continue; + } + if ($stmt->execute(array('location' => $locationId, 'start' => $startLong, 'end' => $endLong))) { + $count += $stmt->rowCount(); + } + } + if ($count > 0) { + Message::addInfo('subnets-created', $count); + $change = true; + } + return $change; + } + + private static function ajaxShowLocation() + { + $locationId = Request::any('locationid', 0, 'integer'); + + User::assertPermission("location.view", $locationId); + + $loc = Database::queryFirst('SELECT locationid, parentlocationid, locationname FROM location WHERE locationid = :lid', + array('lid' => $locationId)); + if ($loc === false) { + die('Unknown locationid'); + } + $res = Database::simpleQuery("SELECT subnetid, startaddr, endaddr FROM subnet WHERE locationid = :lid", + array('lid' => $locationId)); + $rows = array(); + while ($row = $res->fetch(PDO::FETCH_ASSOC)) { + $row['startaddr'] = long2ip($row['startaddr']); + $row['endaddr'] = long2ip($row['endaddr']); + $rows[] = $row; + } + $data = array( + 'locationid' => $loc['locationid'], + 'locationname' => $loc['locationname'], + 'list' => $rows, + 'roomplanner' => Module::get('roomplanner') !== false, + 'parents' => Location::getLocations($loc['parentlocationid'], $locationId, true) + ); + + // Disable locations in the parent selector where the user cannot change to + if (!User::hasPermission('location.edit.*', $loc['parentlocationid']) + || !User::hasPermission('location.edit.parent', $locationId)) { + $allowedLocs = []; + } else { + $allowedLocs = User::getAllowedLocations("location.edit.*"); + foreach ($data['parents'] as &$parent) { + if (!(in_array($parent["locationid"], $allowedLocs) || $parent["locationid"] == $loc['parentlocationid'])) { + $parent["disabled"] = "disabled"; + } + } + } + + if (Module::get('dozmod') !== false) { + $lectures = Database::queryFirst('SELECT Count(*) AS cnt FROM sat.lecture l ' + . ' INNER JOIN sat.lecture_x_location ll ON (l.lectureid = ll.lectureid AND ll.locationid = :lid)', + array('lid' => $locationId)); + $data['lectures'] = $lectures['cnt']; + $data['haveDozmod'] = true; + } + // Get clients matching this location's subnet(s) + $count = $online = $used = 0; + if (Module::get('statistics') !== false) { + $mres = Database::simpleQuery("SELECT state FROM machine" + . " WHERE machine.locationid = :lid", array('lid' => $locationId)); + while ($row = $mres->fetch(PDO::FETCH_ASSOC)) { + $count++; + if ($row['state'] === 'IDLE') { + $online++; + } + if ($row['state'] === 'OCCUPIED') { + $online++; + $used++; + } + } + $data['haveStatistics'] = true; + // Link + if (User::hasPermission('.statistics.view.list')) { + $data['statsLink'] = 'list'; + } elseif (User::hasPermission('.statistics.view.summary')) { + $data['statsLink'] = 'summary'; + } + } + $data['machines'] = $count; + $data['machines_online'] = $online; + $data['machines_used'] = $used; + $data['used_percent'] = $count === 0 ? 0 : round(($used / $count) * 100); + + + Permission::addGlobalTags($data['perms'], $locationId, ['location.edit.name', 'location.edit.subnets', 'location.delete', '.roomplanner.edit'], 'save_button'); + if (empty($allowedLocs)) { + $data['perms']['location']['edit']['parent']['disabled'] = 'disabled'; + } else { + unset($data['perms']['save_button']); + } + + echo Render::parse('location-subnets', $data); + } + +} \ No newline at end of file diff --git a/modules-available/locations/pages/locations.inc.php b/modules-available/locations/pages/locations.inc.php new file mode 100644 index 00000000..275aafdb --- /dev/null +++ b/modules-available/locations/pages/locations.inc.php @@ -0,0 +1,271 @@ + $name) { + $name = trim($name); + if (empty($name)) + continue; + $parent = isset($parents[$idx]) ? (int)$parents[$idx] : 0; + if (!User::hasPermission("location.add", $parent)) { + Message::addError('no-permission-location', isset($locs[$parent]) ? $locs[$parent]['locationname'] : $parent); + continue; + } + if ($parent !== 0) { + $ok = false; + foreach ($locs as $loc) { + if ($loc['locationid'] == $parent) { + $ok = true; + } + } + if (!$ok) { + Message::addWarning('main.value-invalid', 'parentlocationid', $parent); + continue; + } + } + Database::exec("INSERT INTO location (parentlocationid, locationname)" + . " VALUES (:parent, :name)", array( + 'parent' => $parent, + 'name' => $name + )); + $count++; + } + Message::addSuccess('added-x-entries', $count); + Util::redirect('?do=Locations'); + } + + public static function showLocationList() + { + // Warn admin about overlapping subnet definitions + $overlapSelf = $overlapOther = true; + LocationUtil::getOverlappingSubnets($overlapSelf, $overlapOther); + // Find machines assigned to a room with a UUID mismatch + $mismatchMachines = LocationUtil::getMachinesWithLocationMismatch(0, true); + $locationList = Location::getLocationsAssoc(); + unset($locationList[0]); + // Statistics: Count machines for each subnet + $unassigned = false; + $unassignedLoad = 0; + + // Filter view: Remove locations we can't reach at all, but show parents to locations + // we have permission to, so the tree doesn't look all weird + $visibleLocationIds = $allowedLocationIds = User::getAllowedLocations("location.view"); + foreach ($allowedLocationIds as $lid) { + if (!isset($locationList[$lid])) + continue; + $visibleLocationIds = array_merge($visibleLocationIds, $locationList[$lid]['parents']); + } + $visibleLocationIds = array_unique($visibleLocationIds); + foreach (array_keys($locationList) as $lid) { + if (User::hasPermission('.baseconfig.view', $lid)) { + $visibleLocationIds[] = $lid; + } else { + $locationList[$lid]['havebaseconfig'] = false; + } + if (User::hasPermission('.sysconfig.config.view-list', $lid)) { + $visibleLocationIds[] = $lid; + } else { + $locationList[$lid]['havesysconfig'] = false; + } + if (User::hasPermission('.statistics.view.list', $lid)) { + $visibleLocationIds[] = $lid; + } else { + $locationList[$lid]['havestatistics'] = false; + } + if (User::hasPermission('.serversetup.ipxe.menu.assign', $lid)) { + $visibleLocationIds[] = $lid; + } else { + $locationList[$lid]['haveipxe'] = false; + } + if (!in_array($lid, $visibleLocationIds)) { + unset($locationList[$lid]); + } elseif (!in_array($lid, $allowedLocationIds)) { + $locationList[$lid]['show-only'] = true; + } + } + + // Client statistics + if (Module::get('statistics') !== false) { + $unassigned = 0; + $extra = ''; + if (in_array(0, $allowedLocationIds)) { + $extra = ' OR locationid IS NULL'; + } + $res = Database::simpleQuery("SELECT locationid, Count(*) AS cnt, Sum(If(state = 'OCCUPIED', 1, 0)) AS used + FROM machine WHERE (locationid IN (:allowedLocationIds) $extra) GROUP BY locationid", compact('allowedLocationIds')); + while ($row = $res->fetch(PDO::FETCH_ASSOC)) { + $locId = (int)$row['locationid']; + if (isset($locationList[$locId])) { + $locationList[$locId]['clientCount'] = $row['cnt']; + $locationList[$locId]['clientLoad'] = round(100 * $row['used'] / $row['cnt']) . ' %'; + } else { + $unassigned += $row['cnt']; + $unassignedLoad += $row['used']; + } + } + unset($loc); + foreach ($locationList as &$loc) { + if (!in_array($loc['locationid'], $allowedLocationIds)) + continue; + if (!isset($loc['clientCountSum'])) { + $loc['clientCountSum'] = 0; + } + if (!isset($loc['clientCount'])) { + $loc['clientCount'] = 0; + $loc['clientLoad'] = '0%'; + } + $loc['clientCountSum'] += $loc['clientCount']; + foreach ($loc['parents'] as $pid) { + if (!in_array($pid, $allowedLocationIds)) + continue; + $locationList[(int)$pid]['hasChild'] = true; + $locationList[(int)$pid]['clientCountSum'] += $loc['clientCount']; + } + } + unset($loc); + } + // Show currently active sysconfig for each location + $defaultConfig = false; + if (Module::isAvailable('sysconfig')) { + $confs = SysConfig::getAll(); + foreach ($confs as $conf) { + if (strlen($conf['locs']) === 0) + continue; + $confLocs = explode(',', $conf['locs']); + foreach ($confLocs as $locId) { + settype($locId, 'int'); + if ($locId === 0) { + $defaultConfig = $conf['title']; + } + if (!isset($locationList[$locId])) + continue; + $locationList[$locId] += array('configName' => $conf['title'], 'configClass' => 'slx-bold'); + } + } + self::propagateFields($locationList, $defaultConfig, 'configName', 'configClass'); + } + // Count overridden config vars + if (Module::get('baseconfig') !== false) { + $res = Database::simpleQuery("SELECT locationid, Count(*) AS cnt FROM `setting_location` + WHERE locationid IN (:allowedLocationIds) GROUP BY locationid", compact('allowedLocationIds')); + while ($row = $res->fetch(PDO::FETCH_ASSOC)) { + $lid = (int)$row['locationid']; + if (isset($locationList[$lid])) { + $locationList[$lid]['overriddenVars'] = $row['cnt']; + } + } + // Confusing because the count might be inaccurate within a branch + //$this->propagateFields($locationList, '', 'overriddenVars', 'overriddenClass'); + } + // Show ipxe menu + if (Module::isAvailable('serversetup') && class_exists('IPxe')) { + $res = Database::simpleQuery("SELECT ml.locationid, m.title, ml.defaultentryid FROM serversetup_menu m + INNER JOIN serversetup_menu_location ml USING (menuid) + WHERE locationid IN (:allowedLocationIds) GROUP BY locationid", compact('allowedLocationIds')); + while ($row = $res->fetch(PDO::FETCH_ASSOC)) { + $lid = (int)$row['locationid']; + if (isset($locationList[$lid])) { + if ($row['defaultentryid'] !== null) { + $row['title'] .= '(*)'; + } + $locationList[$lid]['customMenu'] = $row['title']; + } + } + self::propagateFields($locationList, '', 'customMenu', 'customMenuClass'); + } + + $addAllowedLocs = User::getAllowedLocations("location.add"); + $addAllowedList = Location::getLocations(0, 0, true); + foreach ($addAllowedList as &$loc) { + if (!in_array($loc["locationid"], $addAllowedLocs)) { + $loc["disabled"] = "disabled"; + } + } + unset($loc); + + // Output + $data = array( + 'list' => array_values($locationList), + 'havestatistics' => Module::get('statistics') !== false, + 'havebaseconfig' => Module::get('baseconfig') !== false, + 'havesysconfig' => Module::get('sysconfig') !== false, + 'haveipxe' => Module::isAvailable('serversetup') && class_exists('IPxe'), + 'overlapSelf' => $overlapSelf, + 'overlapOther' => $overlapOther, + 'mismatchMachines' => $mismatchMachines, + 'unassignedCount' => $unassigned, + 'unassignedLoad' => ($unassigned ? (round(($unassignedLoad / $unassigned) * 100) . ' %') : ''), + 'defaultConfig' => $defaultConfig, + 'addAllowedList' => array_values($addAllowedList), + ); + // TODO: Buttons for config vars and sysconfig are currently always shown, as their availability + // depends on permissions in the according modules, not this one + Permission::addGlobalTags($data['perms'], NULL, ['subnets.edit', 'location.add']); + Render::addTemplate('locations', $data); + } + + private static function propagateFields(&$locationList, $defaultValue, $name, $class) + { + $depth = array(); + foreach ($locationList as &$loc) { + $d = $loc['depth']; + if (!isset($loc[$name])) { + // Has no explicit config assignment + if ($d === 0) { + $loc[$name] = $defaultValue; + } else { + $loc[$name] = $depth[$d - 1]; + } + $loc[$class] = 'gray'; + } + $depth[$d] = $loc[$name]; + unset($depth[$d + 1]); + } + } + +} \ No newline at end of file diff --git a/modules-available/locations/pages/subnets.inc.php b/modules-available/locations/pages/subnets.inc.php new file mode 100644 index 00000000..6c37129a --- /dev/null +++ b/modules-available/locations/pages/subnets.inc.php @@ -0,0 +1,79 @@ + $start) { + if (!isset($ends[$subnetid]) || !isset($locs[$subnetid])) + continue; + $loc = (int)$locs[$subnetid]; + $end = $ends[$subnetid]; + if (!isset($existingLocs[$loc])) { + Message::addError('main.value-invalid', 'locationid', $loc); + continue; + } + $range = LocationUtil::rangeToLongVerbose($start, $end); + if ($range === false) + continue; + list($startLong, $endLong) = $range; + if ($stmt->execute(compact('startLong', 'endLong', 'loc', 'subnetid'))) { + $count += $stmt->rowCount(); + } + } + AutoLocation::rebuildAll(); + Message::addSuccess('subnets-updated', $count); + Util::redirect('?do=Locations'); + } + + public static function doRender($getAction) + { + if ($getAction === false) { + User::assertPermission('subnets.edit', NULL, '?do=locations'); + $res = Database::simpleQuery("SELECT subnetid, startaddr, endaddr, locationid FROM subnet"); + $rows = array(); + while ($row = $res->fetch(PDO::FETCH_ASSOC)) { + $row['startaddr'] = long2ip($row['startaddr']); + $row['endaddr'] = long2ip($row['endaddr']); + $row['locations'] = Location::getLocations($row['locationid']); + $rows[] = $row; + } + $data = array('list' => $rows); + Permission::addGlobalTags($data['perms'], NULL, ['location.view']); + Render::addTemplate('subnets', $data); + return true; + } + return false; + } + + public static function doAjax($action) + { + return false; + } + + /* + * Helpers + */ + +} \ No newline at end of file -- cgit v1.2.3-55-g7522