animiassumenda
Version:
Blox live, desktop app for managing staking accounts
395 lines (368 loc) • 14.2 kB
text/typescript
import net from 'net';
import * as AWS from 'aws-sdk';
import { v4 as uuidv4 } from 'uuid';
import config from '~app/backend/common/config';
import { Log } from '~app/backend/common/logger/logger';
import { Catch, CatchClass, Step } from '~app/backend/decorators';
import UserService from '~app/backend/services/users/users.service';
import Connection from '~app/backend/common/store-manager/connection';
import VersionService from '~app/backend/services/version/version.service';
// TODO import from .env
const defaultAwsOptions = {
apiVersion: '2016-11-15',
region: 'us-west-1'
};
<AwsService>()
export default class AwsService {
private ec2!: AWS.EC2;
private readonly keyName: string = 'BLOX_INFRA_KEY_PAIR';
private readonly securityGroupName: string = 'BLOX_INFRA_GROUP';
private storePrefix: string;
private readonly versionService: VersionService;
private readonly userService: UserService;
private logger: Log;
constructor(prefix: string = '') {
this.logger = new Log('aws');
this.storePrefix = prefix;
if (!this.ec2 && Connection.db(this.storePrefix).exists('credentials')) {
this.setAWSCredentials();
}
this.versionService = new VersionService();
this.userService = new UserService();
}
static async validateAWSCredentials({ accessKeyId, secretAccessKey }) {
const ec2 = new AWS.EC2({
...defaultAwsOptions,
credentials: {
accessKeyId,
secretAccessKey
}
});
try {
await ec2.describeInstances().promise();
await ec2.describeAddresses().promise();
} catch (error) {
throw new Error(error.message);
}
return true;
}
async setAWSCredentials(): Promise<any> {
const credentials: any = Connection.db(this.storePrefix).get('credentials');
this.ec2 = new AWS.EC2({
...defaultAwsOptions,
credentials
});
}
async validateAWSPermissions() {
try {
await this.ec2.describeInstances().promise();
await this.ec2.describeAddresses().promise();
} catch (error) {
Connection.db(this.storePrefix).delete('credentials');
throw new Error(error.message);
}
}
async createEc2KeyPair() {
if (Connection.db(this.storePrefix).exists('keyPair')) return;
const {
KeyPairId: pairId,
KeyMaterial: privateKey
} = await this.ec2
.createKeyPair({ KeyName: `${this.keyName}-${Connection.db(this.storePrefix).get('uuid')}` })
.promise();
Connection.db(this.storePrefix).set('keyPair', { pairId, privateKey });
}
async createElasticIp() {
if (Connection.db(this.storePrefix).exists('addressId')) return;
const {
AllocationId: addressId,
PublicIp: publicIp
} = await this.ec2.allocateAddress({ Domain: 'vpc' }).promise();
Connection.db(this.storePrefix).set('addressId', addressId);
Connection.db(this.storePrefix).set('publicIp', publicIp);
}
/**
* Build security group permissions depending of which sate of instance setup we're on.
*/
getSecurityGroupPermissions({ defaultSshPort, customSshPort, httpPort }: { defaultSshPort?: boolean, customSshPort?: boolean, httpPort?: boolean }): any[] {
const securityGroupPermissions = [];
if (httpPort) {
securityGroupPermissions.push({
IpProtocol: 'tcp',
FromPort: 8200,
ToPort: 8200,
IpRanges: [{ CidrIp: '0.0.0.0/0' }]
});
}
if (defaultSshPort) {
securityGroupPermissions.push({
IpProtocol: 'tcp',
FromPort: 22,
ToPort: 22,
IpRanges: [{ CidrIp: '0.0.0.0/0' }]
});
}
if (customSshPort) {
securityGroupPermissions.push({
IpProtocol: 'tcp',
FromPort: config.env.TARGET_SSH_PORT,
ToPort: config.env.TARGET_SSH_PORT,
IpRanges: [{ CidrIp: '0.0.0.0/0' }]
});
}
return securityGroupPermissions;
}
/**
* If security group exists - return it's ID otherwise create it and return ID.
*/
async getSecurityGroupId() {
const securityGroupId = Connection.db(this.storePrefix).get('securityGroupId');
if (securityGroupId) {
return securityGroupId;
}
const vpcList = await this.ec2.describeVpcs().promise();
if (!vpcList || !Array.isArray(vpcList.Vpcs)) {
this.logger.error('wrong vpcList', vpcList);
throw new Error('get Vpcs failed');
}
const vpc = vpcList.Vpcs[0].VpcId;
const uuid = uuidv4();
const securityData = await this.ec2
.createSecurityGroup({
Description: `${this.securityGroupName}-${uuid}`,
GroupName: `${this.securityGroupName}-${uuid}`,
VpcId: vpc
})
.promise();
Connection.db(this.storePrefix).set('securityGroupId', securityData.GroupId);
return securityData.GroupId;
}
async createSecurityGroup() {
const securityGroupId = await this.getSecurityGroupId();
// validate if in main.json we have port AND port === TARGET PORT (2200)
if (Connection.db(this.storePrefix).exists('port') && Connection.db(this.storePrefix).get('port') === config.env.TARGET_SSH_PORT) {
// If port 2200 exists in blox.json, it means that:
// 1) security group already created before with all ports opened
// 2) this security group is used for all blox-live-related instances
// 3) and port 22 is closed (it is closed after each new instance installation)
// Because of this on reinstall we will not be able to access newly created instance.
// In order to fix it, we should open port 22 again, and after reinstall close it again.
if (securityGroupId) {
const IpPermissions = this.getSecurityGroupPermissions({ defaultSshPort: true });
// Revoke it first, in case if on any failure before - we didn't revoke it before.
// If we didn't revoke it before and rule exists as ALLOW - it will fail entire process as "Already exists ALLOW rule"
await this.ec2.revokeSecurityGroupIngress({
GroupId: securityGroupId,
IpPermissions,
}).promise();
await this.ec2.authorizeSecurityGroupIngress({
GroupId: securityGroupId,
IpPermissions,
}).promise();
}
// Only after opening port 22 we can delete custom 2200 port in order to access new instance on port 22.
Connection.db(this.storePrefix).delete('port');
return;
}
// Authorize all ports on new instance
const IpPermissions = this.getSecurityGroupPermissions({
defaultSshPort: true,
customSshPort: true,
httpPort: true
});
await this.ec2.authorizeSecurityGroupIngress({
GroupId: securityGroupId,
IpPermissions,
}).promise();
Connection.db(this.storePrefix).delete('port');
}
async optimizeInstanceSecurity() {
const securityGroupId = await this.getSecurityGroupId();
if (!securityGroupId) {
this.logger.error('securityGroupId is not set!');
throw new Error('No security group exists in configuration file.');
}
const IpPermissions = this.getSecurityGroupPermissions({ defaultSshPort: true });
await this.ec2.revokeSecurityGroupIngress({
GroupId: securityGroupId,
IpPermissions,
}).promise();
}
async createInstance() {
if (Connection.db(this.storePrefix).exists('instanceId')) return;
const data = await this.ec2.runInstances({
ImageId: 'ami-0d3caf10672b8e870', // Amazon Linux. for us-west-1
InstanceType: 't2.micro',
SecurityGroupIds: [Connection.db(this.storePrefix).get('securityGroupId')],
KeyName: `${this.keyName}-${Connection.db(this.storePrefix).get('uuid')}`,
MinCount: 1,
MaxCount: 1
}).promise();
const instanceId = data.Instances![0].InstanceId;
await this.ec2
.waitFor('instanceRunning', { InstanceIds: [instanceId] })
.promise();
Connection.db(this.storePrefix).set('instanceId', instanceId);
const keyVaultVersion = await this.versionService.getLatestKeyVaultVersion();
const userProfile = await this.userService.get();
const tagsOptions: AWS.EC2.Types.CreateTagsRequest = {
Resources: [instanceId],
Tags: [
{ Key: 'Name', Value: 'blox-staking'},
{ Key: 'kv-version', Value: `${keyVaultVersion}` },
{ Key: 'org-id', Value: `${userProfile.organizationId}`},
]
};
await this.ec2.createTags(tagsOptions).promise();
await this.ec2.associateAddress({
AllocationId: Connection.db(this.storePrefix).get('addressId'),
InstanceId: instanceId
}).promise();
await new Promise((resolve) => setTimeout(resolve, 25000)); // hard delay for 25sec
}
async updatePluginTag() {
const instanceId = Connection.db(this.storePrefix).get('instanceId');
const keyVaultPluginVersion = Connection.db(this.storePrefix).get('keyVaultPluginVersion');
const tagsOptions: AWS.EC2.Types.CreateTagsRequest = {
Resources: [instanceId],
Tags: [
{ Key: 'kv-plugin-version', Value: `${keyVaultPluginVersion}` },
]
};
await this.ec2.createTags(tagsOptions).promise();
}
async uninstallItems() {
await this.ec2.terminateInstances({ InstanceIds: [Connection.db(this.storePrefix).get('instanceId')] }).promise();
await this.ec2.waitFor('instanceTerminated', { InstanceIds: [Connection.db(this.storePrefix).get('instanceId')] }).promise();
await this.ec2.releaseAddress({ AllocationId: Connection.db(this.storePrefix).get('addressId') }).promise();
const keyPair = Connection.db(this.storePrefix).get('keyPair');
await this.ec2.deleteKeyPair({ KeyPairId: keyPair.pairId }).promise();
await this.ec2.deleteSecurityGroup({ GroupId: Connection.db(this.storePrefix).get('securityGroupId'), DryRun: false }).promise();
Connection.db(this.storePrefix).clear();
/*
if (Store.isExist(tempStorePrefix)) {
Store.getStore(tempStorePrefix).clear();
}
*/
}
async truncateServer() {
const sources: any = {
instanceId: Connection.db(this.storePrefix).get('instanceId'),
addressId: Connection.db(this.storePrefix).get('addressId')
};
await this.destroyResources(sources);
return { isActive: true };
}
async truncateOldKvResources() {
const userProfile = await this.userService.get();
const instances = await this.ec2.describeInstances().promise();
const addresses = (await this.ec2.describeAddresses().promise()).Addresses;
const activeInstanceId = Connection.db(this.storePrefix).get('instanceId');
const kvOldOrgInstances = instances.Reservations.reduce((aggr, reserv) => {
// eslint-disable-next-line no-param-reassign
aggr = [
...aggr,
...reserv.Instances
.filter(instance => instance.Tags.find(tag => tag.Key === 'org-id' && tag.Value === `${userProfile.organizationId}`))
.filter(instance => instance.InstanceId !== activeInstanceId)
];
return aggr;
}, []);
// eslint-disable-next-line no-restricted-syntax
for (const oldInstance of kvOldOrgInstances) {
const instanceId = oldInstance.InstanceId;
const filteredAssocs = addresses.filter(addr => addr.InstanceId === instanceId);
const params = {
instanceId,
addressId: filteredAssocs[0]?.AllocationId,
securityGroupId: oldInstance.SecurityGroups[0]?.GroupId
};
this.logger.info('going to destroy aws resources', params);
// eslint-disable-next-line no-await-in-loop
await this.destroyResources(params);
}
return { isActive: true };
}
async destroyResources({ instanceId = null, addressId = null, securityGroupId = null }) {
if (instanceId) {
await this.ec2.terminateInstances({ InstanceIds: [instanceId] }).promise();
await this.ec2.waitFor('instanceTerminated', { InstanceIds: [instanceId] }).promise();
}
addressId && await this.ec2.releaseAddress({ AllocationId: addressId }).promise();
try {
securityGroupId && await this.ec2.deleteSecurityGroup({ GroupId: securityGroupId }).promise();
} catch (e) {
this.logger.error('Error in destroy aws resources process', e);
}
}
async rebootInstance() {
const configPort: any = Connection.db(this.storePrefix).get('port') || config.env.port;
const configIp: any = Connection.db(this.storePrefix).get('publicIp');
await this.ec2.rebootInstances({ InstanceIds: [Connection.db(this.storePrefix).get('instanceId')] }).promise();
await new Promise((resolve, reject) => {
let totalSeconds = 0;
const DELAY = 5000; // 5 sec
const intervalId = setInterval(() => {
const socket = new net.Socket();
const onError = () => {
socket.destroy();
this.logger.debug('waiting', Connection.db(this.storePrefix).get('publicIp'), totalSeconds);
if (totalSeconds >= 80000) { // 80 sec
this.logger.debug('Reached max timeout, exiting...', intervalId);
clearInterval(intervalId);
reject(new Error('Reached max timeout'));
}
totalSeconds += DELAY;
};
socket.setTimeout(1000);
socket.once('error', onError);
socket.once('timeout', onError);
socket.connect(configPort, configIp, () => {
this.logger.debug('Server is online');
socket.destroy();
clearInterval(intervalId);
resolve({});
});
}, DELAY);
});
}
}