api/services/MachineService.js
/**
* Nanocloud turns any traditional software into a cloud solution, without
* changing or redeveloping existing source code.
*
* Copyright (C) 2016 Nanocloud Software
*
* This file is part of Nanocloud.
*
* Nanocloud is free software; you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* Nanocloud is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
/* global App, ConfigService, BrokerLog, Machine, Image, User, ConfigService, Machine, PlazaService, StorageService, Team, UserMachine */
const _ = require('lodash');
const Promise = require('bluebird');
const ManualDriver = require('../drivers/manual/driver');
const AWSDriver = require('../drivers/aws/driver');
const DummyDriver = require('../drivers/dummy/driver');
const QemuDriver = require('../drivers/qemu/driver');
const OpenstackDriver = require('../drivers/openstack/driver');
const promisePoller = require('promise-poller').default;
const request = Promise.promisify(require('request'));
/**
* Service responssible of the machine pool
*
* @class MachineService
*/
const driverNotInitializedError = new Error('Driver not initialized');
const driverAlreadyInitializedError = new Error('Driver already initialized');
const drivers = {
manual : ManualDriver,
aws : AWSDriver,
dummy : DummyDriver,
qemu : QemuDriver,
openstack : OpenstackDriver
};
/**
* The underlying driver used by the service.
*
* @property _driver
* @type {Object}
* @private
*/
let _driver = null;
/**
* The promise returned by `initialize`. Used to prevent multiple
* initializtions.
*
* @property initializing
* @type {Promise}
* @private
*/
let _initializing = null;
/**
* Returns a Promise that reject `err` if `condition` if false. A resolved
* Promise otherwise.
*
* @method assert
* @private
* @param {Boolean} condition The rejection condition
* @param {Object} err The rejected error if condition is false
* @return {Promise[Object]}
*/
function assert(condition, err) {
if (condition) {
return Promise.resolve();
} else {
return Promise.reject(err);
}
}
/**
* Initialize the Iaas driver. It uses the `ConfigService` variables:
* - iaas: the name of the iaas driver to use
*
* @method initialize
* @return {Promise}
*/
function initialize() {
return assert(_driver === null, driverAlreadyInitializedError)
.then(() => {
if (_initializing) {
return _initializing;
}
_initializing = ConfigService.get('iaas', 'instancesSize')
.then((config) => {
return Image.findOrCreate({
buildFrom: null
}, {
iaasId: null,
name: 'Default',
buildFrom: null,
deleted: false,
instancesSize: config.instancesSize,
})
.then(() => {
_driver = new (drivers[config.iaas])();
return _driver.initialize()
.then(() => {
updateMachinesPool();
return null;
});
});
});
return _initializing;
});
}
/**
* Retreive a machine for the specified user. If the user already has a machine,
* then this machine is returned. Otherwise, if a machine is available, it is
* affected to the user. Fails if there is no available machine.
*
* @method getMachineForUser
* @param {User} The user associated to the machine
* @param {Image} The image associated to the machine
* @return {Promise[Machine]} The user's machine
*/
function getMachineForUser(user, image) {
return assert(!!_driver, driverNotInitializedError)
.then(() => {
return ConfigService.get('creditLimit');
})
.then((config) => {
if (config.creditLimit !== '' && parseFloat(user.credit) >= parseFloat(config.creditLimit)) {
return new Promise.reject('Exceeded credit');
}
})
.then(() => {
return Machine.find({ image: image.id }).populate('users', { id: user.id });
})
.then((userMachines) => {
_.remove(userMachines, (machine) => machine.users.length === 0);
if (!userMachines.length) {
return new Promise((resolve, reject) => {
Promise.props({
machines: Machine.find({ image: image.id }).populate('users'),
config: ConfigService.get('ldapUsersPerMachine', 'ldapActivated')
})
.then(({machines, config}) => {
// If ldap is actived, we remove machines who have reached the maximum users limit.
// else if ldap is not activated, the limit is 1.
_.remove(machines, (machine) =>
machine.users.length >= ((config.ldapActivated) ? config.ldapUsersPerMachine : 1));
// Order machines by number of users to assign the user to a machine already assigned.
machines = _.sortBy(machines, (machine) => { return machine.users.length; });
_.reverse(machines);
if (machines.length) {
if (_.findIndex(machines, {status: 'running'}) !== -1) {
let row = _.findIndex(machines, {status: 'running'});
machines[row].user = user.id;
_createBrokerLog(machines[row], 'Assigned')
.then(() => {
return UserMachine.create({
user: user.id,
machine: machines[row].id
});
})
.then(() => {
updateMachinesPool();
delete machines[row].users;
return resolve(machines[row]);
});
} else if (_.findIndex(machines, {status: 'booting'}) !== -1) {
let row = _.findIndex(machines, {status: 'booting'});
machines[row].user = user.id;
_createBrokerLog(machines[row], 'Assigned')
.then(() => {
return increaseMachineEndDate(machines[row]);
})
.then(() => {
return UserMachine.create({
user: user.id,
machine: machines[row].id
});
})
.then(() => {
updateMachinesPool();
return reject(`A machine have been assigned to you, it will be available shortly.`);
});
}
} else {
return Promise.reject('A machine is booting for you. Please retry in one minute.');
}
});
});
} else {
return ConfigService.get('neverTerminateMachine')
.then((config) => {
if (config.neverTerminateMachine) {
if (userMachines[0].status === 'stopped') {
startMachine(userMachines[0]);
return Promise.reject('Your machine is starting. Please retry in one minute.');
} else if (userMachines[0].status === 'running') {
delete userMachines[0].users;
return Promise.resolve(userMachines[0]);
} else {
return Promise.reject(`Your machine is ${userMachines[0].status}. Please retry in one minute.`);
}
} else if (userMachines[0].status === 'booting') {
return Promise.reject(`A machine have been assigned to you, it will be available shortly.`);
} else {
delete userMachines[0].users;
return Promise.resolve(userMachines[0]);
}
});
}
});
}
/**
* Return the name of the underlying iaas driver.
*
* @method driverName
* @return {String}
*/
function driverName() {
return _driver.name();
}
/**
* Check if the driver support session duration.
* Manual driver don't support it.
*
* @method isSessionDurationSupported
* @return {Boolean}
*/
function isSessionDurationSupported() {
return driverName() !== 'manual';
}
/**
* Set the user's machine endDate to now + `ConfigService:sessionDuration`
*
* @method increaseMachineEndDate
* @param {Machine} machine The machine to update
* @return {Promise}
*/
function increaseMachineEndDate(machine) {
if (isSessionDurationSupported()) {
return ConfigService.get('sessionDuration')
.then((config) => {
return machine.setEndDate(config.sessionDuration)
.then(() => {
setTimeout(() => {
_shouldTerminateMachine(machine);
}, config.sessionDuration * 1000);
});
});
} else {
return Promise.resolve();
}
}
/**
* Ask the underlying driver to create a new machine. It uses the
* `ConfigService` variable:
* - machinesName: the name of the machine to be created
* If ldap is activated the machine join the domain and change his name with a reboot
*
* @method _createMachine
* @param image {Object[Image]} image to build the machine from
* @private
* @return {Promise}
*/
function _createMachine(image) {
return ConfigService.get('machinesName')
.then((config) => {
return _driver.createMachine({
name: config.machinesName
}, image);
})
.then((machine) => {
machine.status = 'booting';
machine.image = image.id;
_createBrokerLog(machine, 'Created');
return Machine.create(machine);
})
.then((machine) => {
return promisePoller({
taskFn: () => {
return machine.refresh()
.then((machine) => {
if (machine.status === 'running') {
return Promise.resolve(machine);
} else {
return Promise.reject(machine);
}
});
},
interval: 5000,
retries: 240 // Waiting 20 minutes maximum before considering that the machine have a problem
})
.catch((errs) => { // If timeout is reached
let machine = errs.pop(); // On timeout, promisePoller rejects with an array of all rejected promises. In our case, MachineService rejects the still booting machine. Let's pick the last one.
_createBrokerLog(machine, 'Machine take to many time to boot.');
_terminateMachine(machine);
throw machine;
});
})
.then((machine) => {
return Promise.props({
password: machine.getPassword(),
config: ConfigService.get('ldapActivated')
})
.then(({password, config}) => {
machine.password = password;
// If machine have been assigned when booting we have to keep endDate and user
delete machine.endDate;
delete machine.users;
/**
* If the driver is dummy, this will take more than 10 seconds to reboot,
* and the test timeout at 2 seconds, so we just make a condition for the dummy driver
*/
if (config.ldapActivated === true && _driver.name() !== 'dummy') {
machine.status = 'booting';
} else {
_createBrokerLog(machine, 'Available');
machine.killSession();
}
return Machine.update({id: machine.id}, machine);
});
})
.then((machines) => {
return ConfigService.get(
'ldapActivated',
'ldapConnectLogin',
'ldapConnectPassword',
'ldapDomain',
'ldapDns',
'ldapGroup'
)
.then((config) => {
if (config.ldapActivated === true && _driver.name() !== 'dummy') {
let newName = 'NANO' + Math.random().toString(36).slice(3, 14);
return promisePoller({
taskFn: () => {
return PlazaService.exec(machines[0].ip, machines[0].plazaport, {
command: [
`C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`,
`-Command`,
`$ComputerName = hostname;
$Password = ConvertTo-SecureString -String "${machines[0].password}" -AsPlainText -Force;
$Creds = New-Object -TypeName "System.Management.Automation.PSCredential" -ArgumentList "${machines[0].username}", $Password;
Rename-Computer -ComputerName $ComputerName -NewName ${newName} -LocalCredential $Creds -Force`
],
wait: true,
hideWindow: true,
username: machines[0].username
})
.catch((err) => {
// Ignore the 'exit status 1' error, the script was successfully executed.
return Promise.resolve(err);
});
},
interval: 3000,
timeout: 3000,
retries: 20
})
.then(() => {
return rebootMachine(machines[0]);
})
.then(() => {
return promisePoller({
taskFn: () => {
return PlazaService.exec(machines[0].ip, machines[0].plazaport, {
command: [
`C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`,
`-Command`,
`netsh interface ipv4 add dnsserver "Ethernet" address=${config.ldapDns} index=1;
$ComputerName = hostname;
$Password = ConvertTo-SecureString -String "${config.ldapConnectPassword}" -AsPlainText -Force;
$Creds = New-Object -TypeName "System.Management.Automation.PSCredential" -ArgumentList "${config.ldapConnectLogin}", $Password;
Add-Computer -DomainName "${config.ldapDomain}" -ComputerName $ComputerName -Credential $Creds`
],
wait: true,
hideWindow: true,
username: machines[0].username
})
.catch((err) => {
// Ignore the 'exit status 1' error, the script was successfully executed.
return Promise.resolve(err);
});
},
interval: 3000,
timeout: 3000,
retries: 20
});
})
.then(() => {
return rebootMachine(machines[0]);
})
.then((rebootedMachine) => {
return promisePoller({
taskFn: () => {
return PlazaService.exec(rebootedMachine.ip, rebootedMachine.plazaport, {
command: [
`C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`,
`-Command`,
`Net LocalGroup "Remote Desktop Users" ${config.ldapDomain}\\${config.ldapGroup} /ADD`
],
wait: true,
hideWindow: true,
username: rebootedMachine.username
})
.catch(() => {
// Ignore the 'exit status 1' error, the script was successfully executed.
return Promise.resolve(rebootedMachine);
});
},
interval: 3000,
timeout: 3000,
retries: 20
});
})
.then((machineWithGroup) => {
_createBrokerLog(machineWithGroup, 'Available');
});
}
});
})
.catch((errs) => {
return (errs);
});
}
/**
* Ask the driver to start the specified machine
*
* @method startMachine
* @public
* @return {Promise[Machine]}
*/
function startMachine(machine) {
if (_driver.startMachine) {
return _driver.startMachine(machine)
.then((machineStarting) => {
machineStarting.status = 'starting';
delete machine.users;
return Machine.update({
id: machineStarting.id
}, machineStarting);
})
.then((machines) => {
return promisePoller({
taskFn: () => {
return machines[0].refresh()
.then((machineRefreshed) => {
if (machineRefreshed.status === 'running') {
return Promise.resolve(machineRefreshed);
} else {
return Promise.reject(machineRefreshed);
}
});
},
interval: 5000,
retries: 100
})
.catch((errs) => { // If timeout is reached
let machine = errs.pop(); // On timeout, promisePoller rejects with an array of all rejected promises. In our case, MachineService rejects the still booting machine. Let's pick the last one.
_createBrokerLog(machine, 'Error waiting to start machine');
_terminateMachine(machine);
throw machine;
});
})
.then((machineStarted) => {
_createBrokerLog(machineStarted, 'Started');
delete machineStarted.users;
return Promise.props({
machines: Machine.update({
id: machine.id
}, machineStarted),
machineUsers: UserMachine.find({
machine: machine.id
})
});
})
.then(({machines, machineUsers}) => {
if (machineUsers.length) {
increaseMachineEndDate(machines[0]);
}
return Machine.findOne({ id: machines[0].id }).populate('users');
})
.then((machine) => {
return (machine);
});
} else {
return new Promise((resolve, reject) => {
return reject('Start machine feature is not available on this driver');
});
}
}
/**
* Ask the driver to stop the specified machine
*
* @method stopMachine
* @public
* @return {Promise[Machine]}
*/
function stopMachine(machine) {
if (_driver.stopMachine) {
return _driver.stopMachine(machine)
.then(() => {
machine.status = 'stopping';
delete machine.users;
return Machine.update({
id: machine.id
}, machine);
})
.then((machines) => {
return promisePoller({
taskFn: () => {
return machines[0].refresh()
.then((machineRefreshed) => {
if (machineRefreshed.status === 'stopped') {
return Promise.resolve(machineRefreshed);
} else {
return Promise.reject(machineRefreshed);
}
});
},
interval: 5000,
retries: 100
})
.catch((errs) => { // If timeout is reached
let machine = errs.pop(); // On timeout, promisePoller rejects with an array of all rejected promises. In our case, MachineService rejects the still booting machine. Let's pick the last one.
_createBrokerLog(machine, 'Error waiting to stop machine');
_terminateMachine(machine);
throw machine;
});
})
.then((machineStopped) => {
_createBrokerLog(machineStopped, 'Stopped');
delete machineStopped.users;
return Promise.props({
machines: Machine.update({
id: machine.id
}, machineStopped),
machineUsers: UserMachine.find({
machine: machine.id
})
});
})
.then(({machines, machineUsers}) => {
if (!machineUsers.length) {
updateMachinesPool();
}
return Machine.findOne({ id: machines[0].id }).populate('users');
})
.then((machine) => {
return (machine);
});
} else {
return new Promise((resolve, reject) => {
return reject('Stop machine feature is not available on this driver');
});
}
}
function _terminateMachine(machine) {
if (_driver.destroyMachine) {
return _driver.destroyMachine(machine)
.then(() => {
return Machine.destroy({
id: machine.id
});
})
.then(() => {
return _createBrokerLog(machine, 'Deleted');
});
}
}
/**
* Create new machines if needed in the pool. It uses the `ConfigService`
*
* @method updateMachinesPool
* @public
* @return {Promise}
*/
function updateMachinesPool() {
return assert(!!_driver, driverNotInitializedError)
.then(() => {
return Promise.props({
config: ConfigService.get('machinePoolSize'),
machinesCount: Promise.promisify(Machine.query)({
text: 'SELECT image, COUNT(image) FROM machine WHERE (SELECT COUNT(usermachine.user) FROM usermachine WHERE "machine" = machine.id) = 0 GROUP BY "machine"."image"',
values: []
}),
machines: Machine.find().populate('users'),
images: Image.find(),
})
.then(({config, machinesCount, machines, images}) => {
_.remove(machines, (machine) => machine.users.length !== 0);
let imagesDeleted = _.remove(images, (image) => image.deleted === true);
images.forEach((image) => {
let machineCreated = _.find(machinesCount.rows, (m) => m.image === image.id) || {count: 0};
let machinePoolSize = (image.poolSize !== null) ? image.poolSize : config.machinePoolSize;
let machineToRecreate = _.filter(machines, (m) => {
return m.image === image.id && m.flavor !== _driver.instancesSize(image.instancesSize);
}).length;
let machineToCreate = machinePoolSize - machineCreated.count;
let machineToDestroy = machineCreated.count - machinePoolSize;
if (machineToDestroy > 0) {
return Machine.find({
image: image.id,
})
.populate('users')
.then((machines) => {
_.remove(machines, (machine) => machine.users.length >= 1);
_.times(machineToDestroy, (index) => _terminateMachine(machines[index]));
_createBrokerLog({
type: _driver.name()
}, `Update machine pool for image ${image.name} from ${machineCreated.count} to ${+machineCreated.count - machineToDestroy} (-${machineToDestroy})`);
});
} else if (machineToCreate > 0) {
_.times(machineToCreate, () => _createMachine(image));
_createBrokerLog({
type: _driver.name()
}, `Update machine pool for image ${image.name} from ${machineCreated.count} to ${+machineCreated.count + machineToCreate} (+${machineToCreate})`);
} else if (machineToRecreate > 0) {
return Machine.find({
image: image.id,
or: [{
flavor: null
}, {
flavor: {
'!': _driver.instancesSize(image.instancesSize)
}
}]
})
.populate('users')
.then((machinesWithWrongSize) => {
_.remove(machinesWithWrongSize, (machine) => machine.users.length);
_.times(machineToRecreate, (index) => {
_terminateMachine(machinesWithWrongSize[index]);
_createMachine(image);
});
_createBrokerLog({
type: _driver.name(),
flavor: image.instancesSize
}, `Update machine pool for image ${image.name} recreate ${+machineToRecreate}`);
});
}
});
imagesDeleted.forEach((image) => {
let machineCreated = _.find(machinesCount.rows, (m) => m.image === image.id) || {count: 0};
let machineToDestroy = machineCreated.count;
if (machineToDestroy > 0) {
return Machine.find({
image: image.id,
})
.populate('users')
.then((machines) => {
_.remove(machines, (machine) => machine.users.length);
_.times(machineToDestroy, (index) => _terminateMachine(machines[index]));
_createBrokerLog({
type: _driver.name()
}, `Update machine pool for image ${image.name} from ${machineCreated.count} to ${+machineCreated.count - machineToDestroy} (-${machineToDestroy})`);
});
}
});
})
.then(() => {
return _createBrokerLog({
type: _driver.name()
}, 'Machine pool updated');
})
.catch(() => {
_createBrokerLog({
type: _driver.name()
}, 'Error while updating the pool');
});
});
}
/**
* Check if the specified machine should be terminated and terminate it if so.
* The machine will be terminated if the machine's endDate is in the past and if
* the user doesn't use it.
*
* @method _shouldTerminateMachine
* @private
* @return {null}
*/
function _shouldTerminateMachine(machine) {
Promise.props({
isActive: machine.isSessionActive(),
config: ConfigService.get('neverTerminateMachine'),
machineToTerminate: Machine.findOne({id: machine.id})
})
.then(({isActive, config, machineToTerminate}) => {
if (!isActive) {
const now = new Date();
if (machineToTerminate.endDate < now) {
if (config.neverTerminateMachine) {
machineToTerminate.endDate = null;
stopMachine(machineToTerminate);
} else {
// Machine.update(machineToTerminate.id, machineToTerminate)
UserMachine.destroy({
machine: machineToTerminate.id
})
.then(() => {
_terminateMachine(machineToTerminate);
});
}
}
}
});
return null;
}
/**
* Inform the broker that the user has open a session on his machine.
* It basically just call `increaseMachineEndDate`.
*
* @method sessionOpen
* @param {User} user The user that open the session
* @param {Image} image The image machine has boot with
* @return {Promise}
*/
function sessionOpen(user, image) {
return getMachineForUser(user, image)
.then((machine) => {
machine.endDate = null;
machine.user = user.id;
_createBrokerLog(machine, 'Opened')
.then(() => {
return StorageService.findOrCreate(user)
.then((storage) => {
return PlazaService.exec(machine.ip, machine.plazaport, {
command: [
`C:\\Windows\\System32\\net.exe`,
'use',
'z:',
`\\\\${storage.hostname}\\${storage.username}`,
`/user:${storage.username}`,
storage.password
],
wait: true,
hideWindow: true,
username: machine.username,
})
.catch(() => {
// User storage is probably already mounted
// When an image is published, sometimes storage does not work again
// Let's delete the currupted storage and recreate it
// Let's ignore the error silently
return PlazaService.exec(machine.ip, machine.plazaport, {
command: [
`C:\\Windows\\System32\\net.exe`,
'use',
'z:',
`/DELETE`,
`/YES`
],
wait: true,
hideWindow: true,
username: machine.username,
})
.then(() => {
return PlazaService.exec(machine.ip, machine.plazaport, {
command: [
`C:\\Windows\\System32\\net.exe`,
'use',
'z:',
`\\\\${storage.hostname}\\${storage.username}`,
`/user:${storage.username}`,
storage.password
],
wait: true,
hideWindow: true,
username: machine.username,
});
})
.then(() => {
return Promise.resolve();
});
});
})
.then(() => {
return PlazaService.exec(machine.ip, machine.plazaport, {
command: [
`C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`,
'-Command',
'-'
],
wait: true,
hideWindow: true,
username: machine.username,
stdin: '$a = New-Object -ComObject shell.application;$a.NameSpace( "Z:\" ).self.name = "Personal Storage"'
});
})
.then(() => {
if (user.team) {
Promise.props({
team: Team.findOne(user.team),
config: ConfigService.get('teamStorageAddress'),
})
.then(({team, config}) => {
let command = [
`C:\\Windows\\System32\\net.exe`,
'use',
'y:',
`\\\\${config.teamStorageAddress}\\${team.username}`,
`/user:${team.username}`,
team.password
];
return PlazaService.exec(machine.ip, machine.plazaport, {
command: command,
wait: true,
hideWindow: true,
username: machine.username
})
.catch(() => {
// Team storage is probably already mounted like user storage
// When an image is published, sometimes team storage does not work again
// Let's delete the currupted team storage and recreate it
// Let's ignore the error silently
return PlazaService.exec(machine.ip, machine.plazaport, {
command: [
`C:\\Windows\\System32\\net.exe`,
'use',
'y:',
`/DELETE`,
`/YES`
],
wait: true,
hideWindow: true,
username: machine.username
})
.then(() => {
return PlazaService.exec(machine.ip, machine.plazaport, {
command: command,
wait: true,
hideWindow: true,
username: machine.username
});
})
.then(() => {
return Promise.resolve();
});
});
})
.then(() => {
return PlazaService.exec(machine.ip, machine.plazaport, {
command: [
`C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`,
'-Command',
'-'
],
wait: true,
hideWindow: true,
username: machine.username,
stdin: '$a = New-Object -ComObject shell.application;$a.NameSpace( "Y:\" ).self.name = "Team"'
});
});
}
});
})
.finally(() => {
delete machine.user;
return Machine.update(machine.id, machine);
});
});
}
/**
* Inform if the driver used support Credit
*
* @method isUserCreditSupported
* @param {}
* @return {Boolean}
*/
function isUserCreditSupported() {
if (_driver.name() === 'aws') {
return true;
} else {
return false;
}
}
/**
* Inform the broker that the user's session has ended.
* It basically just call `increaseMachineEndDate`.
*
* @method sessionEnded
* @param {User} user The user that ended the session
* @param {Image} image The image used to boot user's machine
* @return {Promise}
*/
function sessionEnded(user, image) {
let promise = getMachineForUser(user, image)
.then((userMachine) => {
userMachine.user = user.id;
return _createBrokerLog(userMachine, 'Closed')
.then(() => {
return increaseMachineEndDate(userMachine);
});
});
if (isUserCreditSupported()) {
promise.then(() => {
return _driver.getUserCredit(user)
.then((creditUsed) => {
return User.update({
id: user.id
}, {
credit: creditUsed
});
});
});
}
return promise;
}
/**
* Return the list of machines with the status attribute up to date.
*
* @method machines
* @return {Promise[[]Object]}
*/
function machines() {
return Machine.find({
type: _driver.name()
})
.then((machines) => {
machines = machines.map((machine) => {
machine = machine.toObject();
return _driver.getServer(machine.id)
.then((server) => {
machine.status = server.status;
return machine;
});
});
return Promise.all(machines);
});
}
/*
* Create an image from a machine
* The image will be used as default image for future execution servers
*
* @method createImage
* @param {Object} Image object with `buildFrom` attribute set to the machine id to create image from
* @return {Promise[Image]} resolves to the created image
*/
function createImage(image) {
let newImage = null;
return Machine.findOne(image.buildFrom)
.then((machine) => {
return Image.findOne(machine.image)
.populate('apps');
})
.then((oldImage) => {
return _driver.createImage(image)
.then((image) => {
return Image.create(image);
})
.then((image) => {
newImage = image;
let promises = [];
oldImage.apps.forEach((app) => {
if (app.alias !== 'Desktop') {
promises.push(App.create({
alias: app.alias,
displayName: app.displayName,
filePath: app.filePath,
image: newImage.id
}));
}
});
return Promise.all(promises);
})
.then(() => {
updateMachinesPool();
return Promise.resolve(newImage);
});
});
}
/*
* Delete an image
*
* @method deleteImage
* @param {Object} Image object
* @return {Promise[Image]} resolves to the deleted image
*/
function deleteImage(image) {
return _driver.deleteImage(image)
.catch((err) => {
/**
* If the method is not implemented on the driver, it's not an
* error du to the driver, so we ignore this error silently
*/
if (err.message === 'Driver\'s method "deleteImage" not implemented') {
return Promise.resolve(image);
}
return Promise.reject(err);
});
}
/*
* Create a new broker log
*
* @method _createBrokerLog
* @param {Machine} the machine to log
* @param {string} the state to save (created, deleted, opened, ...)
* @return {Promise} created log
*/
function _createBrokerLog(machine, state) {
return Machine.count({
status: 'running'
})
.then((nbrMachines) => {
return BrokerLog.create({
userId: (machine.user) ? machine.user : null,
machineId: machine.id,
machineDriver: machine.type,
machineFlavor: machine.flavor,
state: state,
poolSize: nbrMachines
});
});
}
/**
* Retrieve the machine's data
*
* @method refresh
* @param {machine} Machine model
* @return {Promise[Machine]}
*/
function refresh(machine) {
return _driver.refresh(machine);
}
/**
* Retrieve the machine's password
*
* @method getPassword
* @param {machine} Machine model
* @return {Promise[String]}
*/
function getPassword(machine) {
return _driver.getPassword(machine);
}
/**
* Reboot the machine
*
* @method rebootMachine
* @param string Id of the machine
* @return {Promise[Object]}
*/
function rebootMachine(machine) {
return _driver.rebootMachine(machine)
.then(() => {
return Machine.update({
id: machine.id
}, {
status: 'booting'
});
})
.then((machines) => {
let updatedMachine = machines[0];
let requestOptions = {
url: 'http://' + updatedMachine.ip + ':' + updatedMachine.plazaport,
method: 'GET'
};
return new Promise((resolve) => {
setTimeout(() => {
return promisePoller({
taskFn: () => {
return request(requestOptions)
.then(() => {
return resolve(updatedMachine);
})
.catch(() => {
return Promise.reject(updatedMachine);
});
},
interval: 5000,
retries: 100
})
.catch((errs) => { // If timeout is reached
let machine = errs.pop(); // On timeout, promisePoller rejects with an array of all rejected promises. In our case, MachineService rejects the still booting machine. Let's pick the last one.
_createBrokerLog(machine, `Error rebooting machine ${machine.id}`);
_terminateMachine(machine);
throw machine;
});
}, 10000);
});
})
.then((updatedMachine) => {
_createBrokerLog(updatedMachine, `Machine rebooted`);
return Machine.update({
id: updatedMachine.id
}, {status: 'running'})
.then((machines) => {
return machines[0];
});
});
}
module.exports = {
initialize, getMachineForUser, driverName, sessionOpen, sessionEnded,
machines, createImage, refresh, getPassword, rebootMachine, startMachine,
stopMachine, updateMachinesPool, deleteImage
};