diff --git a/hook.php b/hook.php index 66dd3725..54d99277 100644 --- a/hook.php +++ b/hook.php @@ -295,7 +295,13 @@ function plugin_carbon_hook_pre_purge_assettype(CommonDBTM $item) ]); } -function plugin_carbon_MassiveActions($itemtype) +/** + * Massive actions enumeration hook + * + * @param class-string $itemtype + * @return array + */ +function plugin_carbon_MassiveActions($itemtype): array { switch ($itemtype) { case GlpiComputer::class: diff --git a/phpstan.neon b/phpstan.neon index c02769b3..bc4ce357 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -5,6 +5,7 @@ parameters: bootstrapFiles: - ../../stubs/glpi_constants.php - ../../vendor/autoload.php + - stubs/OptionalClass.php.stub paths: - src - front @@ -17,5 +18,6 @@ parameters: - ../../src stubFiles: - ../../stubs/glpi_constants.php + - ./stubs/OptionalClass.php.stub rules: - GlpiProject\Tools\PHPStan\Rules\GlobalVarTypeRule diff --git a/setup.php b/setup.php index f2aedb35..5bccce42 100644 --- a/setup.php +++ b/setup.php @@ -33,6 +33,7 @@ use Config as GlpiConfig; use CronTask as GlpiCronTask; use Glpi\Plugin\Hooks; +use GlpiPlugin\Carbon\CloudInventoryConnector; use GlpiPlugin\Carbon\Config; use GlpiPlugin\Carbon\CronTask; use GlpiPlugin\Carbon\Dashboard\Grid; @@ -115,6 +116,8 @@ function plugin_carbon_setupHooks() LcaClientFactory::getSecuredConfigs() ); + $PLUGIN_HOOKS[Hooks::POST_INIT]['carbon'] = [CloudInventoryConnector::class, 'checkPluginAvailability']; + // add new cards to the dashboard $PLUGIN_HOOKS[Hooks::DASHBOARD_CARDS]['carbon'] = [Grid::class, 'getDashboardCards']; $PLUGIN_HOOKS[Hooks::DASHBOARD_TYPES]['carbon'] = [Widget::class, 'WidgetTypes']; diff --git a/src/CarbonIntensity.php b/src/CarbonIntensity.php index 4c9fc7ec..8fc7cde6 100644 --- a/src/CarbonIntensity.php +++ b/src/CarbonIntensity.php @@ -251,7 +251,7 @@ public function downloadOneZone(ClientInterface $data_source, Source_Zone $sourc $gap_end = DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $gap['end']); $count = $data_source->fullDownload($source_zone, $gap_start, $gap_end, $this, $limit, $progress_bar); $total_count += $count; - if ($total_count >= $limit) { + if ($limit > 0 && $total_count >= $limit) { return $total_count; } } diff --git a/src/CloudInventoryConnector.php b/src/CloudInventoryConnector.php new file mode 100644 index 00000000..491a8a73 --- /dev/null +++ b/src/CloudInventoryConnector.php @@ -0,0 +1,57 @@ +. + * + * ------------------------------------------------------------------------- + */ + +namespace GlpiPlugin\Carbon; + +use Plugin; + +/** + * Establish the functional bridge between Carbon and the plugin CloudInventory + */ +class CloudInventoryConnector +{ + public static function checkPluginAvailability(): void + { + /** @var array $CFG_GLPI */ + global $CFG_GLPI; + + $CFG_GLPI['plugin:carbon']['use_cloudinventory'] = Plugin::isPluginActive('cloudinventory'); + } + + public function pluginAvailable(): bool + { + /** @var array $CFG_GLPI */ + global $CFG_GLPI; + + return $CFG_GLPI['plugin:carbon']['use_cloudinventory'] ?? false; + } +} diff --git a/src/ComputerType.php b/src/ComputerType.php index c20955d7..ce4ede8f 100644 --- a/src/ComputerType.php +++ b/src/ComputerType.php @@ -50,6 +50,7 @@ class ComputerType extends AbstractChildDropdown public const CATEGORY_LAPTOP = 3; public const CATEGORY_TABLET = 4; public const CATEGORY_SMARTPHONE = 5; + public const CATEGORY_CLOUD = 6; public static function getCategories(): array { @@ -60,6 +61,7 @@ public static function getCategories(): array self::CATEGORY_LAPTOP => __('Laptop', 'carbon'), self::CATEGORY_TABLET => __('Tablet', 'carbon'), self::CATEGORY_SMARTPHONE => __('Smartphone', 'carbon'), + self::CATEGORY_CLOUD => __('Cloud server', 'carbon'), ]; } diff --git a/src/Dashboard/Dashboard.php b/src/Dashboard/Dashboard.php index b13b8d7f..ec0d593d 100644 --- a/src/Dashboard/Dashboard.php +++ b/src/Dashboard/Dashboard.php @@ -61,22 +61,4 @@ public static function getTotalPowerPerModel(): array { return Provider::getSumPowerPerModel([ComputerModel::getTableField('power_consumption') => ['>', '0']]); } - - public static function cardCarbonintensityProvider(array $params = []) - { - $default_params = [ - 'label' => __('Carbon dioxyde intensity', 'carbon'), - 'icon' => "fas fa-computer", - 'color' => '#ea9999', - ]; - $params = array_merge($default_params, $params); - - $data = Provider::getCarbonIntensity($params); - - return [ - 'data' => $data, - 'label' => $params['label'], - 'icon' => $params['icon'], - ]; - } } diff --git a/src/Dashboard/Widget.php b/src/Dashboard/Widget.php index e0674257..1d2cd42d 100644 --- a/src/Dashboard/Widget.php +++ b/src/Dashboard/Widget.php @@ -102,13 +102,6 @@ public static function WidgetTypes(?array $types = null): array 'width' => 16, 'height' => 12, ], - 'usage_abiotic_depletion' => [ - 'label' => __('Usage abiotic depletion potential', 'carbon'), - 'function' => self::class . '::displayUsageAbioticDepletion', - 'image' => '', - 'width' => 6, - 'height' => 3, - ], 'impact_criteria_number' => [ 'label' => __('Impact criteria', 'carbon'), diff --git a/src/DataSource/CarbonIntensity/ElectricityMaps/Client.php b/src/DataSource/CarbonIntensity/ElectricityMaps/Client.php index 1c9a6c08..5c31f611 100644 --- a/src/DataSource/CarbonIntensity/ElectricityMaps/Client.php +++ b/src/DataSource/CarbonIntensity/ElectricityMaps/Client.php @@ -377,7 +377,9 @@ public function fetchRange(DateTimeImmutable $start, DateTimeImmutable $stop, So } $downloaded_year_month = $start->format('Y-m'); - if (count($full_response) > 0 && $downloaded_year_month < date('Y-m')) { + if (count($full_response) > 0 && $downloaded_year_month < date('Y-m') && date('j') > 1) { + // Cache only if the month being processed is older than the month of now + // and we are at least the 2nd day of the current month (to handle +14 hours overlap) $json = json_encode($full_response); file_put_contents($cache_file, $json); } diff --git a/src/DataSource/CarbonIntensity/Rte/Client.php b/src/DataSource/CarbonIntensity/Rte/Client.php index ec7b66c7..b93d87a7 100644 --- a/src/DataSource/CarbonIntensity/Rte/Client.php +++ b/src/DataSource/CarbonIntensity/Rte/Client.php @@ -336,8 +336,9 @@ public function fetchRange(DateTimeImmutable $start, DateTimeImmutable $stop, So } } else { $downloaded_year_month = $start->format('Y-m'); - if ($downloaded_year_month < date('Y-m')) { + if (count($response) > 0 && $downloaded_year_month < date('Y-m') && date('j') > 1) { // Cache only if the month being processed is older than the month of now + // and we are at least the 2nd day of the current month (to handle +14 hours overlap) $json = json_encode($response); file_put_contents($cache_file, $json); } diff --git a/src/DataSource/Lca/Boaviztapi/Client.php b/src/DataSource/Lca/Boaviztapi/Client.php index 5eeb0f12..a0f8275e 100644 --- a/src/DataSource/Lca/Boaviztapi/Client.php +++ b/src/DataSource/Lca/Boaviztapi/Client.php @@ -44,6 +44,7 @@ use GlpiPlugin\Carbon\Zone; use Override; use RuntimeException; +use Session; class Client extends AbstractClient { @@ -257,6 +258,16 @@ public static function getZones() public function parseResponse(array $response, string $scope): array { $impacts = []; + if (!isset($response['impacts'])) { + if (Session::getLoginUserID(true)) { + $message = __('An error occured while processing the response from Boaviztapi', 'carbon'); + $message .= '
' . ($response['detail'] ?? 'unknown error'); + Session::addMessageAfterRedirect($message, true, ERROR); + } + trigger_error('Error parsing the response ' . var_export($response, true), E_USER_WARNING); + return $impacts; + } + $types = Type::getImpactTypes(); foreach ($response['impacts'] as $type => $impact) { if (!in_array($type, $types)) { @@ -273,7 +284,7 @@ public function parseResponse(array $response, string $scope): array return $impacts; } - protected function parseCriteria(string $name, $impact): ?TrackedFloat + protected function parseCriteria(string $name, array|string $impact): ?TrackedFloat { if ($impact === 'not implemented') { return null; @@ -300,4 +311,19 @@ public static function dropdownBoaviztaZone(string $name, array $options = []) { return Dropdown::showFromArray($name, self::getZones(), $options); } + + /** + * Get instances known types of a cloud provider + * + * @param string $provider + * @return array isntances types + */ + public function getCloudInstances(string $provider): array + { + $response = $this->get('cloud/instance/all_instances', [ + 'query' => ['provider' => $provider], + ]); + + return $response; + } } diff --git a/src/Impact/Embodied/Boavizta/AbstractAsset.php b/src/Impact/Embodied/Boavizta/AbstractAsset.php index 8cf253c0..3d104e8e 100644 --- a/src/Impact/Embodied/Boavizta/AbstractAsset.php +++ b/src/Impact/Embodied/Boavizta/AbstractAsset.php @@ -55,6 +55,10 @@ abstract class AbstractAsset extends AbstractEmbodiedImpact implements AssetInte /** @var Client instance of the HTTP client */ protected ?Client $client = null; + protected const USAGE_NULL = [ + 'avg_power' => 0, + ]; + // abstract public static function getEngine(CommonDBTM $item): EngineInterface; /** diff --git a/src/Impact/Embodied/Boavizta/Computer.php b/src/Impact/Embodied/Boavizta/Computer.php index 9d16f684..ca1188ca 100644 --- a/src/Impact/Embodied/Boavizta/Computer.php +++ b/src/Impact/Embodied/Boavizta/Computer.php @@ -35,10 +35,19 @@ use CommonDBTM; use Computer as GlpiComputer; +use ComputerModel as GlpiComputerModel; use ComputerType as GlpiComputerType; +use GlpiPlugin\Carbon\CloudInventoryConnector; use GlpiPlugin\Carbon\ComputerType; use GlpiPlugin\Carbon\DataSource\Lca\Boaviztapi\ComputerModelizationAdapterTrait; +use GlpiPlugin\Cloudinventory\Amazon; +use GlpiPlugin\Cloudinventory\Azure; +use GlpiPlugin\Cloudinventory\CloudInstance; +use GlpiPlugin\Cloudinventory\Google; +use GlpiPlugin\Cloudinventory\Ovh; +use GlpiPlugin\Cloudinventory\Scaleway; use Override; +use UnhandledMatchError; class Computer extends AbstractAsset { @@ -48,34 +57,109 @@ class Computer extends AbstractAsset protected string $endpoint = 'server'; + /** + * If the plugin CloudInventory is available, this is an oblect from that + * plugin representing the cloud related data of the computer + */ + protected ?CloudInstance $cloud_instance = null; + + /** + * @var array Description of the asset for querying Boaviztapi + */ + protected array $description = []; + #[Override] protected function doEvaluation(): ?array { - // adapt $this->endpoint depending on the type of computer (server, laptop, ...) $type = $this->getType($this->item); - $this->endpoint = $this->getEndpoint($type); + + $response = null; + $this->chooseEvaluationMode($type); + + // select all impact types $this->endpoint .= '?' . $this->getCriteriasQueryString(); - // Ask for embodied impact only - $handle_hardware = in_array($type, [ - ComputerType::CATEGORY_SERVER, - ComputerType::CATEGORY_DESKTOP, - ComputerType::CATEGORY_UNDEFINED, - ]); - $configuration = $this->analyzeHardware(); - if ($handle_hardware && count($configuration) === 0) { - return null; + // Query Boaviztapi + $response = $this->query($this->description); + + $impacts = $this->client->parseResponse($response, 'embedded'); + return $impacts; + } + + private function chooseEvaluationMode(int $type): string + { + if ($type === ComputerType::CATEGORY_CLOUD) { + $cloud_provider = ''; + switch ($this->cloud_instance->fields['itemtype']) { + case Amazon::class: + $cloud_provider = 'aws'; + break; + case Azure::class: + $cloud_provider = 'azure'; + break; + case Google::class: + $cloud_provider = 'gcp'; + break; + case Ovh::class: + $cloud_provider = 'ovhcloud'; + break; + case Scaleway::class: + $cloud_provider = 'scaleway'; + break; + } + $glpi_computer_model = GlpiComputerModel::getById($this->cloud_instance->fields['computermodels_id']); + if ($glpi_computer_model !== false) { + $instance_types = $this->client->getCloudInstances($cloud_provider); + $model = $this->normalizeModel($cloud_provider, $glpi_computer_model->fields['name']); + if (in_array($model, $instance_types)) { + $this->prepareCloudDescription($cloud_provider, $model); + return 'cloud'; + } + } } - $description = [ - 'configuration' => $configuration, - 'usage' => [ - 'avg_power' => 0, - ], + + $this->prepareHardwareDescription($type); + return 'hardware'; + } + + /** + * Prepare description of the asset for the Boaviztapi query + */ + private function prepareHardwareDescription(int $type): void + { + try { + $this->endpoint = match ($type) { + ComputerType::CATEGORY_SERVER => 'server', + ComputerType::CATEGORY_LAPTOP => 'terminal/laptop', + ComputerType::CATEGORY_TABLET => 'terminal/tablet', + ComputerType::CATEGORY_SMARTPHONE => 'terminal/smartphone', + }; + } catch (UnhandledMatchError $e) { + $this->endpoint = 'terminal/desktop'; + } + + $this->description = [ + 'configuration' => $this->analyzeHardware(), + 'usage' => self::USAGE_NULL, ]; - $response = $this->query($description); - $impacts = $this->client->parseResponse($response, 'embedded'); + } - return $impacts; + /** + * Prepare description of the asset for the Boaviztapi query + * + * @param string $provider + * @param string $model + * @return void + */ + private function prepareCloudDescription(string $provider, string $model) + { + $this->endpoint = 'cloud/instance'; + + $this->description = [ + 'usage' => self::USAGE_NULL, + ]; + $this->description['provider'] = $provider; + $this->description['instance_type'] = $model; } /** @@ -85,6 +169,18 @@ protected function doEvaluation(): ?array */ protected function getType(CommonDBTM $item): int { + $cloudInventory_connector = new CloudInventoryConnector(); + if ($cloudInventory_connector->pluginAvailable()) { + $cloud_instance = new CloudInstance(); + $cloud_instance->getFromDBByCrit([ + 'computers_id' => $item->getID(), + ]); + if (!$cloud_instance->isNewItem()) { + $this->cloud_instance = $cloud_instance; + return ComputerType::CATEGORY_CLOUD; + } + } + $computer_table = GlpiComputer::getTable(); $computer_type_table = ComputerType::getTable(); $glpi_computer_type_table = GlpiComputerType::getTable(); @@ -115,24 +211,14 @@ protected function getType(CommonDBTM $item): int return $computer_type->fields['category']; } - /** - * Get the endpoint to use for the given type - */ - protected function getEndpoint(int $type) + protected function normalizeModel(string $provider, string $model): string { - switch ($type) { - case ComputerType::CATEGORY_SERVER: - return 'server'; - case ComputerType::CATEGORY_LAPTOP: - return 'terminal/laptop'; - case ComputerType::CATEGORY_TABLET: - return 'terminal/tablet'; - case ComputerType::CATEGORY_SMARTPHONE: - return 'terminal/smartphone'; + switch ($provider) { + case 'scaleway': + // CloudInventory sets scaleway models with the prefix "SCW-" + return strtolower(substr($model, 4)); } - // ComputerType::CATEGORY_UNDEFINED - // ComputerType::CATEGORY_DESKTOP - return 'terminal/desktop'; + return $provider; } } diff --git a/src/Impact/Embodied/Boavizta/Monitor.php b/src/Impact/Embodied/Boavizta/Monitor.php index 1e8acdb0..91298400 100644 --- a/src/Impact/Embodied/Boavizta/Monitor.php +++ b/src/Impact/Embodied/Boavizta/Monitor.php @@ -52,9 +52,7 @@ protected function doEvaluation(): ?array $description = [ 'configuration' => $configuration, - 'usage' => [ - 'avg_power' => 0, - ], + 'usage' => self::USAGE_NULL, ]; $response = $this->query($description); $impacts = $this->client->parseResponse($response, 'embedded'); diff --git a/src/SearchOptions.php b/src/SearchOptions.php index 06df8939..77a66819 100644 --- a/src/SearchOptions.php +++ b/src/SearchOptions.php @@ -130,6 +130,11 @@ public static function getCoreSearchOptions(string $itemtype): array { $sopt = []; + $sopt[] = [ + 'id' => 'carbon', + 'name' => __('Environmental impact', 'carbon'), + ]; + if ($itemtype === GlpiLocation::class) { $sopt[] = [ 'id' => SearchOptions::LOCATION_BOAVIZTA_ZONE, diff --git a/stubs/OptionalClass.php.stub b/stubs/OptionalClass.php.stub new file mode 100644 index 00000000..6a363cf0 --- /dev/null +++ b/stubs/OptionalClass.php.stub @@ -0,0 +1,25 @@ + */ + public $fields = []; + + /** + * @param array $criteria + * @return bool + * + * @phpstan-impure + */ + public function getFromDBByCrit(array $criteria): bool {} + + public function isNewItem(): bool {} +} + +class Amazon {} +class Azure {} +class Google {} +class Ovh {} +class Scaleway {} diff --git a/templates/environmentalimpact-item.html.twig b/templates/environmentalimpact-item.html.twig index a499c1ec..574e49ff 100644 --- a/templates/environmentalimpact-item.html.twig +++ b/templates/environmentalimpact-item.html.twig @@ -73,6 +73,12 @@ 'fas fa-chart-pie' ) }} +{% if ((embodied_impact.fields['engine'] is defined) and (embodied_impact.fields['engine_version'] is defined)) %} + {{ fields.smallTitle( + embodied_impact.fields['engine'] ~ ' ' ~ embodied_impact.fields['engine_version'], + ) }} +{% endif %} +
diff --git a/tests/src/CommonTestCase.php b/tests/src/CommonTestCase.php index 35abfb7a..97d2e182 100644 --- a/tests/src/CommonTestCase.php +++ b/tests/src/CommonTestCase.php @@ -64,7 +64,7 @@ class CommonTestCase extends TestCase /** @var int $debugMode save state of GLPI debug mode */ private $debugMode = null; - protected $str = null; + protected ?string $str = null; protected function disableDebug() { @@ -115,7 +115,7 @@ protected function setupGLPIFramework(): void return; } - protected function login($name, $password, $noauto = false) + protected function login(string $name, string $password, $noauto = false) { Session::start(); $auth = new Auth(); @@ -539,7 +539,7 @@ protected function isolateInEntity(): int * * @return mixed */ - protected function callPrivateMethod($instance, string $methodName, ...$args) + protected function callPrivateMethod($instance, string $methodName, mixed ...$args) { $method = new ReflectionMethod($instance, $methodName); if (version_compare(PHP_VERSION, '8.1.0') < 0) { diff --git a/tests/units/DataSource/CarbonIntensity/ElectricityMaps/ConfigTest.php b/tests/units/DataSource/CarbonIntensity/ElectricityMaps/ConfigTest.php index da22ef07..034a2347 100644 --- a/tests/units/DataSource/CarbonIntensity/ElectricityMaps/ConfigTest.php +++ b/tests/units/DataSource/CarbonIntensity/ElectricityMaps/ConfigTest.php @@ -99,10 +99,6 @@ public function testConfigUpdate(array $input, array $expected) /** @var array $CFG_GLPI */ global $CFG_GLPI; - $CFG_GLPI['plugi:carbon']['lca_datasources'] = [ - Client::class, - ]; - $instance = new Config(); $result = $instance->configUpdate($input); $this->assertEquals($expected, $result); diff --git a/tests/units/DataSource/Lca/Boaviztapi/ConfigTest.php b/tests/units/DataSource/Lca/Boaviztapi/ConfigTest.php index 7f5249e1..ef3a40b7 100644 --- a/tests/units/DataSource/Lca/Boaviztapi/ConfigTest.php +++ b/tests/units/DataSource/Lca/Boaviztapi/ConfigTest.php @@ -124,10 +124,6 @@ public function testConfigUpdate(array $input, array $expected) /** @var array $CFG_GLPI */ global $CFG_GLPI; - $CFG_GLPI['plugi:carbon']['lca_datasources'] = [ - Client::class, - ]; - $result = (new Config())->configUpdate($input); $this->assertEquals($expected, $result); }