chore: Implement basics of hetzner installation logic

pull/213/head
NaiJi ✨ 2023-02-21 13:11:04 +04:00
parent e739f7ab9d
commit 8da7341ccb
7 changed files with 287 additions and 32 deletions

View File

@ -429,6 +429,7 @@
"modals": {
"dns_removal_error": "Couldn't remove DNS records.",
"server_deletion_error": "Couldn't delete active server.",
"volume_creation_error": "Couldn't create volume.",
"server_validators_error": "Couldn't fetch available servers.",
"already_exists": "Such server already exists.",
"unexpected_error": "Unexpected error during placement from the provider side.",
@ -479,4 +480,4 @@
"length_not_equal": "Length is [], should be {}",
"length_longer": "Length is [], should be shorter than or equal to {}"
}
}
}

View File

@ -340,34 +340,72 @@ class HetznerApi extends ServerProviderApi with VolumeProviderApi {
return success;
}
Future<GenericResult<ServerHostingDetails?>> createServer({
Future<GenericResult> createServer({
required final String dnsApiToken,
required final String dnsProviderType,
required final String serverApiToken,
required final User rootUser,
required final String base64Password,
required final String databasePassword,
required final String domainName,
required final String hostName,
required final int volumeId,
required final String serverType,
required final DnsProviderType dnsProvider,
}) async {
final GenericResult<ServerVolume?> newVolumeResponse = await createVolume();
final String stagingAcme = StagingOptions.stagingAcme ? 'true' : 'false';
if (!newVolumeResponse.success || newVolumeResponse.data == null) {
return GenericResult(
data: null,
success: false,
message: newVolumeResponse.message,
code: newVolumeResponse.code,
);
Response? serverCreateResponse;
DioError? hetznerError;
bool success = false;
final Dio client = await getClient();
try {
final Map<String, Object> data = {
'name': hostName,
'server_type': serverType,
'start_after_create': false,
'image': 'ubuntu-20.04',
'volumes': [volumeId],
'networks': [],
'user_data': '#cloud-config\n'
'runcmd:\n'
'- curl https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-infect/raw/branch/providers/hetzner/nixos-infect | '
"STAGING_ACME='$stagingAcme' PROVIDER=$infectProviderName DNS_PROVIDER_TYPE=$dnsProviderType "
"NIX_CHANNEL=nixos-21.05 DOMAIN='$domainName' LUSER='${rootUser.login}' ENCODED_PASSWORD='$base64Password' "
'CF_TOKEN=$dnsApiToken DB_PASSWORD=$databasePassword API_TOKEN=$serverApiToken HOSTNAME=$hostName bash 2>&1 | '
'tee /tmp/infect.log',
'labels': {},
'automount': true,
'location': region!,
};
print('Decoded data: $data');
serverCreateResponse = await client.post('/servers', data: data);
success = true;
} on DioError catch (e) {
print(e);
hetznerError = e;
} catch (e) {
print(e);
} finally {
close(client);
}
return createServerWithVolume(
dnsApiToken: dnsApiToken,
rootUser: rootUser,
domainName: domainName,
volume: newVolumeResponse.data!,
serverType: serverType,
dnsProvider: dnsProvider,
String? apiResultMessage = serverCreateResponse?.statusMessage;
if (hetznerError != null &&
hetznerError.response!.data['error']['code'] == 'uniqueness_error') {
apiResultMessage = 'uniqueness_error';
}
return GenericResult(
data: serverCreateResponse?.data,
success: success && hetznerError == null,
code: serverCreateResponse?.statusCode ??
hetznerError?.response?.statusCode,
message: apiResultMessage,
);
}
Future<GenericResult<ServerHostingDetails?>> createServerWithVolume({
Future<GenericResult<ServerHostingDetails?>> skldfjalkdsjflkasd({
required final String dnsApiToken,
required final User rootUser,
required final String domainName,
@ -375,8 +413,6 @@ class HetznerApi extends ServerProviderApi with VolumeProviderApi {
required final String serverType,
required final DnsProviderType dnsProvider,
}) async {
final Dio client = await getClient();
final String dbPassword = StringGenerators.dbPassword();
final int volumeId = volume.id;
@ -388,14 +424,11 @@ class HetznerApi extends ServerProviderApi with VolumeProviderApi {
base64.encode(utf8.encode(rootUser.password ?? 'PASS'));
final String dnsProviderType = dnsProviderToInfectName(dnsProvider);
final String userdataString =
"#cloud-config\nruncmd:\n- curl https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-infect/raw/branch/$infectBranch/nixos-infect | STAGING_ACME='$stagingAcme' PROVIDER=$infectProviderName DNS_PROVIDER_TYPE=$dnsProviderType NIX_CHANNEL=nixos-21.05 DOMAIN='$domainName' LUSER='${rootUser.login}' ENCODED_PASSWORD='$base64Password' CF_TOKEN=$dnsApiToken DB_PASSWORD=$dbPassword API_TOKEN=$apiToken HOSTNAME=$hostname bash 2>&1 | tee /tmp/infect.log";
Response? serverCreateResponse;
ServerHostingDetails? serverDetails;
DioError? hetznerError;
bool success = false;
final Dio client = await getClient();
try {
final Map<String, Object> data = {
'name': hostname,
@ -404,7 +437,13 @@ class HetznerApi extends ServerProviderApi with VolumeProviderApi {
'image': 'ubuntu-20.04',
'volumes': [volumeId],
'networks': [],
'user_data': userdataString,
'user_data': '#cloud-config\n'
'runcmd:\n'
'- curl https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-infect/raw/branch/$infectBranch/nixos-infect | '
"STAGING_ACME='$stagingAcme' PROVIDER=$infectProviderName DNS_PROVIDER_TYPE=$dnsProviderType "
"NIX_CHANNEL=nixos-21.05 DOMAIN='$domainName' LUSER='${rootUser.login}' ENCODED_PASSWORD='$base64Password' "
'CF_TOKEN=$dnsApiToken DB_PASSWORD=$dbPassword API_TOKEN=$apiToken HOSTNAME=$hostname bash 2>&1 | '
'tee /tmp/infect.log',
'labels': {},
'automount': true,
'location': region!,

View File

@ -9,8 +9,6 @@ import 'package:selfprivacy/logic/api_maps/rest_maps/api_controller.dart';
import 'package:selfprivacy/logic/providers/provider_settings.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider_api_settings.dart';
import 'package:selfprivacy/logic/providers/providers_controller.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/server_provider.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/server_provider_api_settings.dart';
import 'package:selfprivacy/logic/models/hive/backblaze_credential.dart';
import 'package:selfprivacy/logic/models/hive/server_details.dart';
import 'package:selfprivacy/logic/models/hive/server_domain.dart';
@ -174,7 +172,7 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
await repository.saveServerType(serverType);
await ProvidersController.currentServerProvider!
.trySetServerType(serverType);
.trySetServerLocation(serverType);
emit(
(state as ServerInstallationNotFinished).copyWith(

View File

@ -0,0 +1,21 @@
import 'package:selfprivacy/logic/api_maps/generic_result.dart';
class CallbackDialogueBranching {
CallbackDialogueBranching({
required this.title,
required this.description,
required this.choices,
});
final String title;
final String description;
final List<CallbackDialogueChoice> choices;
}
class CallbackDialogueChoice {
CallbackDialogueChoice({
required this.title,
required this.callback,
});
final String title;
final Future<GenericResult<CallbackDialogueBranching?>> Function()? callback;
}

View File

@ -0,0 +1,19 @@
import 'package:selfprivacy/logic/models/hive/server_domain.dart';
import 'package:selfprivacy/logic/models/hive/user.dart';
import 'package:selfprivacy/logic/models/server_type.dart';
class LaunchInstallationData {
LaunchInstallationData({
required this.rootUser,
required this.dnsApiToken,
required this.dnsProviderType,
required this.domainName,
required this.serverType,
});
final User rootUser;
final String dnsApiToken;
final String domainName;
final DnsProviderType dnsProviderType;
final ServerType serverType;
}

View File

@ -5,7 +5,7 @@ import 'package:selfprivacy/logic/models/server_type.dart';
export 'package:selfprivacy/logic/api_maps/generic_result.dart';
abstract class ServerProvider {
Future<GenericResult<bool>> trySetServerType(final ServerType type);
Future<GenericResult<bool>> trySetServerLocation(final String location);
Future<GenericResult<bool>> tryInitApiByToken(final String token);
Future<GenericResult<List<ServerProviderLocation>>> getAvailableLocations();
Future<GenericResult<List<ServerType>>> getServerTypes({

View File

@ -1,8 +1,14 @@
import 'dart:convert';
import 'package:easy_localization/easy_localization.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/hetzner/hetzner_api.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/logic/models/callback_dialogue_branching.dart';
import 'package:selfprivacy/logic/models/disk_size.dart';
import 'package:selfprivacy/logic/models/hive/server_details.dart';
import 'package:selfprivacy/logic/models/hive/server_domain.dart';
import 'package:selfprivacy/logic/models/json/hetzner_server_info.dart';
import 'package:selfprivacy/logic/models/launch_installation_data.dart';
import 'package:selfprivacy/logic/models/metrics.dart';
import 'package:selfprivacy/logic/models/price.dart';
import 'package:selfprivacy/logic/models/server_basic_info.dart';
@ -11,6 +17,8 @@ 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';
import 'package:selfprivacy/utils/network_utils.dart';
import 'package:selfprivacy/utils/password_generator.dart';
class ApiAdapter {
ApiAdapter({final String? region, final bool isWithToken = true})
@ -42,7 +50,9 @@ class HetznerServerProvider extends ServerProvider {
ApiAdapter _adapter;
@override
Future<GenericResult<bool>> trySetServerType(final ServerType type) async {
Future<GenericResult<bool>> trySetServerLocation(
final String location,
) async {
final bool apiInitialized = _adapter.api().isWithToken;
if (!apiInitialized) {
return GenericResult(
@ -54,7 +64,7 @@ class HetznerServerProvider extends ServerProvider {
_adapter = ApiAdapter(
isWithToken: true,
region: type.location.identifier,
region: location,
);
return success;
}
@ -302,6 +312,7 @@ class HetznerServerProvider extends ServerProvider {
end,
'cpu',
);
if (cpuResult.data.isEmpty || !cpuResult.success) {
return GenericResult(
success: false,
@ -387,4 +398,170 @@ class HetznerServerProvider extends ServerProvider {
data: timestamp,
);
}
String dnsProviderToInfectName(final DnsProviderType dnsProvider) {
String dnsProviderType;
switch (dnsProvider) {
case DnsProviderType.digitalOcean:
dnsProviderType = 'DIGITALOCEAN';
break;
case DnsProviderType.cloudflare:
default:
dnsProviderType = 'CLOUDFLARE';
break;
}
return dnsProviderType;
}
Future<GenericResult<CallbackDialogueBranching?>> launchInstallation(
final LaunchInstallationData installationData,
) async {
final volumeResult = await _adapter.api().createVolume();
if (!volumeResult.success || volumeResult.data == null) {
return GenericResult(
data: CallbackDialogueBranching(
choices: [
CallbackDialogueChoice(
title: 'basis.cancel'.tr(),
callback: null,
),
CallbackDialogueChoice(
title: 'basis.try_again'.tr(),
callback: () async => launchInstallation(installationData),
),
],
description:
volumeResult.message ?? 'modals.volume_creation_error'.tr(),
title: 'modals.unexpected_error'.tr(),
),
success: false,
message: volumeResult.message,
code: volumeResult.code,
);
}
final volume = volumeResult.data!;
final serverApiToken = StringGenerators.apiToken();
final hostname = getHostnameFromDomain(installationData.domainName);
final serverResult = await _adapter.api().createServer(
dnsApiToken: installationData.dnsApiToken,
rootUser: installationData.rootUser,
domainName: installationData.domainName,
serverType: installationData.serverType.identifier,
dnsProviderType:
dnsProviderToInfectName(installationData.dnsProviderType),
hostName: hostname,
volumeId: volume.id,
base64Password: base64.encode(
utf8.encode(installationData.rootUser.password ?? 'PASS'),
),
databasePassword: StringGenerators.dbPassword(),
serverApiToken: serverApiToken,
);
if (!serverResult.success || serverResult.data == null) {
await _adapter.api().deleteVolume(volume);
await Future.delayed(const Duration(seconds: 5));
if (serverResult.message != null &&
serverResult.message == 'uniqueness_error') {
return GenericResult(
data: CallbackDialogueBranching(
choices: [
CallbackDialogueChoice(
title: 'basis.cancel'.tr(),
callback: null,
),
CallbackDialogueChoice(
title: 'basis.yes'.tr(),
callback: () async {
final deleting = await deleteServer(hostname);
if (deleting.success) {
return launchInstallation(installationData);
}
return deleting;
},
),
],
description: volumeResult.message ?? 'modals.destroy_server'.tr(),
title: 'modals.already_exists'.tr(),
),
success: false,
message: volumeResult.message,
code: volumeResult.code,
);
} else {
return GenericResult(
data: CallbackDialogueBranching(
choices: [
CallbackDialogueChoice(
title: 'basis.cancel'.tr(),
callback: null,
),
CallbackDialogueChoice(
title: 'basis.try_again'.tr(),
callback: () async => launchInstallation(installationData),
),
],
description:
volumeResult.message ?? 'recovering.generic_error'.tr(),
title: 'modals.unexpected_error'.tr(),
),
success: false,
message: volumeResult.message,
code: volumeResult.code,
);
}
}
final serverDetails = ServerHostingDetails(
id: serverResult.data['server']['id'],
ip4: serverResult.data['server']['public_net']['ipv4']['ip'],
createTime: DateTime.now(),
volume: volume,
apiToken: serverApiToken,
provider: ServerProviderType.hetzner,
);
final createDnsResult = await _adapter.api().createReverseDns(
serverId: serverDetails.id,
ip4: serverDetails.ip4,
dnsPtr: installationData.domainName,
);
if (!createDnsResult.success) {
return GenericResult(
data: CallbackDialogueBranching(
choices: [
CallbackDialogueChoice(
title: 'basis.cancel'.tr(),
callback: null,
),
CallbackDialogueChoice(
title: 'basis.try_again'.tr(),
callback: () async {
final deletion = await deleteServer(hostname);
if (deletion.success) {
return launchInstallation(installationData);
}
return deletion;
},
),
],
description: volumeResult.message ?? 'recovering.generic_error'.tr(),
title: 'modals.unexpected_error'.tr(),
),
success: false,
message: volumeResult.message,
code: volumeResult.code,
);
}
}
Future<GenericResult<CallbackDialogueBranching?>> deleteServer(
final String hostname,
) async {}
}