From 76536f81154499415b3063614f5aef642d7dccfd Mon Sep 17 00:00:00 2001 From: NaiJi Date: Fri, 3 Mar 2023 03:01:09 +0400 Subject: [PATCH] chore: Move basic functionality of Digital Ocean to provider layer --- .../digital_ocean/digital_ocean_api.dart | 264 ++++-------------- .../server_providers/hetzner/hetzner_api.dart | 10 +- .../server_providers/digital_ocean.dart | 247 +++++++++++++++- .../providers/server_providers/hetzner.dart | 10 - 4 files changed, 308 insertions(+), 223 deletions(-) diff --git a/lib/logic/api_maps/rest_maps/server_providers/digital_ocean/digital_ocean_api.dart b/lib/logic/api_maps/rest_maps/server_providers/digital_ocean/digital_ocean_api.dart index e1996dc4..3f886597 100644 --- a/lib/logic/api_maps/rest_maps/server_providers/digital_ocean/digital_ocean_api.dart +++ b/lib/logic/api_maps/rest_maps/server_providers/digital_ocean/digital_ocean_api.dart @@ -2,7 +2,6 @@ import 'dart:convert'; import 'dart:io'; import 'package:dio/dio.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/volume_provider.dart'; import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/server_provider.dart'; @@ -11,13 +10,9 @@ import 'package:selfprivacy/logic/models/disk_size.dart'; import 'package:selfprivacy/logic/models/hive/server_domain.dart'; import 'package:selfprivacy/logic/models/hive/server_details.dart'; import 'package:selfprivacy/logic/models/hive/user.dart'; -import 'package:selfprivacy/logic/models/metrics.dart'; import 'package:selfprivacy/logic/models/price.dart'; import 'package:selfprivacy/logic/models/server_basic_info.dart'; -import 'package:selfprivacy/logic/models/server_metadata.dart'; import 'package:selfprivacy/logic/models/server_provider_location.dart'; -import 'package:selfprivacy/logic/models/server_type.dart'; -import 'package:selfprivacy/utils/extensions/string_extensions.dart'; import 'package:selfprivacy/utils/network_utils.dart'; import 'package:selfprivacy/utils/password_generator.dart'; @@ -107,13 +102,11 @@ class DigitalOceanApi extends ServerProviderApi with VolumeProviderApi { /// Hardcoded on their documentation and there is no pricing API at all /// Probably we should scrap the doc page manually - @override Future getPricePerGb() async => Price( value: 0.10, currency: 'USD', ); - @override Future> createVolume() async { ServerVolume? volume; @@ -163,7 +156,6 @@ class DigitalOceanApi extends ServerProviderApi with VolumeProviderApi { ); } - @override Future> getVolumes({final String? status}) async { final List volumes = []; @@ -216,7 +208,6 @@ class DigitalOceanApi extends ServerProviderApi with VolumeProviderApi { return requestedVolume; } - @override Future deleteVolume(final ServerVolume volume) async { final Dio client = await getClient(); try { @@ -228,7 +219,6 @@ class DigitalOceanApi extends ServerProviderApi with VolumeProviderApi { } } - @override Future> attachVolume( final ServerVolume volume, final int serverId, @@ -268,7 +258,6 @@ class DigitalOceanApi extends ServerProviderApi with VolumeProviderApi { ); } - @override Future detachVolume(final ServerVolume volume) async { bool success = false; @@ -295,7 +284,6 @@ class DigitalOceanApi extends ServerProviderApi with VolumeProviderApi { return success; } - @override Future resizeVolume( final ServerVolume volume, final DiskSize size, @@ -325,7 +313,6 @@ class DigitalOceanApi extends ServerProviderApi with VolumeProviderApi { return success; } - @override Future> createServer({ required final String dnsApiToken, required final User rootUser, @@ -472,132 +459,65 @@ class DigitalOceanApi extends ServerProviderApi with VolumeProviderApi { close(client); } - return GenericResult( - success: true, - data: true, - ); + return GenericResult(success: true, data: true); } - @override - Future restart() async { - final ServerHostingDetails server = getIt().serverDetails!; - + Future> restart(final int serverId) async { final Dio client = await getClient(); try { await client.post( - '/droplets/${server.id}/actions', + '/droplets/$serverId/actions', data: { 'type': 'reboot', }, ); } catch (e) { print(e); + return GenericResult( + success: false, + data: null, + message: e.toString(), + ); } finally { close(client); } - return server.copyWith(startTime: DateTime.now()); + return GenericResult(success: true, data: null); } - @override - Future powerOn() async { - final ServerHostingDetails server = getIt().serverDetails!; - + Future> powerOn(final int serverId) async { final Dio client = await getClient(); try { await client.post( - '/droplets/${server.id}/actions', + '/droplets/$serverId/actions', data: { 'type': 'power_on', }, ); } catch (e) { print(e); + return GenericResult( + success: false, + data: null, + message: e.toString(), + ); } finally { close(client); } - return server.copyWith(startTime: DateTime.now()); + return GenericResult(success: true, data: null); } - /// Digital Ocean returns a map of lists of /proc/stat values, - /// so here we are trying to implement average CPU - /// load calculation for each point in time on a given interval. - /// - /// For each point of time: - /// - /// `Average Load = 100 * (1 - (Idle Load / Total Load))` - /// - /// For more info please proceed to read: - /// https://rosettacode.org/wiki/Linux_CPU_utilization - List calculateCpuLoadMetrics(final List rawProcStatMetrics) { - final List cpuLoads = []; - - final int pointsInTime = (rawProcStatMetrics[0]['values'] as List).length; - for (int i = 0; i < pointsInTime; ++i) { - double currentMetricLoad = 0.0; - double? currentMetricIdle; - for (final rawProcStat in rawProcStatMetrics) { - final String rawProcValue = rawProcStat['values'][i][1]; - // Converting MBit into bit - final double procValue = double.parse(rawProcValue) * 1000000; - currentMetricLoad += procValue; - if (currentMetricIdle == null && - rawProcStat['metric']['mode'] == 'idle') { - currentMetricIdle = procValue; - } - } - currentMetricIdle ??= 0.0; - currentMetricLoad = 100.0 * (1 - (currentMetricIdle / currentMetricLoad)); - cpuLoads.add( - TimeSeriesData( - rawProcStatMetrics[0]['values'][i][0], - currentMetricLoad, - ), - ); - } - - return cpuLoads; - } - - @override - Future getMetrics( + Future> getMetricsCpu( final int serverId, final DateTime start, final DateTime end, ) async { - ServerMetrics? metrics; + List metrics = []; - const int step = 15; final Dio client = await getClient(); try { - Response response = await client.get( - '/monitoring/metrics/droplet/bandwidth', - queryParameters: { - 'start': '${(start.microsecondsSinceEpoch / 1000000).round()}', - 'end': '${(end.microsecondsSinceEpoch / 1000000).round()}', - 'host_id': '$serverId', - 'interface': 'public', - 'direction': 'inbound', - }, - ); - - final List inbound = response.data['data']['result'][0]['values']; - - response = await client.get( - '/monitoring/metrics/droplet/bandwidth', - queryParameters: { - 'start': '${(start.microsecondsSinceEpoch / 1000000).round()}', - 'end': '${(end.microsecondsSinceEpoch / 1000000).round()}', - 'host_id': '$serverId', - 'interface': 'public', - 'direction': 'outbound', - }, - ); - - final List outbound = response.data['data']['result'][0]['values']; - - response = await client.get( + final Response response = await client.get( '/monitoring/metrics/droplet/cpu', queryParameters: { 'start': '${(start.microsecondsSinceEpoch / 1000000).round()}', @@ -605,122 +525,75 @@ class DigitalOceanApi extends ServerProviderApi with VolumeProviderApi { 'host_id': '$serverId', }, ); - - metrics = ServerMetrics( - bandwidthIn: inbound - .map( - (final el) => TimeSeriesData(el[0], double.parse(el[1]) * 100000), - ) - .toList(), - bandwidthOut: outbound - .map( - (final el) => TimeSeriesData(el[0], double.parse(el[1]) * 100000), - ) - .toList(), - cpu: calculateCpuLoadMetrics(response.data['data']['result']), - start: start, - end: end, - stepsInSecond: step, - ); + metrics = response.data['data']['result']; } catch (e) { print(e); + return GenericResult( + success: false, + data: [], + message: e.toString(), + ); } finally { close(client); } - return metrics; + return GenericResult(success: true, data: metrics); } - @override - Future> getMetadata(final int serverId) async { - List metadata = []; + Future> getMetricsBandwidth( + final int serverId, + final DateTime start, + final DateTime end, + final bool isInbound, + ) async { + List metrics = []; final Dio client = await getClient(); try { - final Response response = await client.get('/droplets/$serverId'); - final droplet = response.data!['droplet']; - metadata = [ - ServerMetadataEntity( - type: MetadataType.id, - name: 'server.server_id'.tr(), - value: droplet['id'].toString(), - ), - ServerMetadataEntity( - type: MetadataType.status, - name: 'server.status'.tr(), - value: droplet['status'].toString().capitalize(), - ), - ServerMetadataEntity( - type: MetadataType.cpu, - name: 'server.cpu'.tr(), - value: 'server.core_count'.plural(droplet['vcpus']), - ), - ServerMetadataEntity( - type: MetadataType.ram, - name: 'server.ram'.tr(), - value: "${droplet['memory'].toString()} MB", - ), - ServerMetadataEntity( - type: MetadataType.cost, - name: 'server.monthly_cost'.tr(), - value: droplet['size']['price_monthly'].toString(), - ), - ServerMetadataEntity( - type: MetadataType.location, - name: 'server.location'.tr(), - value: - '${droplet['region']['name']} ${getEmojiFlag(droplet['region']['slug'].toString()) ?? ''}', - ), - ServerMetadataEntity( - type: MetadataType.other, - name: 'server.provider'.tr(), - value: displayProviderName, - ), - ]; + final Response response = await client.get( + '/monitoring/metrics/droplet/bandwidth', + queryParameters: { + 'start': '${(start.microsecondsSinceEpoch / 1000000).round()}', + 'end': '${(end.microsecondsSinceEpoch / 1000000).round()}', + 'host_id': '$serverId', + 'interface': 'public', + 'direction': isInbound ? 'inbound' : 'outbound', + }, + ); + metrics = response.data['data']['result'][0]['values']; } catch (e) { print(e); + return GenericResult( + success: false, + data: [], + message: e.toString(), + ); } finally { close(client); } - return metadata; + return GenericResult(success: true, data: metrics); } - @override - Future> getServers() async { - List servers = []; + Future> getServers() async { + List servers = []; final Dio client = await getClient(); try { final Response response = await client.get('/droplets'); - servers = response.data!['droplets'].map( - (final server) { - String ipv4 = '0.0.0.0'; - if (server['networks']['v4'].isNotEmpty) { - for (final v4 in server['networks']['v4']) { - if (v4['type'].toString() == 'public') { - ipv4 = v4['ip_address'].toString(); - } - } - } - - return ServerBasicInfo( - id: server['id'], - reverseDns: server['name'], - created: DateTime.now(), - ip: ipv4, - name: server['name'], - ); - }, - ).toList(); + servers = response.data; } catch (e) { print(e); + return GenericResult( + success: false, + data: servers, + message: e.toString(), + ); } finally { close(client); } - print(servers); - return servers; + return GenericResult(success: true, data: servers); } Future> getAvailableLocations() async { @@ -769,21 +642,4 @@ class DigitalOceanApi extends ServerProviderApi with VolumeProviderApi { return GenericResult(data: types, success: true); } - - @override - Future> createReverseDns({ - required final ServerHostingDetails serverDetails, - required final ServerDomain domain, - }) async { - /// TODO remove from provider interface - const bool success = true; - return GenericResult(success: success, data: null); - } - - @override - ProviderApiTokenValidation getApiTokenValidation() => - ProviderApiTokenValidation( - regexp: RegExp(r'\s+|[-!$%^&*()@+|~=`{}\[\]:<>?,.\/]'), - length: 71, - ); } diff --git a/lib/logic/api_maps/rest_maps/server_providers/hetzner/hetzner_api.dart b/lib/logic/api_maps/rest_maps/server_providers/hetzner/hetzner_api.dart index 42ec94bb..d1051b76 100644 --- a/lib/logic/api_maps/rest_maps/server_providers/hetzner/hetzner_api.dart +++ b/lib/logic/api_maps/rest_maps/server_providers/hetzner/hetzner_api.dart @@ -455,10 +455,7 @@ class HetznerApi extends ServerProviderApi with VolumeProviderApi { close(client); } - return GenericResult( - success: true, - data: null, - ); + return GenericResult(success: true, data: null); } Future> powerOn(final int serverId) async { @@ -476,10 +473,7 @@ class HetznerApi extends ServerProviderApi with VolumeProviderApi { close(client); } - return GenericResult( - success: true, - data: null, - ); + return GenericResult(success: true, data: null); } Future>> getMetrics( diff --git a/lib/logic/providers/server_providers/digital_ocean.dart b/lib/logic/providers/server_providers/digital_ocean.dart index d94418cd..9538c6a4 100644 --- a/lib/logic/providers/server_providers/digital_ocean.dart +++ b/lib/logic/providers/server_providers/digital_ocean.dart @@ -1,7 +1,13 @@ import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/digital_ocean/digital_ocean_api.dart'; +import 'package:selfprivacy/logic/models/disk_size.dart'; +import 'package:selfprivacy/logic/models/metrics.dart'; +import 'package:selfprivacy/logic/models/price.dart'; +import 'package:selfprivacy/logic/models/server_basic_info.dart'; +import 'package:selfprivacy/logic/models/server_metadata.dart'; import 'package:selfprivacy/logic/models/server_provider_location.dart'; import 'package:selfprivacy/logic/models/server_type.dart'; import 'package:selfprivacy/logic/providers/server_provider.dart'; +import 'package:selfprivacy/utils/extensions/string_extensions.dart'; class ApiAdapter { ApiAdapter({final String? region, final bool isWithToken = true}) @@ -149,7 +155,7 @@ class DigitalOceanServerProvider extends ServerProvider { ); } - final List rawTypes = result.data; + final List rawSizes = result.data; for (final rawSize in rawSizes) { for (final rawRegion in rawSize['regions']) { final ramMb = rawSize['memory'].toDouble(); @@ -174,4 +180,243 @@ class DigitalOceanServerProvider extends ServerProvider { return GenericResult(success: true, data: types); } + + Future>> getServers() async { + final List servers = []; + final result = await _adapter.api().getServers(); + if (result.data.isEmpty || !result.success) { + return GenericResult( + success: result.success, + data: servers, + code: result.code, + message: result.message, + ); + } + + final List rawServers = result.data; + rawServers.map( + (final server) { + String ipv4 = '0.0.0.0'; + if (server['networks']['v4'].isNotEmpty) { + for (final v4 in server['networks']['v4']) { + if (v4['type'].toString() == 'public') { + ipv4 = v4['ip_address'].toString(); + } + } + } + + return ServerBasicInfo( + id: server['id'], + reverseDns: server['name'], + created: DateTime.now(), + ip: ipv4, + name: server['name'], + ); + }, + ).toList(); + + return GenericResult(success: true, data: servers); + } + + Future>> getMetadata( + final int serverId, + ) async { + List metadata = []; + final result = await _adapter.api().getServers(); + if (result.data.isEmpty || !result.success) { + return GenericResult( + success: false, + data: metadata, + code: result.code, + message: result.message, + ); + } + + final List servers = result.data; + try { + final droplet = servers.firstWhere( + (final server) => server['id'] == serverId, + ); + + metadata = [ + ServerMetadataEntity( + type: MetadataType.id, + trId: 'server.server_id', + value: droplet['id'].toString(), + ), + ServerMetadataEntity( + type: MetadataType.status, + trId: 'server.status', + value: droplet['status'].toString().capitalize(), + ), + ServerMetadataEntity( + type: MetadataType.cpu, + trId: 'server.cpu', + value: droplet['vcpus'].toString(), + ), + ServerMetadataEntity( + type: MetadataType.ram, + trId: 'server.ram', + value: "${droplet['memory'].toString()} MB", + ), + ServerMetadataEntity( + type: MetadataType.cost, + trId: 'server.monthly_cost', + value: droplet['size']['price_monthly'].toString(), + ), + ServerMetadataEntity( + type: MetadataType.location, + trId: 'server.location', + value: + '${droplet['region']['name']} ${getEmojiFlag(droplet['region']['slug'].toString()) ?? ''}', + ), + ServerMetadataEntity( + type: MetadataType.other, + trId: 'server.provider', + value: _adapter.api().displayProviderName, + ), + ]; + } catch (e) { + return GenericResult( + success: false, + data: [], + message: e.toString(), + ); + } + + return GenericResult(success: true, data: metadata); + } + + /// Digital Ocean returns a map of lists of /proc/stat values, + /// so here we are trying to implement average CPU + /// load calculation for each point in time on a given interval. + /// + /// For each point of time: + /// + /// `Average Load = 100 * (1 - (Idle Load / Total Load))` + /// + /// For more info please proceed to read: + /// https://rosettacode.org/wiki/Linux_CPU_utilization + List calculateCpuLoadMetrics(final List rawProcStatMetrics) { + final List cpuLoads = []; + + final int pointsInTime = (rawProcStatMetrics[0]['values'] as List).length; + for (int i = 0; i < pointsInTime; ++i) { + double currentMetricLoad = 0.0; + double? currentMetricIdle; + for (final rawProcStat in rawProcStatMetrics) { + final String rawProcValue = rawProcStat['values'][i][1]; + // Converting MBit into bit + final double procValue = double.parse(rawProcValue) * 1000000; + currentMetricLoad += procValue; + if (currentMetricIdle == null && + rawProcStat['metric']['mode'] == 'idle') { + currentMetricIdle = procValue; + } + } + currentMetricIdle ??= 0.0; + currentMetricLoad = 100.0 * (1 - (currentMetricIdle / currentMetricLoad)); + cpuLoads.add( + TimeSeriesData( + rawProcStatMetrics[0]['values'][i][0], + currentMetricLoad, + ), + ); + } + + return cpuLoads; + } + + @override + Future> getMetrics( + final int serverId, + final DateTime start, + final DateTime end, + ) async { + ServerMetrics? metrics; + + const int step = 15; + final inboundResult = await _adapter.api().getMetricsBandwidth( + serverId, + start, + end, + true, + ); + + if (inboundResult.data.isEmpty || !inboundResult.success) { + return GenericResult( + success: false, + data: null, + code: inboundResult.code, + message: inboundResult.message, + ); + } + + final outboundResult = await _adapter.api().getMetricsBandwidth( + serverId, + start, + end, + false, + ); + + if (outboundResult.data.isEmpty || !outboundResult.success) { + return GenericResult( + success: false, + data: null, + code: outboundResult.code, + message: outboundResult.message, + ); + } + + final cpuResult = await _adapter.api().getMetricsCpu(serverId, start, end); + + if (cpuResult.data.isEmpty || !cpuResult.success) { + return GenericResult( + success: false, + data: null, + code: cpuResult.code, + message: cpuResult.message, + ); + } + + metrics = ServerMetrics( + bandwidthIn: inboundResult.data + .map( + (final el) => TimeSeriesData(el[0], double.parse(el[1]) * 100000), + ) + .toList(), + bandwidthOut: outboundResult.data + .map( + (final el) => TimeSeriesData(el[0], double.parse(el[1]) * 100000), + ) + .toList(), + cpu: calculateCpuLoadMetrics(cpuResult.data), + start: start, + end: end, + stepsInSecond: step, + ); + + return GenericResult(success: true, data: metrics); + } + + @override + Future> restart(final int serverId) async { + DateTime? timestamp; + final result = await _adapter.api().restart(serverId); + if (!result.success) { + return GenericResult( + success: false, + data: timestamp, + code: result.code, + message: result.message, + ); + } + + timestamp = DateTime.now(); + + return GenericResult( + success: true, + data: timestamp, + ); + } } diff --git a/lib/logic/providers/server_providers/hetzner.dart b/lib/logic/providers/server_providers/hetzner.dart index 43d60aec..41669a76 100644 --- a/lib/logic/providers/server_providers/hetzner.dart +++ b/lib/logic/providers/server_providers/hetzner.dart @@ -173,16 +173,6 @@ class HetznerServerProvider extends ServerProvider { return GenericResult(success: true, data: types); } - Future> createReverseDns({ - required final ServerHostingDetails serverDetails, - required final ServerDomain domain, - }) async => - _adapter.api().createReverseDns( - serverId: serverDetails.id, - ip4: serverDetails.ip4, - dnsPtr: domain.domainName, - ); - Future>> getServers() async { final List servers = []; final result = await _adapter.api().getServers();