diff --git a/CHANGELOG.md b/CHANGELOG.md index eb8a6d8..b6f3c5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,4 +5,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). -## [unreleased] - \ No newline at end of file +## [unreleased] - + +### Added + +- Enrich computer inventory builder templates with all inventory schema fields (bios, operatingsystem, ...). + +### Fixed + +- Empty sections no longer fall back to the default template when the DB columns are null. +- Avoid multiple scrollbars on the builder mapping edition view. \ No newline at end of file diff --git a/builder/computer/data/README.md b/builder/computer/data/README.md new file mode 100644 index 0000000..7019b04 --- /dev/null +++ b/builder/computer/data/README.md @@ -0,0 +1,53 @@ +# Computer Builder Data Templates + +These JSON files are the default templates used when creating a `ComputerBuilderMapping`. +Each file corresponds to a section of the GLPI Inventory Format schema. + +Fields containing `{{ ldap.attributeName }}` are replaced at sync time with the value +of the corresponding LDAP attribute. Empty fields (`""`) are stripped from the final +inventory payload and can be filled in with an LDAP placeholder or a static value. + +## bios.json + +Reference: [inventory.schema.json — bios](https://github.com/glpi-project/inventory_format/blob/89f177aa93595bdff01c3f670e32edd11ace0524/inventory.schema.json#L166-L247) + +| Field | Description | Notes | +|-------|-------------|-------| +| `ssn` | System serial number | | +| `smanufacturer` | System manufacturer | | +| `smodel` | System model | | +| `assettag` | Asset tag | | +| `bdate` | BIOS release date | Format: `dateordatetime` (e.g. `2023-10-15`) | +| `bmanufacturer` | BIOS manufacturer | | +| `bversion` | BIOS version | | +| `mmanufacturer` | Motherboard manufacturer | | +| `mmodel` | Motherboard model | | +| `msn` | Motherboard serial number | | +| `skunumber` | SKU number | | +| `biosserial` | BIOS serial number | | +| `enclosureserial` | Chassis serial number | | +| `secure_boot` | Secure boot status | Accepted values: `enabled`, `disabled`, `unsupported` | + +## operatingsystem.json + +Reference: [inventory.schema.json — operatingsystem](https://github.com/glpi-project/inventory_format/blob/89f177aa93595bdff01c3f670e32edd11ace0524/inventory.schema.json#L1312-L1436) + +| Field | Description | Notes | +|-------|-------------|-------| +| `name` | OS distributor name | | +| `version` | OS release version | | +| `arch` | Processor architecture | e.g. `x86_64` | +| `boot_time` | Last boot timestamp | Format: `datetime` | +| `dns_domain` | DNS domain | | +| `fqdn` | Fully qualified domain name | | +| `full_name` | Full OS description | e.g. `Fedora release 25 (Twenty Five)` | +| `hostid` | Unique host identifier | | +| `install_date` | OS installation date | Format: `dateordatetime` | +| `kernel_name` | Kernel type | e.g. `linux` | +| `kernel_version` | Kernel version | e.g. `4.11.3-200.fc25.x86_64` | +| `ssh_key` | SSH public key | | +| `service_pack` | Service pack version | | + +> **Note:** The `timezone` field (object with `name` and `offset`) is intentionally +> omitted from the default template. It can be added manually in the mapping if needed, +> but both `name` and `offset` must be set — a partial timezone object is invalid. diff --git a/builder/computer/data/bios.json b/builder/computer/data/bios.json new file mode 100644 index 0000000..51fcc6f --- /dev/null +++ b/builder/computer/data/bios.json @@ -0,0 +1,16 @@ +{ + "ssn": "{{ ldap.serialNumber }}", + "smanufacturer": "", + "smodel": "", + "assettag": "", + "bdate": "", + "bmanufacturer": "", + "bversion": "", + "mmanufacturer": "", + "mmodel": "", + "msn": "", + "skunumber": "", + "biosserial": "", + "enclosureserial": "", + "secure_boot": "" +} diff --git a/builder/computer/data/cpus.json b/builder/computer/data/cpus.json new file mode 100644 index 0000000..ed88f17 --- /dev/null +++ b/builder/computer/data/cpus.json @@ -0,0 +1,15 @@ +[ + { + "core": "", + "description": "", + "familynumber": "", + "id": "", + "manufacturer": "", + "model": "", + "name": "", + "serial": "", + "speed": "", + "stepping": "", + "thread": "" + } +] diff --git a/builder/computer/data/drives.json b/builder/computer/data/drives.json new file mode 100644 index 0000000..e9049cd --- /dev/null +++ b/builder/computer/data/drives.json @@ -0,0 +1,14 @@ +[ + { + "description": "", + "filesystem": "", + "free": "", + "label": "", + "letter": "", + "serial": "", + "systemdrive": "", + "total": "", + "type": "", + "volumn": "" + } +] diff --git a/builder/computer/data/memories.json b/builder/computer/data/memories.json new file mode 100644 index 0000000..9501ea3 --- /dev/null +++ b/builder/computer/data/memories.json @@ -0,0 +1,14 @@ +[ + { + "capacity": "", + "caption": "", + "description": "", + "manufacturer": "", + "memorycorrection": "", + "model": "", + "numslots": "", + "serialnumber": "", + "speed": "", + "type": "" + } +] diff --git a/builder/computer/data/networks.json b/builder/computer/data/networks.json new file mode 100644 index 0000000..5358f63 --- /dev/null +++ b/builder/computer/data/networks.json @@ -0,0 +1,11 @@ +[ + { + "description": "", + "mac": "{{ ldap.macAddress }}", + "pciid": "", + "pnpdeviceid": "", + "status": "", + "type": "", + "virtualdev": "" + } +] diff --git a/builder/computer/data/operatingsystem.json b/builder/computer/data/operatingsystem.json new file mode 100644 index 0000000..75696be --- /dev/null +++ b/builder/computer/data/operatingsystem.json @@ -0,0 +1,15 @@ +{ + "name": "{{ ldap.operatingSystem }}", + "version": "{{ ldap.operatingSystemVersion }}", + "arch": "", + "boot_time": "", + "dns_domain": "", + "fqdn": "", + "full_name": "", + "hostid": "", + "install_date": "", + "kernel_name": "", + "kernel_version": "", + "ssh_key": "", + "service_pack": "" +} diff --git a/builder/computer/data/storages.json b/builder/computer/data/storages.json new file mode 100644 index 0000000..3afb895 --- /dev/null +++ b/builder/computer/data/storages.json @@ -0,0 +1,13 @@ +[ + { + "description": "", + "disksize": "", + "firmware": "", + "interface": "", + "manufacturer": "", + "model": "", + "name": "", + "serial": "", + "type": "" + } +] diff --git a/front/authldapsyncfilter.form.php b/front/authldapsyncfilter.form.php index 9510714..e79a72f 100644 --- a/front/authldapsyncfilter.form.php +++ b/front/authldapsyncfilter.form.php @@ -35,11 +35,10 @@ $relation = new AuthLdapSyncFilter(); if (isset($_POST["add"])) { + /** @var array $input ($_POST keys are always strings) */ $input = $_POST; - $relation->check(-1, CREATE, $input); // @phpstan-ignore argument.type ($_POST keys are always strings) - if ($input !== null) { - $relation->add($input); - } + $relation->check(-1, CREATE, $input); + $relation->add($input); } Html::back(); diff --git a/src/ComputerBuilderMapping.php b/src/ComputerBuilderMapping.php index 9a665e1..8828ca4 100644 --- a/src/ComputerBuilderMapping.php +++ b/src/ComputerBuilderMapping.php @@ -61,7 +61,7 @@ protected static function getTemplateBasePath(): string public static function getSectionNames(): array { - return ['main', 'hardware']; + return ['main', 'hardware', 'bios', 'operatingsystem', 'networks', 'cpus', 'memories', 'drives', 'storages']; } public static function install(Migration $migration): void @@ -81,6 +81,13 @@ public static function install(Migration $migration): void `id` int {$default_key_sign} NOT NULL AUTO_INCREMENT, `main` text, `hardware` text, + `bios` text, + `operatingsystem` text, + `networks` text, + `cpus` text, + `memories` text, + `drives` text, + `storages` text, `date_creation` timestamp NULL DEFAULT NULL, `date_mod` timestamp NULL DEFAULT NULL, PRIMARY KEY (`id`), @@ -89,6 +96,16 @@ public static function install(Migration $migration): void ) ENGINE=InnoDB DEFAULT CHARSET={$default_charset} COLLATE={$default_collation} ROW_FORMAT=DYNAMIC"; $DB->doQuery($query); + } else { + // Add new columns for existing installations + $migration->addField($table, 'bios', 'text', ['after' => 'hardware']); + $migration->addField($table, 'operatingsystem', 'text', ['after' => 'bios']); + $migration->addField($table, 'networks', 'text', ['after' => 'operatingsystem']); + $migration->addField($table, 'cpus', 'text', ['after' => 'networks']); + $migration->addField($table, 'memories', 'text', ['after' => 'cpus']); + $migration->addField($table, 'drives', 'text', ['after' => 'memories']); + $migration->addField($table, 'storages', 'text', ['after' => 'drives']); + $migration->executeMigration(); } } diff --git a/src/SyncFilter.php b/src/SyncFilter.php index fac591b..d28c47a 100644 --- a/src/SyncFilter.php +++ b/src/SyncFilter.php @@ -287,6 +287,10 @@ public function showBuilderMappingTab(): void } $content = $builder->getSection($section_name); + if ($content === []) { + $content = $builder_itemtype::loadDefaultTemplate($section_name); + } + $sections[] = [ 'name' => $section_name, 'content' => json_encode($content, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE), diff --git a/templates/builder_mapping.html.twig b/templates/builder_mapping.html.twig index c16522e..c6ae600 100644 --- a/templates/builder_mapping.html.twig +++ b/templates/builder_mapping.html.twig @@ -82,7 +82,7 @@ {{ __('Reset', 'advancedldap') }} -
+
{% endfor %} @@ -111,13 +111,27 @@ window.GLPI.Monaco.createEditor(containerId, 'twig', section.content, completions).then((monaco) => { editors[section.name] = monaco; + const container = document.getElementById(containerId); + + monaco.editor.updateOptions({ + scrollBeyondLastLine: false, + scrollbar: { vertical: 'hidden', alwaysConsumeMouseWheel: false }, + automaticLayout: false, + }); + + const updateHeight = () => { + const height = monaco.editor.getContentHeight(); + container.style.height = `${height}px`; + monaco.editor.layout({ width: container.offsetWidth, height }); + }; + + monaco.editor.onDidContentSizeChange(updateHeight); + updateHeight(); + // Update hidden input on change monaco.editor.onDidChangeModelContent(() => { document.getElementById(inputId).value = monaco.editor.getValue(); }); - - // Force layout after creation - monaco.editor.layout(); }); }); }); diff --git a/tests/ComputerBuilderMappingTest.php b/tests/ComputerBuilderMappingTest.php new file mode 100644 index 0000000..326b1c3 --- /dev/null +++ b/tests/ComputerBuilderMappingTest.php @@ -0,0 +1,118 @@ +. + * ------------------------------------------------------------------------- + * @copyright Copyright (C) 2018-2023 by Teclib'. + * @license GPLv3+ https://www.gnu.org/licenses/gpl-3.0.html + * @link https://services.glpi-network.com + * ------------------------------------------------------------------------- + */ + +namespace GlpiPlugin\Advancedldap\Tests; + +use Glpi\Tests\DbTestCase; +use GlpiPlugin\Advancedldap\ComputerBuilderMapping; + +use function Safe\json_encode; + +/** + * @method void assertTrue($condition, string $message = '') + * @method void assertFalse($condition, string $message = '') + * @method void assertEquals($expected, $actual, string $message = '') + * @method void assertNotFalse($condition, string $message = '') + * @method void assertNotEmpty($actual, string $message = '') + * @method void assertIsArray($actual, string $message = '') + * @method void assertGreaterThan($expected, $actual, string $message = '') + * @method void assertArrayHasKey($key, $array, string $message = '') + * @method void assertIsInt($actual, string $message = '') + */ +final class ComputerBuilderMappingTest extends DbTestCase +{ + public function testCreateWithDefaultsPopulatesAllSections(): void + { + $mapping = new ComputerBuilderMapping(); + $id = $mapping->createWithDefaults(); + + $this->assertIsInt($id); + /** @var int $id */ + $this->assertGreaterThan(0, $id); + + $loaded = new ComputerBuilderMapping(); + $loaded->getFromDB($id); + + foreach (['main', 'hardware', 'bios', 'operatingsystem', 'networks', 'cpus', 'memories', 'drives', 'storages'] as $section) { + $content = $loaded->getSection($section); + $this->assertIsArray($content); + $this->assertNotEmpty($content); + } + } + + public function testGetSectionReturnsExpectedKeys(): void + { + $mapping = new ComputerBuilderMapping(); + $id = $mapping->createWithDefaults(); + $this->assertIsInt($id); + /** @var int $id */ + + $loaded = new ComputerBuilderMapping(); + $loaded->getFromDB($id); + + $hardware = $loaded->getSection('hardware'); + $this->assertIsArray($hardware); + $this->assertArrayHasKey('name', $hardware); + + $bios = $loaded->getSection('bios'); + $this->assertIsArray($bios); + $this->assertArrayHasKey('ssn', $bios); + + $os = $loaded->getSection('operatingsystem'); + $this->assertIsArray($os); + $this->assertArrayHasKey('name', $os); + } + + public function testResetSectionRestoresDefaultTemplate(): void + { + $mapping = new ComputerBuilderMapping(); + $id = $mapping->createWithDefaults(); + $this->assertIsInt($id); + /** @var int $id */ + + $loaded = new ComputerBuilderMapping(); + $loaded->getFromDB($id); + + $loaded->update([ + 'id' => $id, + 'hardware' => json_encode(['name' => 'modified']), + ]); + + $loaded->getFromDB($id); + $this->assertEquals(['name' => 'modified'], $loaded->getSection('hardware')); + + $loaded->resetSection('hardware'); + + $loaded->getFromDB($id); + + $default = ComputerBuilderMapping::loadDefaultTemplate('hardware'); + $this->assertEquals($default, $loaded->getSection('hardware')); + } +} diff --git a/tests/SyncFilterTest.php b/tests/SyncFilterTest.php index 7d0311f..958d0f5 100644 --- a/tests/SyncFilterTest.php +++ b/tests/SyncFilterTest.php @@ -30,7 +30,9 @@ namespace GlpiPlugin\Advancedldap\Tests; +use Computer; use Glpi\Tests\DbTestCase; +use GlpiPlugin\Advancedldap\ComputerBuilderMapping; use GlpiPlugin\Advancedldap\SyncFilter; /** @@ -41,6 +43,8 @@ * @method void assertNotEmpty($actual, string $message = '') * @method void assertIsArray($actual, string $message = '') * @method void assertGreaterThan($expected, $actual, string $message = '') + * @method void assertArrayHasKey($key, $array, string $message = '') + * @method void assertIsInt($actual, string $message = '') */ final class SyncFilterTest extends DbTestCase { @@ -107,6 +111,55 @@ public function testUpdate(): void $this->assertEquals('Printer', $updatedfilter->getField('itemtype')); } + public function testCreateWithComputerItemtypeCreatesBuilderMapping(): void + { + $syncfilter = $this->createItem(SyncFilter::class, [ + 'name' => 'Computer Sync Filter', + 'connection_filter' => '(objectClass=computer)', + 'basedn' => 'ou=computers,dc=example,dc=com', + 'itemtype' => Computer::class, + ]); + + $loaded = new SyncFilter(); + $loaded->getFromDB($syncfilter->getID()); + + $this->assertEquals(ComputerBuilderMapping::class, $loaded->getField('builder_itemtype')); + $builder_id = $loaded->getField('builder_items_id'); + $this->assertIsInt($builder_id); + /** @var int $builder_id */ + $this->assertGreaterThan(0, $builder_id); + + $builder = new ComputerBuilderMapping(); + $builder->getFromDB($builder_id); + foreach (['main', 'hardware', 'bios', 'operatingsystem', 'networks', 'cpus', 'memories', 'drives', 'storages'] as $section) { + $this->assertNotEmpty($builder->getSection($section)); + } + } + + public function testPurgeSyncFilterDeletesBuilderMapping(): void + { + $syncfilter = $this->createItem(SyncFilter::class, [ + 'name' => 'Purgeable Filter', + 'connection_filter' => '(objectClass=computer)', + 'basedn' => 'ou=computers,dc=example,dc=com', + 'itemtype' => Computer::class, + ]); + $syncfilter_id = $syncfilter->getID(); + + $loaded = new SyncFilter(); + $loaded->getFromDB($syncfilter_id); + + $builder_id = $loaded->getField('builder_items_id'); + $this->assertIsInt($builder_id); + /** @var int $builder_id */ + $this->assertGreaterThan(0, $builder_id); + + $this->deleteItem(SyncFilter::class, $syncfilter_id, true); + + $builder = new ComputerBuilderMapping(); + $this->assertFalse($builder->getFromDB($builder_id)); + } + public function testDelete(): void { $totalSyncFilters = countElementsInTable(SyncFilter::getTable());