SDK JavaScript pkgcloud pour Cloudwatt

Site officiel : pkgcloud
Documentation : pkgcloud Getting Started
Remonter un bug : GitHub


Ce tutoriel a pour but de vous expliquer comment utiliser les APIs Cloudwatt pour le développement de vos applications avec la bibliothèque JavaScript pkgcloud.

Cloudwatt propose une gamme d’APIs puissantes qui peuvent être utilisées pour piloter vos Services Cloud (Serveurs Cloud, Stockage objet, Stockage bloc, Mise en réseau…). Pour en savoir plus, veuillez consulter notre référentiel des APIs Cloudwatt.

Installation de la librairie pkgcloud avec NodeJS

Avant de commencer, vous devez avoir une plateforme NodeJS fonctionnelle, vous pouvez le faire depuis le site officiel de NodeJS. Si vous utilisez Linux vous pouvez utiliser le gestionnaire de paquets de votre distribution.

La librairie du client pkgcloud peut être installé avec le Node Package Manager en utilisant le client de ligne de commande npm. Attention, les paquets NodeJS sont uniquement disponibles dans le répertoire dans lequel vous les avez installés.

$ npm install pkgcloud

Ce tutoriel se base sur la version 1.4.0 du librairie client pkgcloud. Si vous avez une version plus ancienne, vous pouvez mettre à jour le paquet avec la commande:

$ npm update pkgcloud

De plus, vous devez installer certains paquets de développement pour faciliter les développements sur NodeJS :

$ npm install q underscore

“q” est une librairie qui implémente paradigme des Promises (il est important de bien comprendre ce concept avant de continuer) et underscore est juste une librairie fournissant quelques fonctions utilitaires.

Utiliser la librairie client pkgcloud

Voici un scénario dont les différentes étapes serviront d’exemple à l’utilisation de la librairie client pkgcloud :

  • déclarer les informations d’identification,
  • importer une paire de clés SSH,
  • créer un réseau et un sous-réseau,
  • créer un routeur,
  • lancer une instance,
  • allouer une IP flottante,
  • associer l’IP flottante à une instance,
  • créer un volume,
  • attacher le volume à l’instance,
  • redémarrer l’instance,
  • terminer l’instance.

Vous pouvez retrouver une description de cette librairie dans la documentation officielle.

Importer

Voici les modules à importer dans notre scénario de test :

    var pkgcloud = require('pkgcloud');
    var _        = require('underscore');
    var Q        = require('q');

Déclarer vos informations d’identification pour utiliser les APIs de Cloudwatt

Pour commencer à travailler avec les APIs vous devez récupérer vos informations d’identification depuis la console Cloudwatt. Vous devez utiliser l’identifiant (username) et le mot de passe (password) que vous avez utilisés pour créer votre compte. L’URL d’authenfication est disponible dans la console. Actuellement, cette URL est la suivante :

  • https://identity.fr1.cloudwatt.com/v2.0

La première fonction JavaScript que nous allons écrire permet de formater les informations d’authentification dans un Objet que nous allons pouvoir réutiliser plus tard dans les différents clients.

/**
 * @description Returns an Object filled with the credentials
 * @returns {
 *  {
 *    provider: string,
 *    username: string,
 *    password: string,
 *    region: string,
 *    tenantId : string,
 *    authUrl: string
 *  }
 *}
 */
function getCredentials() {
  /*
   Due to some weird implementation on the region recognition we have to set the
   default to RegionOne
   */
  var authUrl,
    url = require('url'),
    region = typeof process.env.OS_REGION_NAME === 'undefined' ?
      'RegionOne': process.env.OS_REGION_NAME;

  if (typeof process.env.OS_USERNAME === 'undefined') {
    throw new Error('OS_USERNAME environment variable is not set');
  }

  if (typeof process.env.OS_PASSWORD === 'undefined') {
    throw new Error('OS_PASSWORD environment variable is not set');
  }

  if (typeof process.env.OS_TENANT_ID=== 'undefined') {
    throw new Error('OS_TENANT_ID environment variable is not set');
  }

  if (typeof process.env.OS_AUTH_URL === 'undefined') {
    throw new Error('OS_AUTH_URL environment variable is not set');
  } else {
    /*
    The pkgcloud module works only for the v2.0 version of the identity service
    and will automatically add the suffix at the end of the url, so we need to
    remove it from the AUTH_URL
     */
    authUrl = url.resolve(process.env.OS_AUTH_URL, '/');
  }

  return {
    provider: 'openstack',
    username: process.env.OS_USERNAME,
    password: process.env.OS_PASSWORD,
    tenantId: process.env.OS_TENANT_ID,
    region: region,
    authUrl: authUrl
  };
}

Dans cet exemple, nous fournissons à la fonction les informations des variables d’environnement requises. Comme vous pouvez vous y attendre, c’est à vous d’adapter en fonction de vos besoins. Toutes les considérations de sécurité ont été délibérément ignorées afin de ne pas sacrifier la lisibilité.

Créer un réseau privé :

Cette fonction va créer un nouveau réseau privé et une liste de sous-réseaux qui pourront être attachés à ce dernier. Vous remarquerez comment la fonction getCredentials est appelée pour gérer l’authentification. La fonction va renvoyer une promesse qui devrait vous permettre de facilement traiter les résultats.

/**
 * @description Create a private network and subnets.
 * @param {string}   networkName  The name of the created network
 * @param {string[]} cidrList     Array of Classless Inter-Domain Routing for the
 * subnets
 * @returns {Q.promise}           A promise containing the network and subnets
 * objects if successful, the errors otherwise
 */
function createPrivateNetwork(networkName, cidrList){
  var credentials = getCredentials(),
    networkClient = pkgcloud.network.createClient(credentials),
    d = Q.defer();


  networkClient.createNetwork({
    name: networkName,
    adminStateUp : true
  }, function (err, network) {
    if (err) {
      d.reject(err);
    } else {
      //We may want to create several subnets in this network
      Q.all(_.map(cidrList, function(cidr) {
        var d = Q.defer();

        networkClient.createSubnet({
          networkId: network.id,
          ip_version: 4,
          cidr: cidr
        }, function (err, subnet) {
          if (err) {
            d.reject(err);
          } else {
            d.resolve(network, subnet);
          }
        });
        return d.promise;
      }))
      .then(
        function(subnets) {
          d.resolve(network, subnets);
        },
        d.reject
      );
    }
  });

  return d.promise;
}

Connecter un réseau privé à Internet

La création d’un routeur ne fonctionne pas sur pkgcloud. Il n’est donc pas possible de le connecter à Internet.

Importer une clé publique ssh

Cette fonction permet d’importer une clé ssh dans Nova, le composant gérant les serveurs virtuels. Cette clé ssh sera utilisée lors de la création des instances afin de permettre un accès sans mot de passe à vos instances.

Nous n’allons pas couvrir la phase de génération de la clé et nous allons partir du principe que celle-ci est stockée sous forme de fichier sur votre système. Nous fournissons juste un exemple, ainsi, n’hésitez pas à faire des adaptations.

/**
 * @description This function imports a public key into nova.
 * @param {string} name    The name the keypair is imported to
 * @param {string} keyPath The path of the key file on the disk
 * @returns {Q.promise}    A promise containing the keypair object if
 * successful, the error otherwise
 */
function createKeypair(name, keyPath){
  var fs = require('fs'),
    credentials = getCredentials(),
    novaClient = pkgcloud.compute.createClient(credentials),
    d = Q.defer();

  fs.readFile(keyPath, function (err, data) {
    if (err) {
      d.reject(err);
    } else {
      novaClient.addKey({name: name, public_key: data.toString()},
        function (err, keypair) {
          if (err) {
            d.reject(err);
          } else {
            d.resolve(keypair);
          }
        });
    }
  });
  return d.promise;
}

Créer un groupe de sécurité et définir les règles

/**
* Add a rule to a security group.
* @param {string}         groupId    The group id on which to attach the rule
* @param {strint|integer} fromPort   The starting port of the rule
* @param {string|integer} toPort     The ending port of the rule
* @param {string}         ipProtocol The protocol on which the rule apply, can
* be tcp, udp or icmp
* @param {string}         cidr       The cidr of the rule, ex: 0.0.0.0/0
* @returns {Q.promise}               A promise provided with the security group
* if successful, the error otherwise
*/
function createSecGroup(name, description) {
  var credentials = getCredentials(),
    novaClient = pkgcloud.compute.createClient(credentials),
    d = Q.defer();

  novaClient.addGroup({
    name: name,
    description: description || ''
  }, function (err, sec_group) {
    if (err) {
      d.reject(err);
    } else {
      d.resolve(sec_group);
    }
  });

  return d.promise;
}

/**
* Add a rule to a security group.
* @param {string}         groupId    The group id on which to attach the rule
* @param {strint|integer} fromPort   The starting port of the rule
* @param {string|integer} toPort     The ending port of the rule
* @param {string}         ipProtocol The protocol on which the rule apply, can
* be tcp, udp, icmp or any
* @param {string}         cidr       The cidr of the rule, ex: 0.0.0.0/0
* @returns {Q.promise}               A promise provided with the rule if
* successful, the error otherwise
*/
function addRule(groupId, fromPort, toPort, ipProtocol, cidr) {
  var credentials = getCredentials(),
    novaClient = pkgcloud.compute.createClient(credentials),
    d = Q.defer();

  novaClient.addRule({
    groupId: groupId,
    fromPort: fromPort,
    toPort: toPort,
    ipProtocol: ipProtocol,
    cidr: cidr
  }, function (err, rule, res) {
    if (err) {
      d.reject(err);
    } else {
      d.resolve(rule);
    }
  })

  return d.promise;
}

Lancer une instance

/**
 * @description Launch an instance with the flavor, image, and name specified
 * as arguments, attach networks and keypair
 * @param {string}      instanceName      Name of the instance
 * @param {string}      flavorId          Id of the needed flavor
 * @param {string}      imageId           Id of the needed image
 * @param {string}      keypairName       Name of the keypair to provide
 * @param {string[]}    networksId        Array of network IDs to link to the
 * @param {string[]}    securityGroupsId  Array of security groups ID to link
 * to the instance
 * @returns {Q.promise} A promise containing the server object if
 * successful, the error otherwise
 */
function launchInstance(
  instanceName, flavorId, imageId, keyPair, networksId)
{
  var credentials = getCredentials(),
    novaClient = pkgcloud.compute.createClient(credentials),
    d = Q.defer();
  novaClient.createServer({
    name: instanceName,
    flavor: flavorId,
    image: imageId,
    keypair: keyPair,
    networks: [{uuid: networksId}]
  }, function (err, server) {
    if (err) {
      d.reject(err);
    } else {
      d.resolve(server);
    }
  });

  return d.promise;
}

L’API pkgcloud supporte les métadonnées et les personnalités. Par contre les données utilisateurs ne le sont pas.

Allouer une IP flottante

Nous allons maintenant corser les choses. Nous savons comment créer des ressources et comme vous vous en doutez, vous pouvez créer une adresse IP flottante de la même manière que les autres ressources que nous avons utilisé (réseau, sous-réseau, paire de clés)

À la place, nous allons ajouter une petite modification dans cette fonction. Nous allons d’abord vérifier si l’adresse IP flottante n’est pas déjà utilisée. Si et seulement si aucune adresse n’est disponible, nous allons en créer une nouvelle.

/**
 * @description         First let's check if there are floating ips available.
 * If there are, we'll return a random one.
 * If not, we'll create one and return it.
 * @param {string}      pool A pool to filter floating IPs
 * @returns {Q.promise} A promise containing the floating IP object if
 * successful, the error otherwise
 */
function getOrCreateFloatingIP(pool) {
  var credentials = getCredentials(),
    novaClient = pkgcloud.compute.createClient(credentials),
    d = Q.defer();
  novaClient.getFloatingIps(function (err, flIPs) {
    if (err) {
      d.reject(err);
    } else {
      //Filtering out associated floating IPs
      flIPs = _.filter(flIPs, function(flIP) {
        return !! flIP.instance_id;
      });

      //Filtering floating IPs accord to a specific pool
      if (typeof pool === 'string') {
        flIPs = _.filter(flIPs, function(flIP) {
          return flIP.pool === pool;
        });
      }
      if (flIPs.length) {
        d.resolve(_.sample(flIPs));
      } else {
        novaClient.allocateNewFloatingIp(pool, function (err, flIP) {
          if (err) {
            d.reject(err);
          } else {
            d.resolve(flIP);
          }
        });
      }
    }
  });
  return d.promise;
}

Associer une adresse IP flottante à une instance

Associer l’adresse IP à une instance est également très simple. La librairie ne permet pas de définir le port : faites attention à l’ordre du réseau lorsque vous créez votre instance.

/**
 * @description Attach a floating IP to a server
 * @param {Server|string} server A server object, or the ID of a server
 * @param {IP|string}     ip     An IP object, or string of the IP
 * @returns {Q.promise}   A promise with nothing provided if
 * successful, the error otherwise
 */
function attachFloatingIp (server, ip) {
  var credentials = getCredentials(),
    novaClient = pkgcloud.compute.createClient(credentials),
    d = Q.defer();
  novaClient.addFloatingIp(server, ip, function (err) {
    if (err) {
      d.reject(err);
    } else {
      d.resolve();
    }
  });
  return d.promise;
}

Créer un volume

La création de volume cinder s’effectue en utilisant la méthode createVolume du client cinder.

/**
 * Create a new cinder volume.
 * @param {volumeName|string} the name of the cinder volume.
 * @param {volumeDescription|string} a brief description of the volume.
 * @param {volumeSize|int} the size of the volume to create.
 * @returns {Q.promise}   A promise containing the volume object if
 * successfully created, the error otherwise
 */
function createVolume(volumeName, volumeDescription, volumeSize) {
  var credentials = getCredentials(),
    cinderClient = pkgcloud.blockstorage.createClient(credentials),
    d = Q.defer();

  cinderClient.serviceType = 'volumev2';// In order to use cinder v2 Api

  cinderClient.createVolume({
      name: volumeName,
      description: volumeDescription,
      size: volumeSize
  }, function (err, volume){
    if (err) {
      d.reject(err);
    } else {
      d.resolve(volume);
    }
  });

  return d.promise;
}

Attacher un volume à une instance

Attacher un volume à une instance s’effectue au travers du client Nova.

/**
 * Attach a volume to an instance.
 * @param {Server|string} server A server object, or the ID of a server
 * @param {Volume|string} volume A volume object, or the ID of a volume
 * @returns {Q.promise}   A promise containing the volume object if
 * successful, the error otherwise
 */
function attachVolume(server, volume) {
  var credentials = getCredentials(),
    novaClient = pkgcloud.compute.createClient(credentials),
    d = Q.defer();

  novaClient.attachVolume(server, volume, function (err, volume){
    if (err) {
      d.reject(err);
    } else {
      d.resolve(volume);
    }
  });

  return d.promise;
}

Redémarrer ou Terminer une instance

/**
 * Reboot a server
 * @param {Server|string} server      A server object, or the ID of a server
 * @param {string}        rebootType  A string for the reboot type, it can be
 * 'SOFT' or 'HARD'
 * @returns {Q.promise}   A promise with nothing provided if
 * successful, the error otherwise
 */
function rebootServer(server, rebootType) {
  var credentials = getCredentials(),
    novaClient = pkgcloud.compute.createClient(credentials),
    d = Q.defer();
  //reboot_type can be "SOFT" or "HARD"
  novaClient.rebootServer(server, {type: rebootType},
    function (err) {
      if (err) {
        d.reject(err);
      } else {
        d.resolve();
      }
    });

  return d.promise;
}


/**
 * Delete a server
 * @param {Server|string} server      A server object, or the ID of a server
 * @returns {Q.promise}   A promise with nothing provided if
 * successful, the error otherwise
 */
function deleteServer(server) {
  var credentials = getCredentials(),
    novaClient = pkgcloud.compute.createClient(credentials),
    d = Q.defer();

  novaClient.destroyServer(server, function (err) {
    if (err) {
      d.reject(err);
    } else {
      d.resolve();
    }
  });
  return d.promise;
}

Utiliser ces fonctions

Pour commencer, nous définissons une fonction permettant de nettoyer notre environnement :

function clean() {
  var credentials = getCredentials(),
    networkClient = pkgcloud.network.createClient(credentials),
    novaClient = pkgcloud.compute.createClient(credentials),
    cinderClient = pkgcloud.blockstorage.createClient(credentials),
    destroyNetwork, destroyKey, destroyServer, destroyVolume, destroySecurityGroup;

  function getNetworksId(networksLabel) {
    var credentials = getCredentials(),
      networkClient = pkgcloud.network.createClient(credentials),
      d = Q.defer();
    networkClient.getNetworks(function(err, networks) {
      if (err) {
        d.reject(err);
      } else {
        d.resolve(_.filter(networks, function (network) {
          return !! _.find(networksLabel, function (networkLabel) {
            return networkLabel === network.name;
          });
        }).map(function (network) {
          return network.id;
        }));
      }
    });

    return d.promise;
  }

   function getVolumesId(volumesLabel) {
    var credentials = getCredentials(),
      cinderClient = pkgcloud.blockstorage.createClient(credentials),
      d = Q.defer();
    cinderClient.serviceType = 'volumev2';
    cinderClient.getVolumes(function(err, volumes) {
      if (err) {
        d.reject(err);
      } else {
        d.resolve(_.filter(volumes, function (volume) {
          return !! _.find(volumesLabel, function (volumeLabel) {
            return volumeLabel === volume.name;
          });
        }).map(function (volume) {
          return volume.id;
        }));
      }
    });

    return d.promise;
  }

  destroyNetwork = getNetworksId(['network_1']).then(function (networks) {
    return Q.all(_.map(networks, function(network){
      var d = Q.defer(), i;
      i = setInterval( function () {
        networkClient.destroyNetwork(network, function (err){
          if (!err) {
            clearInterval(i);
            d.resolve();
          }
        });
      }, 500);
      return d.promise;
    }));
  });

  destroyVolume = getVolumesId(['volume_1']).then(function (volumes) {
    return Q.all(_.map(volumes, function(volume){
      var d = Q.defer(), i;
      i = setInterval( function () {
        cinderClient.deleteVolume(volume, function (err){
          if (!err) {
            clearInterval(i);
            d.resolve();
          }
        });
      }, 500);
      return d.promise;
    }));
  });

  destroyKey = (function () {
    var d = Q.defer();
    novaClient.destroyKey('keypair_1', function (err) {
      d.resolve();
    });
    return d.promise;
  })();

  destroyServer = (function () {
    var d = Q.defer();
    novaClient.getServers(function (err, servers) {
      var server = _.find(servers, function (server) {
        return server.name === 'instance_50';
      });
      if ( server ) {
        novaClient.destroyServer(server, function (err) {
          if (err) {
            d.reject(err);
          } else {
            d.resolve();
          }
        });
      } else {
        d.resolve();
      }

    });
    return d.promise;
  })();

  destroySecurityGroup = (function () {
    var d = Q.defer();
    novaClient.listGroups(function (err, sec_groups) {
      var sec_group = _.find(sec_groups, function (sec_group) {
        return sec_group.name === 'group_1';
      });
      if ( !!sec_group ) {
        novaClient.destroyGroup(sec_group.id, function (err) {
          if (err) {
            d.reject(err);
          } else {
            d.resolve();
          }
        });
      } else {
        d.resolve();
      }
    });
    return d.promise;
  })();

  console.log('cleaning old build');
  console.log('\tdestroy server');
  return destroyServer
    .then(function () {
      console.log('\tdestroy key pair');
      return destroyKey;
    })
    .then(function () {
      console.log('\tdestroy network');
      return destroyNetwork;
    })
    .then(function () {
      console.log('\tdestroy volume');
      return destroyVolume;
    })
    .then(function () {
      console.log('\tdestroy security group');
      return destroySecurityGroup;
    });
}

Voici une fonction main() simple qui va utiliser les fonctions que nous avons définies ci-dessus pour implémenter un scénario très simple :

function main() {
  var network, server, keypair, volume, imageId, secGroup;

  function waitForActiveStatus(server) {
    var credentials = getCredentials(),
      novaClient = pkgcloud.compute.createClient(credentials),
      d = Q.defer(), i, dots = [];
    console.log('waiting for ' + server.name + ' to be in Active state\n');
    i = setInterval(function () {
      novaClient.getServer(server, function (err, server) {
        if (err) {
          d.reject(err);
        } else {
          if (server.status === 'RUNNING') {
            clearInterval(i);
            process.stdout.write(
                server.name + ' is RUNNING, continuing\n'
            );
            d.resolve(server);
          }
        }
      });
    }, 15000);

    return d.promise;
  }

  function getImageId(imageLabel) {
    var credentials = getCredentials(),
      novaClient = pkgcloud.compute.createClient(credentials),
      d = Q.defer();
    novaClient.getImages(function(err, images) {
      if (err) {
        d.reject(err);
      } else {
        var image = images.filter(function (image) {
            return imageLabel === image.name;
        });
        d.resolve(image[0].id);
      }
    });
    return d.promise;
  }

  function getVolumeId(volumeLabel) {
    var credentials = getCredentials(),
      cinderClient = pkgcloud.blockstorage.createClient(credentials),
      d = Q.defer();
    cinderClient.serviceType = 'volumev2';
    cinderClient.getVolumes(function(err, volumes) {
      if (err) {
        d.reject(err);
       } else {
        var volume = volumes.filter(function (volume) {
            return volumeLabel === volume.name;
        });
        d.resolve(volume[0].id);
      }
    });
    return d.promise;
  }

  clean()
    .then(
    function () {
      console.log('creating network, keypair and security group');
      return Q.all([
        createPrivateNetwork('network_1', ["192.168.199.0/24"]),
        createVolume('volume_1', 'my_volume', 2),
        createKeypair('keypair_1', '/home/zarrouk/zarrouk.pub'),
        createSecGroup('group_1'),
        getImageId('Fedora 20')
      ]);
    })
    .then(
    function (results) {
      network = results[0];
      volume = results[1];
      keypair = results[2];
      secGroup = results[3];
      imageId = results[4];
      console.log('add rule to sec_group');
      addRule(secGroup.id, 8000, 8001, 'tcp', '0.0.0.0/0');
      console.log('creating instance');
      return launchInstance(
        'instance_50',
        '22',    //flavor id
        imageId, //image id
        keypair,
        network.id
      );
    })
    .then(function (s) {
      server = s;
      return waitForActiveStatus(server);
    })
    .then(function () {
      console.log('get floating IP');
      return getOrCreateFloatingIP('public');
    })
    .then(function (floatingIP) {
      console.log('attach floating IP');
      return attachFloatingIp(server, floatingIP.ip);
    })
    .then(function () {
      console.log('attach volume');
      getVolumeId('volume_1').then(function (Ids) {
          return attachVolume(server, Ids[0]);
      })
    })
    .then(function () {
      console.log('reboot');
      return rebootServer(server, 'SOFT');
    })
    .then(function () {
      return waitForActiveStatus(server);
    })

    .then(
    function () {
      console.log('DONE!');
    },
    console.error //display the error in the shell
  );
}

Le code complet de l’exemple :

Si vous souhaitez tester cet exemple, voici le script complet :

'use strict';
var pkgcloud = require('pkgcloud');
var _        = require('underscore');
var Q        = require('q');

/**
 * @description Returns an Object filled with the credentials
 * @returns {
 *  {
 *    provider: string,
 *    username: string,
 *    password: string,
 *    region: string,
 *    tenantId : string,
 *    authUrl: string
 *  }
 *}
 */
function getCredentials() {
  /*
   Due to some weird implementation on the region recognition we have to set the
   default to RegionOne
   */
  var authUrl,
    url = require('url'),
    region = typeof process.env.OS_REGION_NAME === 'undefined' ?
      'RegionOne': process.env.OS_REGION_NAME;

  if (typeof process.env.OS_USERNAME === 'undefined') {
    throw new Error('OS_USERNAME environment variable is not set');
  }

  if (typeof process.env.OS_PASSWORD === 'undefined') {
    throw new Error('OS_PASSWORD environment variable is not set');
  }

  if (typeof process.env.OS_TENANT_ID=== 'undefined') {
    throw new Error('OS_TENANT_ID environment variable is not set');
  }

  if (typeof process.env.OS_AUTH_URL === 'undefined') {
    throw new Error('OS_AUTH_URL environment variable is not set');
  } else {
    /*
    The pkgcloud module works only for the v2.0 version of the identity service
    and will automatically add the suffix at the end of the url, so we need to
    remove it from the AUTH_URL
     */
    authUrl = url.resolve(process.env.OS_AUTH_URL, '/');
  }

  return {
    provider: 'openstack',
    username: process.env.OS_USERNAME,
    password: process.env.OS_PASSWORD,
    tenantId: process.env.OS_TENANT_ID,
    region: region,
    authUrl: authUrl
  };
}

/**
 * @description Create a private network and subnets.
 * @param {string}   networkName  The name of the created network
 * @param {string[]} cidrList     Array of Classless Inter-Domain Routing for the
 * subnets
 * @returns {Q.promise}           A promise containing the network and subnets
 * objects if successful, the errors otherwise
 */
function createPrivateNetwork(networkName, cidrList){
  var credentials = getCredentials(),
    networkClient = pkgcloud.network.createClient(credentials),
    d = Q.defer();


  networkClient.createNetwork({
    name: networkName,
    adminStateUp : true
  }, function (err, network) {
    if (err) {
      d.reject(err);
    } else {
      //We may want to create several subnets in this network
      Q.all(_.map(cidrList, function(cidr) {
        var d = Q.defer();

        networkClient.createSubnet({
          networkId: network.id,
          ip_version: 4,
          cidr: cidr
        }, function (err, subnet) {
          if (err) {
            d.reject(err);
          } else {
            d.resolve(network, subnet);
          }
        });
        return d.promise;
      }))
        .then(
        function(subnets) {
          d.resolve(network, subnets);
        },
        d.reject
      );
    }
  });

  return d.promise;
}

/**
 * @description This function imports a public key into nova.
 * @param {string} name    The name the keypair is imported to
 * @param {string} keyPath The path of the key file on the disk
 * @returns {Q.promise}    A promise containing the keypair object if
 * successful, the error otherwise
 */
function createKeypair(name, keyPath){
  var fs = require('fs'),
    credentials = getCredentials(),
    novaClient = pkgcloud.compute.createClient(credentials),
    d = Q.defer();

  fs.readFile(keyPath, function (err, data) {
    if (err) {
      d.reject(err);
    } else {
      novaClient.addKey({name: name, public_key: data.toString()},
        function (err, keypair) {
          if (err) {
            d.reject(err);
          } else {
            d.resolve(keypair);
          }
        });
    }
  });
  return d.promise;
}

/**
* Add a rule to a security group.
* @param {string}         groupId    The group id on which to attach the rule
* @param {strint|integer} fromPort   The starting port of the rule
* @param {string|integer} toPort     The ending port of the rule
* @param {string}         ipProtocol The protocol on which the rule apply, can
* be tcp, udp or icmp
* @param {string}         cidr       The cidr of the rule, ex: 0.0.0.0/0
* @returns {Q.promise}               A promise provided with the security group
* if successful, the error otherwise
*/
function createSecGroup(name, description) {
  var credentials = getCredentials(),
    novaClient = pkgcloud.compute.createClient(credentials),
    d = Q.defer();

  novaClient.addGroup({
    name: name,
    description: description || ''
  }, function (err, sec_group) {
    if (err) {
      d.reject(err);
    } else {
      d.resolve(sec_group);
    }
  });

  return d.promise;
}

/**
* Add a rule to a security group.
* @param {string}         groupId    The group id on which to attach the rule
* @param {strint|integer} fromPort   The starting port of the rule
* @param {string|integer} toPort     The ending port of the rule
* @param {string}         ipProtocol The protocol on which the rule apply, can
* be tcp, udp, icmp or any
* @param {string}         cidr       The cidr of the rule, ex: 0.0.0.0/0
* @returns {Q.promise}               A promise provided with the rule if
* successful, the error otherwise
*/
function addRule(groupId, fromPort, toPort, ipProtocol, cidr) {
  var credentials = getCredentials(),
    novaClient = pkgcloud.compute.createClient(credentials),
    d = Q.defer();

  novaClient.addRule({
    groupId: groupId,
    fromPort: fromPort,
    toPort: toPort,
    ipProtocol: ipProtocol,
    cidr: cidr
  }, function (err, rule, res) {
    if (err) {
      d.reject(err);
    } else {
      d.resolve(rule);
    }
  })

  return d.promise;
}

/**
 * @description Launch an instance with the flavor, image, and name specified
 * as arguments, attach networks and keypair
 * @param {string}      instanceName      Name of the instance
 * @param {string}      flavorId          Id of the needed flavor
 * @param {string}      imageId           Id of the needed image
 * @param {string}      keypairName       Name of the keypair to provide
 * @param {string[]}    networksId        Array of network IDs to link to the
 * @param {string[]}    securityGroupsId  Array of security groups ID to link
 * to the instance
 * @returns {Q.promise} A promise containing the server object if
 * successful, the error otherwise
 */
function launchInstance(
  instanceName, flavorId, imageId, keyPair, networksId)
{
  var credentials = getCredentials(),
    novaClient = pkgcloud.compute.createClient(credentials),
    d = Q.defer();
  novaClient.createServer({
    name: instanceName,
    flavor: flavorId,
    image: imageId,
    keypair: keyPair,

    networks: [{uuid: networksId}]
  }, function (err, server) {
    if (err) {
      d.reject(err);
    } else {
      d.resolve(server);
    }
  });

  return d.promise;
}

/**
 * @description         First let's check if there are floating ips available.
 * If there are, we'll return a random one.
 * If not, we'll create one and return it.
 * @param {string}      pool A pool to filter floating IPs
 * @returns {Q.promise} A promise containing the floating IP object if
 * successful, the error otherwise
 */
function getOrCreateFloatingIP(pool) {
  var credentials = getCredentials(),
    novaClient = pkgcloud.compute.createClient(credentials),
    d = Q.defer();
  novaClient.getFloatingIps(function (err, flIPs) {
    if (err) {
      d.reject(err);
    } else {
      //Filtering out associated floating IPs
      flIPs = _.filter(flIPs, function(flIP) {
        return !! flIP.instance_id;
      });

      //Filtering floating IPs accord to a specific pool
      if (typeof pool === 'string') {
        flIPs = _.filter(flIPs, function(flIP) {
          return flIP.pool === pool;
        });
      }
      if (flIPs.length) {
        d.resolve(_.sample(flIPs));
      } else {
        novaClient.allocateNewFloatingIp(pool, function (err, flIP) {
          if (err) {
            d.reject(err);
          } else {
            d.resolve(flIP);
          }
        });
      }
    }
  });
  return d.promise;
}


/**
 * @description Attach a floating IP to a server
 * @param {Server|string} server A server object, or the ID of a server
 * @param {IP|string}     ip     An IP object, or string of the IP
 * @returns {Q.promise}   A promise with nothing provided if
 * successful, the error otherwise
 */
function attachFloatingIp (server, ip) {
  var credentials = getCredentials(),
    novaClient = pkgcloud.compute.createClient(credentials),
    d = Q.defer();
  novaClient.addFloatingIp(server, ip, function (err) {
    if (err) {
      d.reject(err);
    } else {
      d.resolve();
    }
  });
  return d.promise;
}

 /**
 * Create a new cinder volume.
 * @param {volumeName|string} the name of the cinder volume.
 * @param {volumeDescription|string} a brief description of the volume.
 * @param {volumeSize|int} the size of the volume to create.
 * @returns {Q.promise}   A promise containing the volume object if
 * successfully created, the error otherwise
 */
function createVolume(volumeName, volumeDescription, volumeSize) {
  var credentials = getCredentials(),
    cinderClient = pkgcloud.blockstorage.createClient(credentials),
    d = Q.defer();

  cinderClient.serviceType = 'volumev2';// In order to use cinder v2 Api

  cinderClient.createVolume({
      name: volumeName,
      description: volumeDescription,
      size: volumeSize
  }, function (err, volume){
    if (err) {
      d.reject(err);
    } else {
      d.resolve(volume);
    }
  });

  return d.promise;
}

/**
 * Attach a volume to an instance.
 * @param   {Server|string} server A server object, or the ID of a server
 * @param   {Volume|string} volume A volume object, or the ID of a volume
 * @returns {Q.promise}            A promise containing the volume object if
 * successful, the error otherwise
 */
function attachVolume(server, volume) {
  var credentials = getCredentials(),
    novaClient = pkgcloud.compute.createClient(credentials),
    d = Q.defer();

  novaClient.attachVolume(server, volume, function (err, volume){
    if (err) {
      d.reject(err);
    } else {
      d.resolve(volume);
    }
  });

  return d.promise;
}

/**
 * Reboot a server
 * @param {Server|string} server      A server object, or the ID of a server
 * @param {string}        rebootType  A string for the reboot type, it can be
 * 'SOFT' or 'HARD'
 * @returns {Q.promise}   A promise with nothing provided if
 * successful, the error otherwise
 */
function rebootServer(server, rebootType) {
  var credentials = getCredentials(),
    novaClient = pkgcloud.compute.createClient(credentials),
    d = Q.defer();
  //reboot_type can be "SOFT" or "HARD"
  novaClient.rebootServer(server, {type: rebootType},
    function (err) {
      if (err) {
        d.reject(err);
      } else {
        d.resolve();
      }
    });

  return d.promise;
}


/**
 * Delete a server
 * @param {Server|string} server      A server object, or the ID of a server
 * @returns {Q.promise}   A promise with nothing provided if
 * successful, the error otherwise
 */
function deleteServer(server) {
  var credentials = getCredentials(),
    novaClient = pkgcloud.compute.createClient(credentials),
    d = Q.defer();

  novaClient.destroyServer(server, function (err) {
    if (err) {
      d.reject(err);
    } else {
      d.resolve();
    }
  });
  return d.promise;
}


function clean() {
  var credentials = getCredentials(),
    networkClient = pkgcloud.network.createClient(credentials),
    novaClient = pkgcloud.compute.createClient(credentials),
    cinderClient = pkgcloud.blockstorage.createClient(credentials),
    destroyNetwork, destroyKey, destroyServer, destroyVolume, destroySecurityGroup;

  function getNetworksId(networksLabel) {
    var credentials = getCredentials(),
      networkClient = pkgcloud.network.createClient(credentials),
      d = Q.defer();
    networkClient.getNetworks(function(err, networks) {
      if (err) {
        d.reject(err);
      } else {
        d.resolve(_.filter(networks, function (network) {
          return !! _.find(networksLabel, function (networkLabel) {
            return networkLabel === network.name;
          });
        }).map(function (network) {
          return network.id;
        }));
      }
    });

    return d.promise;
  }

   function getVolumesId(volumesLabel) {
    var credentials = getCredentials(),
      cinderClient = pkgcloud.blockstorage.createClient(credentials),
      d = Q.defer();
    cinderClient.serviceType = 'volumev2';
    cinderClient.getVolumes(function(err, volumes) {
      if (err) {
        d.reject(err);
      } else {
        d.resolve(_.filter(volumes, function (volume) {
          return !! _.find(volumesLabel, function (volumeLabel) {
            return volumeLabel === volume.name;
          });
        }).map(function (volume) {
          return volume.id;
        }));
      }
    });

    return d.promise;
  }

  destroyNetwork = getNetworksId(['network_1']).then(function (networks) {
    return Q.all(_.map(networks, function(network){
      var d = Q.defer(), i;
      i = setInterval( function () {
        networkClient.destroyNetwork(network, function (err){
          if (!err) {
            clearInterval(i);
            d.resolve();
          }
        });
      }, 500);
      return d.promise;
    }));
  });

  destroyVolume = getVolumesId(['volume_1']).then(function (volumes) {
    return Q.all(_.map(volumes, function(volume){
      var d = Q.defer(), i;
      i = setInterval( function () {
        cinderClient.deleteVolume(volume, function (err){
          if (!err) {
            clearInterval(i);
            d.resolve();
          }
        });
      }, 500);
      return d.promise;
    }));
  });

  destroyKey = (function () {
    var d = Q.defer();
    novaClient.destroyKey('keypair_1', function (err) {
      d.resolve();
    });
    return d.promise;
  })();

  destroyServer = (function () {
    var d = Q.defer();
    novaClient.getServers(function (err, servers) {
      var server = _.find(servers, function (server) {
        return server.name === 'instance_50';
      });
      if ( server ) {
        novaClient.destroyServer(server, function (err) {
          if (err) {
            d.reject(err);
          } else {
            d.resolve();
          }
        });
      } else {
        d.resolve();
      }

    });
    return d.promise;
  })();

  destroySecurityGroup = (function () {
    var d = Q.defer();
    novaClient.listGroups(function (err, sec_groups) {
      var sec_group = _.find(sec_groups, function (sec_group) {
        return sec_group.name === 'group_1';
      });
      if ( !!sec_group ) {
        novaClient.destroyGroup(sec_group.id, function (err) {
          if (err) {
            d.reject(err);
          } else {
            d.resolve();
          }
        });
      } else {
        d.resolve();
      }
    });
    return d.promise;
  })();

  console.log('cleaning old build');
  console.log('\tdestroy server');
  return destroyServer
    .then(function () {
      console.log('\tdestroy key pair');
      return destroyKey;
    })
    .then(function () {
      console.log('\tdestroy network');
      return destroyNetwork;
    })
    .then(function () {
      console.log('\tdestroy volume');
      return destroyVolume;
    })
    .then(function () {
      console.log('\tdestroy security group');
      return destroySecurityGroup;
    });
}


function main() {
  var network, server, keypair, volume, imageId, secGroup;

  function waitForActiveStatus(server) {
    var credentials = getCredentials(),
      novaClient = pkgcloud.compute.createClient(credentials),
      d = Q.defer(), i, dots = [];
    console.log('waiting for ' + server.name + ' to be in Active state\n');
    i = setInterval(function () {
      novaClient.getServer(server, function (err, server) {
        if (err) {
          d.reject(err);
        } else {
          if (server.status === 'RUNNING') {
            clearInterval(i);
            process.stdout.write(
                server.name + ' is RUNNING, continuing\n'
            );
            d.resolve(server);
          }
        }
      });
    }, 15000);

    return d.promise;
  }

  function getImageId(imageLabel) {
    var credentials = getCredentials(),
      novaClient = pkgcloud.compute.createClient(credentials),
      d = Q.defer();
    novaClient.getImages(function(err, images) {
      if (err) {
        d.reject(err);
      } else {
        var image = images.filter(function (image) {
            return imageLabel === image.name;
        });
        d.resolve(image[0].id);
      }
    });
    return d.promise;
  }

  function getVolumeId(volumeLabel) {
    var credentials = getCredentials(),
      cinderClient = pkgcloud.blockstorage.createClient(credentials),
      d = Q.defer();
    cinderClient.serviceType = 'volumev2';
    cinderClient.getVolumes(function(err, volumes) {
      if (err) {
        d.reject(err);
       } else {
        var volume = volumes.filter(function (volume) {
            return volumeLabel === volume.name;
        });
        d.resolve(volume[0].id);
      }
    });
    return d.promise;
  }

  clean()
    .then(
    function () {
      console.log('creating network, keypair and security group');
      return Q.all([
        createPrivateNetwork('network_1', ["192.168.199.0/24"]),
        createVolume('volume_1', 'my_volume', 2),
        createKeypair('keypair_1', '/home/zarrouk/zarrouk.pub'),
        createSecGroup('group_1'),
        getImageId('Fedora 20')
      ]);
    })
    .then(
    function (results) {
      network = results[0];
      volume = results[1];
      keypair = results[2];
      secGroup = results[3];
      imageId = results[4];
      console.log('add rule to sec_group');
      addRule(secGroup.id, 8000, 8001, 'tcp', '0.0.0.0/0');
      console.log('creating instance');
      return launchInstance(
        'instance_50',
        '22',    //flavor id
        imageId, //image id
        keypair,
        network.id
      );
    })
    .then(function (s) {
      server = s;
      return waitForActiveStatus(server);
    })
    .then(function () {
      console.log('get floating IP');
      return getOrCreateFloatingIP('public');
    })
    .then(function (floatingIP) {
      console.log('attach floating IP');
      return attachFloatingIp(server, floatingIP.ip);
    })
    .then(function () {
      console.log('attach volume');
      getVolumeId('volume_1').then(function (Ids) {
          return attachVolume(server, Ids[0]);
      })
    })
    .then(function () {
      console.log('reboot');
      return rebootServer(server, 'SOFT');
    })
    .then(function () {
      return waitForActiveStatus(server);
    })

    .then(
    function () {
      console.log('DONE!');
    },
    console.error //display the error in the shell
  );
}

main();