Ajouter un commentaire

1.5 Déploiement et configuration réseau d’une instance CloudStack

Découvrez comment créer une VM sous Debian à partir d’une image au travers de l’API CloudStack

L'article précédent vous a présenté comment déployer simplement une instance dans un environnement CloudStack, aujourd’hui nous poussons les possibilités un peu plus loin et augmentons quelque peu la technicité puisque nous y intégrons une gestion du réseau. L’aspect sécurité est omniprésent au sein de votre infrastructure virtuelle CloudStack : la politique de base est très stricte, rien n’entre et rien ne sort par défaut. Il vous incombe donc de procéder à l’autorisation des flux sortants depuis vos réseaux virtuels ainsi que des flux entrants à destination de vos instances.

La plupart d’entre vous disposent, à titre professionnel ou personnel, d’un accès ADSL ou Fibre via (pour la plupart) une « Box » fournie par votre opérateur derrière laquelle se trouvent vos postes personnels/professionnels ; tablettes ; smartphones … en adressage privé (non joignable directement depuis Internet).

Les réseaux CloudStack fonctionnent un peu de la même manière : toutes vos instances disposent d’adresses privées (non-joignables depuis Internet) et chacun de vos réseaux dispose d’un routeur virtuel (comparable à une Box ADSL/Fibre) par laquelle vos instances doivent passer pour sortir et au travers de laquelle les machines de l’extérieur doivent transiter pour joindre vos instances.

Le script fourni ci-dessous permet donc, en plus de la création initiale de l’instance abordée lors du précédent article, de créer des règles pour permettre à vos instances de sortir pour quatres usages : le PING, le HTTP (navigation web), le HTTPS (navigation web sécurisée) et le DNS (requêtes de correspondances IP <-> Noms de domaines).

De plus, nous ouvrons la connexion entrante sur le port 2222 (choisi arbitrairement) que nous faisons correspondre sur le port 22 (SSH, pour la console à distance) de notre instance.

De cette manière, en nous connectant à notre IP publique en SSH sur le port 2222 nous accéderons à notre instance qui, elle-même, pourra naviguer sur Internet via les ouvertures effectuées en sortie.

Déploiement de l'API

(les prérequis sont les mêmes que pour l'article précédent "Déploiement d'une instance GNU/LINUX Debian 7 sous CloudStack")

Dans un premier temps, l'utilisateur doit :
  • Disposer d’un réseau déjà déployé (pour les comptes Cloudstack Instance c’est normalement le cas).
  • Avoir la possibilité de créer une nouvelle instance (ne pas être à sa limite de création donc).
  • Déterminer l’ID du réseau auquel sera connecté son instance (via l’interface web ou l’API, de même, indications en commentaires dans le script).
  • Déterminer l’ID de l’offre de service (configuration CPU/RAM/Réseau) voulue, indications au sein du script.
[Code]

<?php
	// Liste complète des appels possibles : http://download.cloud.com/releases/3.0.6/api_3.0.6/TOC_User.html

	################################
	# Paramètres généraux de l'API #
	################################
	
	// Renseignez ici votre clef API ainsi que votre clef secrète
	// Nous conseillons de déporter cette information au sein d'un autre fichier PHP dont on restreindra l'accès
	define("APIKEY","d1Ii4ON33yqUahgtlsjnzK0o7QXNzXZONZKcW_rDcOV4jln48b5ujsSvU-mo9KsoLbp631VyCI4fLb3u176JZg");
	define("SECRETKEY","fBenapO2hjOd3Vaj-NDmrekTIK85B8Jl_m8wFld4gi0XvLXy4w0iJz-2srztoVqkGVoblT25DyG4709gibo_TA");
	//On définit l'URL d'appel de l'API (ou 'EndPoint')
	define("ENDPOINT","https://cloudstack.ikoula.com/client/api");
	
	#############################################################
	# Paramètres utilisateur de configuration d(es) instance(s) #
	#############################################################
	
	//Première instance :
	
		/* UUID(s) du réseau auquel sera connectée votre instance.
		Utilisez la requête API 'listNetworks' ou l'interface pour lister les réseaux existants et déterminer le réseau voulu
		*/
		$vm01['conf']['networkid'] = "<ID DU RESEAU QUE VOUS SOUHAITEZ UTILISER>";
		
		/* UUID de l'offre de service (configuration matérielle) voulue.
		Utilisez la requête API 'listServiceOfferings' pour lister les offres existantes.
		Actuellement :
			- m1.small : c6b89fea-1242-4f54-b15e-9d8ec8a0b7e8
			- m1.medium : 8dae8be9-5dae-4f81-89d1-b171f25ef3fd
			- m1.large : d2b2e7b9-4ffa-419e-9ef1-6d413f08deab
			- m1.extralarge : 1412009f-0e89-4cfc-a681-1cda0631094b
		*/
		$vm01['conf']['serviceofferingid'] = "<ID DE L'OFFRE DE SERVICE A UTILISER>";

		
		/* UUID du modèle de systeme d'exploitation.
		Vous pouvez aussi utiliser la requête API 'listTemplates' pour lister les modèles existants.
		Dans notre exemple nous allons nous baser sur le template 'Debian 7 - Minimal - 64bits'
		*/
		$vm01['conf']['templateid'] = "6d5b069b-4268-4c7c-810f-2793c2ac44fb";
		
		/* Nom de votre choix pour l'instance (Nom de la machine et d'affichage dans l'interface)
		Caractères alphanumériques uniquement. Ce nom doit être unique au sein de votre réseau. */
		$vm01['conf']['hostname'] = "<NOM DE VOTRE INSTANCE>";
		
/* ------------------------------ NE PLUS RIEN MODIFIER APRES CETTE LIGNE ----------------------------------- */
	$json_response = null;

	################################################
	# Récupération de l'ID de zone pour ce network #
	################################################
	// Requête API
	$args['command'] = "listNetworks";

	// On reprend l'ID de réseau renseigné plus tôt
	$args['id'] = $vm01['conf']['networkid'];

	// Execution de la requête
	sendRequest($args, $json_response);

	// Stockage de l'ID de zone pour ce réseau
	$vm01['conf']['zoneid'] = $json_response['listnetworksresponse']["network"][0]['zoneid'];

	// On réinitialise les variables utilisées pour l'envoi de requêtes API
	unset($args);
	
	######################################################
	# Récupération de l'id de l'adresse IP de ce network #
	######################################################
	// Requête API
	$args['command'] = "listPublicIpAddresses";
	$args['associatednetworkid'] = $vm01['conf']['networkid'];

	// Execution de la requête
	sendRequest($args, $json_response);

	// Stockage de l'ID de l'adresse IP publique du réseau
	$vm01['conf']['publicIPid'] = $json_response['listpublicipaddressesresponse']["publicipaddress"][0]['id'];

	// On réinitialise les variables utilisées pour l'envoi de requêtes API
	unset($args);
		
	####################################
	# Création de la première instance #
	####################################
	
	// Requête API
	$args['command'] = "deployVirtualMachine";
	$args['zoneid'] = $vm01['conf']['zoneid'];
	$args['serviceofferingid'] = $vm01['conf']['serviceofferingid'];
	$args['templateid'] = $vm01['conf']['templateid'];
	$args['networkids'] = $vm01['conf']['networkid'];
	$args['name'] = $vm01['conf']['hostname']; //Hostname
	$args['displayname'] = $args['name']; //Nom d'affichage
	
	//Type de retour : JSON ou XML (par défaut : XML)
	$args['response'] = "json";

	//On réinitialise les variables avant l'appel API
	
	// Initialisation du client API
	sendRequest($args, $json_response);
	
	//On vérifie la presence d'un Job
	if(preg_match("/^[0-9a-f\-]+$/", $json_response['deployvirtualmachineresponse']['jobid']) > 0)
	{
		$jobs[] = $json_response['deployvirtualmachineresponse']['jobid'];
	}
	else{
		echo "ID de job non trouvé.\n";
	}
	
	//On mémorise la correspondance name/id de l'instance au sein d'un tableau
	$vm01['conf']['id'] = $json_response['deployvirtualmachineresponse']['id'];
	
	//On utilise la fonction de vérification des jobs asynchrones
	if(!checkJobs($jobs))
		exit;
		
/*-------------------------------- Création des règles de sortie --------------------------------*/
	// On réinitialise les variables utilisées pour l'envoi de requêtes API
	unset($args);
	
	################
	# Ping sortant #
	################
	$args['command'] = "createEgressFirewallRule";
	$args['networkid'] = $vm01['conf']['networkid'];
	
	// Définition du protocole a filtrer ici ICMP (utiliser pour les ping), les protocole sont ICMP/UDP/TCP
	$args['protocol'] = "ICMP";
	// Définition du masque réseau qui nous permet d'autoriser certainne IP,
	// ici nous acceptons "le ping" vers toute les IPs
	$args['cidrlist'] = "0.0.0.0/0";
	// On définit le type ICMP le code 8 et une demande d'ECHO (echo-request) cela permet le ping
	$args['icmptype'] = 8;
	
	// On définit le code ICMP le code 0 doit être défini à 0 pour l'utilisation du type 8 pour l'ICMP
	$args['icmpcode'] = 0;
	
	// Execution de la requête
	sendRequest($args, $json_response);

	// Traitement de la requête
	if(preg_match("/^[0-9a-f\-]+$/", $json_response['createegressfirewallruleresponse']['jobid']) > 0)
	{
		$jobs[] = $json_response['createegressfirewallruleresponse']['jobid'];
	}
	else{
		echo "ID de job non trouvé.\n";
	}
	
	if(!checkJobs($jobs))
		exit;
	
	// On réinitialise les variables utilisées pour l'envoi de requêtes API
	unset($args);
	
	##########################
	# Protocole HTTP sortant #
	##########################
	$args['command'] = "createEgressFirewallRule";
	$args['networkid'] = $vm01['conf']['networkid'];
	
	// Définition du protocole a filtrer ici TCP (utiliser par le protocole HTTP), les protocole sont ICMP/UDP/TCP
	$args['protocol'] = "TCP";
	
	// Définition du masque réseau qui nous permet d'autoriser certainne IP,
	// ici nous accepton la connection vers toute les IP
	$args['cidrlist'] = "0.0.0.0/0";
	
	// Le port de début (port 80 port standart pour le HTTP)
	$args['startport'] = 80;
	
	// Le port de fin (port 80 port standart pour le HTTP)
	$args['endport'] = 80;
	
	// Execution de la requête
	sendRequest($args, $json_response);

	// Traitement de la requête
	if(preg_match("/^[0-9a-f\-]+$/", $json_response['createegressfirewallruleresponse']['jobid']) > 0)
	{
		$jobs[] = $json_response['createegressfirewallruleresponse']['jobid'];
	}
	else{
		echo "ID de job non trouvé.\n";
	}
	
	// On réinitialise les variables utilisées pour l'envoi de requêtes API
	unset($args);
	
	###########################
	# Protocole HTTPS sortant #
	###########################
	// Requête API
	$args['command'] = "createEgressFirewallRule";
	$args['networkid'] = $vm01['conf']['networkid'];
	
	// Définition du protocole a filtrer ici TCP (utiliser par le protocole HTTPS), les protocole sont ICMP/UDP/TCP
	$args['protocol'] = "TCP";
	
	// Définition du masque réseau qui nous permet d'autoriser certainne IP,
	// ici nous accepton la connection vers toute les IP
	$args['cidrlist'] = "0.0.0.0/0";
	
	// Le port de début (port 443 port standart pour le HTTPs)
	$args['startport'] = 443;
	
	// Le port de fin (port 443 port standart pour le HTTPs)
	$args['endport'] = 443;
	
	// Execution de la requête
	sendRequest($args, $json_response);

	// Traitement de la requête
	if(preg_match("/^[0-9a-f\-]+$/", $json_response['createegressfirewallruleresponse']['jobid']) > 0)
	{
		$jobs[] = $json_response['createegressfirewallruleresponse']['jobid'];
	}
	else{
		echo "ID de job non trouvé.\n";
	}
	
	// On réinitialise les variables utilisées pour l'envoi de requêtes API
	unset($args);

	
	#########################
	# Protocole DNS sortant #
	#########################
	// Requête API
	$args['command'] = "createEgressFirewallRule";
	$args['networkid'] = $vm01['conf']['networkid'];
	
	// Définition du protocole a filtrer ici UDP (utiliser par le protocole DNS), les protocole sont ICMP/UDP/TCP
	$args['protocol'] = "UDP";
	
	// Définition du masque réseau qui nous permet d'autoriser certainne IP,
	// ici nous accepton la connection vers toute les IP
	$args['cidrlist'] = "0.0.0.0/0";
	
	// Le port de début (port 443 port standart pour le HTTPs)
	$args['startport'] = 53;
	
	// Le port de fin (port 443 port standart pour le HTTPs)
	$args['endport'] = 53;
	
	// Execution de la requête
	sendRequest($args, $json_response);
	
	// Traitement de la requête
	if(preg_match("/^[0-9a-f\-]+$/", $json_response['createegressfirewallruleresponse']['jobid']) > 0)
	{
		$jobs[] = $json_response['createegressfirewallruleresponse']['jobid'];
	}
	else{
		echo "ID de job non trouvé.\n";
	}
	
	// On réinitialise les variables utilisées pour l'envoi de requêtes API
	unset($args);
	
	########################################
	# Ouverture du port public pour le SSH #
	########################################
	// Requête API
	$args['command'] = "createFirewallRule";
	$args['ipaddressid'] = $vm01['conf']['publicIPid'];
	
	// Définition du protocole a filtrer ici TCP (utiliser par le protocole SSH), les protocole sont ICMP/UDP/TCP
	$args['protocol'] = "TCP";
	
	// Définition du masque réseau qui nous permet d'autoriser certainne IP,
	// ici nous accepton la connection vers toute les IP
	$args['cidrlist'] = "0.0.0.0/0";
	
	// Le port de début (port 2222 port qui sera plus tard rediriger vers le port 22(ssh) de votre VM)
	$args['startport'] = 2222;
	
	// Le port de fin (port 2222 port qui sera plus tard rediriger vers le port 22(ssh) de votre VM)
	$args['endport'] = 2222;
	
	// Execution de la requête
	sendRequest($args, $json_response);
	
	// Traitement de la requête
	if(preg_match("/^[0-9a-f\-]+$/", $json_response['createfirewallruleresponse']['jobid']) > 0)
	{
		$jobs[] = $json_response['createfirewallruleresponse']['jobid'];
	}
	else{
		echo "ID de job non trouvé.\n";
	}
	
	// On réinitialise les variables utilisées pour l'envoi de requêtes API
	unset($args);
	
	###################################
	# Redirection de port pour le SSH #
	###################################
	// Requête API
	$args['command'] = "createPortForwardingRule";
	$args['ipaddressid'] = $vm01['conf']['publicIPid'];
	$args['virtualmachineid'] = $vm01['conf']['id'];
	
	// Définition du protocole a filtrer ici TCP (utiliser par le protocole SSH), les protocole sont ICMP/UDP/TCP
	$args['protocol'] = "TCP";

	// Le Port public sur lequels vous allez initialiser la connection et définie plus tôt comme ouvert
	$args['publicport'] = 2222;
	
	// Le SSH de votre VM
	$args['privateport'] = 22;
	
	// Execution de la requête
	sendRequest($args, $json_response);
	
	// Traitement de la requête
	if(preg_match("/^[0-9a-f\-]+$/", $json_response['createportforwardingruleresponse']['jobid']) > 0)
	{
		$jobs[] = $json_response['createportforwardingruleresponse']['jobid'];
	}
	else{
		echo "ID de job non trouvé.\n";
	}
	
	// On réinitialise les variables utilisées pour l'envoi de requêtes API
	unset($args);
	
	if(!checkJobs($jobs))
			exit;
	
/*-----------------------------------------------------------------------------------------------------------*/
	
	#############
	# Fonctions #
	#############
	
	//Fonction de gestion d'erreur(s) API
	function apiErrorCheck($json_response)
	{
		if(is_array($json_response))
		{
			$key = array_keys($json_response);
			if(isset($json_response['errorcode']))
			{
				echo "ERREUR : ".$json_response['errorcode']." - ".$json_response['errortext']."\n";
				exit;
			}
			if(isset($json_response['errorcode']) || (isset($key[0]) && isset($json_response[$key[0]]['errorcode'])))
			{
				echo "ERREUR : ".$json_response[$key[0]]['errorcode']." - ".$json_response[$key[0]]['errortext']."\n";
				exit;
			}
		}
		else
		{
			echo "ERREUR : PARAMETRE INVALIDE";
				exit;
		}
	}
	
	//Fonction d'envoi de requête à l'API
	function sendRequest($args, &$json_response)
	{
		$json_response = null;
		// Clef API
		$args['apikey'] = APIKEY;
		$args['response'] = "json";
		//On classe les paramètres
		ksort($args);
		// On construit la requête HTTP basée sur les paramètres contenus dans $args
		$query = http_build_query($args);
		// On s'assure de bien remplacer toutes les occurences de '+' par des '%20'
		$query = str_replace("+", "%20", $query);
		//On utilise la clef secrète et un algorithme HMAC SHA-1 sur la requête pour encoder la signature
		$hash = hash_hmac("SHA1",  strtolower($query), SECRETKEY, true);
		$base64encoded = base64_encode($hash);
		$signature = urlencode($base64encoded);

		// Construction de la requête finale sous la forme 'URL API + Requête API et paramètres + Signature'
		$query .= "&signature=" . $signature;

		// $jobs = null;
		
		// Initialisation du client API
		try
		{
			//Construction de la requête
			$httpRequest = new HttpRequest();
			$httpRequest->setMethod(HTTP_METH_POST);
			$httpRequest->setUrl(ENDPOINT . "?" . $query);
			
			// Envoi de la requête au serveur :
			$httpRequest->send();
			// Récupération du retour de l'API
			$response = $httpRequest->getResponseData();
			// retour de la réponse
			$json_response = json_decode($response['body'], true);
			
			apiErrorCheck($json_response);
		}
		catch (Exception $e)
		{
			echo "Probleme lors de l'envoi de la requête. ERREUR=".$e->getMessage();
			exit;
		}
	}
	
	//Fonction de vérification des jobs asynchrones
	function checkJobs($jobs)
	{
		$json_response = null;
		$error_msg = "";
		if(is_array($jobs) && count($jobs) > 0)
		{
			// La tâche est asynchrone, on doit donc régulièrement vérifier les tâches avec une sécurité
			$secu = 0;
			// On indexe les tâches
			$ij = 0;
			// Tant qu'il y a des tâches asynchrones non-terminées dans la pile de vérification, on boucle et on vérifie le statut
			while(count($jobs) > 0 && $secu < 100)
			{
				try
				{
					//On interroge le statut de la tâche asynchrone
					// http://download.cloud.com/releases/3.0.6/api_3.0.6/root_admin/queryAsyncJobResult.html
					$args['apikey'] = APIKEY;
					$args['command'] = "queryAsyncJobResult";
					$args['jobid'] = $jobs[$ij];
					$args['response'] = "json";
					
					$json_response = null;
					sendRequest($args, $json_response);
					
					if(is_array($json_response['queryasyncjobresultresponse']))
					{
						// Si OK...
						if($json_response['queryasyncjobresultresponse']['jobstatus'] == 1)
						{
							// ...On retire simplement la tâche du tableau a surveiller
							//return("JOB OK\n");
							array_splice($jobs, $ij, 1);
						}
						// Sinon...
						elseif($json_response['queryasyncjobresultresponse']['jobstatus'] == 2)
						{
							//...On mémorise l'erreur et on retire la tâche du tableau à surveiller
							//return("JOB ERREUR\n");
							array_splice($jobs, $ij, 1);
							$error_msg .= "ERREUR ! RESULT_CODE=".$json_response['queryasyncjobresultresponse']['jobresultcode'];
						}
						// Cette tâche est encore en cours, on passe à la suivante et on temporise
						elseif($json_response['queryasyncjobresultresponse']['jobstatus'] == 0)
						{
							// Tâche suivante
							$ij++;
							// Temporisation entre chaque interrogation pour ne pas charger inutilement l'API
							sleep(5);
						}
					}
				}
				catch(Exception $e)
				{
					$error_msg .= "EXCEPTION Lors de la verification la tache asynchrone. JOB_UUID:".$jobs[$ij]." ERREUR=".$e->getMessage()." \n";
				}

				// Si l'index arrive en bout de tableau, on le réinitialise
				if($ij == count($jobs))
				{
					$ij = 0;
					$secu++;
				}
			}

			if($error_msg)
			{
				echo "ERRORS:".$error_msg."\n";
				return false;
			}
			return true;
		}
		echo "No job\n";
		return false;
	}
?>

Prochainement "Comment créer une VM sous Debien avec un MySQL préinstallé"