<?php
/**
 * @category   Zend
 * @package    Zend_Cloud_Infrastructure
 * @subpackage Adapter
 * @copyright  Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 */

require_once 'Zend/Service/Rackspace/Servers.php';
require_once 'Zend/Cloud/Infrastructure/Instance.php';
require_once 'Zend/Cloud/Infrastructure/InstanceList.php';
require_once 'Zend/Cloud/Infrastructure/Image.php';
require_once 'Zend/Cloud/Infrastructure/ImageList.php';
require_once 'Zend/Cloud/Infrastructure/Adapter/AbstractAdapter.php';

/**
 * Rackspace servers adapter for infrastructure service
 *
 * @package    Zend_Cloud_Infrastructure
 * @subpackage Adapter
 * @copyright  Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 */
class Zend_Cloud_Infrastructure_Adapter_Rackspace extends Zend_Cloud_Infrastructure_Adapter_AbstractAdapter
{
    /**
     * RACKSPACE constants
     */
    const RACKSPACE_USER      = 'rackspace_user';
    const RACKSPACE_KEY       = 'rackspace_key';
    const RACKSPACE_REGION    = 'rackspace_region';
    const RACKSPACE_ZONE_USA  = 'USA';
    const RACKSPACE_ZONE_UK   = 'UK';
    const MONITOR_CPU_SAMPLES = 3;
    /**
     * Rackspace Servers Instance
     *
     * @var Zend_Service_Rackspace_Servers
     */
    protected $rackspace;
    /**
     * Rackspace access user
     *
     * @var string
     */
    protected $accessUser;

    /**
     * Rackspace access key
     *
     * @var string
     */
    protected $accessKey;
    /**
     * Rackspace Region
     *
     * @var string
     */
    protected $region;
    /**
     * Flavors
     *
     * @var array
     */
    protected $flavors;
    /**
     * Map array between Rackspace and Infrastructure status
     *
     * @var array
     */
    protected $mapStatus = [
        'ACTIVE'             => Zend_Cloud_Infrastructure_Instance::STATUS_RUNNING,
        'SUSPENDED'          => Zend_Cloud_Infrastructure_Instance::STATUS_STOPPED,
        'BUILD'              => Zend_Cloud_Infrastructure_Instance::STATUS_REBUILD,
        'REBUILD'            => Zend_Cloud_Infrastructure_Instance::STATUS_REBUILD,
        'QUEUE_RESIZE'       => Zend_Cloud_Infrastructure_Instance::STATUS_PENDING,
        'PREP_RESIZE'        => Zend_Cloud_Infrastructure_Instance::STATUS_PENDING,
        'RESIZE'             => Zend_Cloud_Infrastructure_Instance::STATUS_REBUILD,
        'VERIFY_RESIZE'      => Zend_Cloud_Infrastructure_Instance::STATUS_REBUILD,
        'PASSWORD'           => Zend_Cloud_Infrastructure_Instance::STATUS_PENDING,
        'RESCUE'             => Zend_Cloud_Infrastructure_Instance::STATUS_PENDING,
        'REBOOT'             => Zend_Cloud_Infrastructure_Instance::STATUS_REBOOTING,
        'HARD_REBOOT'        => Zend_Cloud_Infrastructure_Instance::STATUS_REBOOTING,
        'SHARE_IP'           => Zend_Cloud_Infrastructure_Instance::STATUS_PENDING,
        'SHARE_IP_NO_CONFIG' => Zend_Cloud_Infrastructure_Instance::STATUS_PENDING,
        'DELETE_IP'          => Zend_Cloud_Infrastructure_Instance::STATUS_PENDING,
        'UNKNOWN'            => Zend_Cloud_Infrastructure_Instance::STATUS_PENDING
    ];
    /**
     * Constructor
     *
     * @param  array|Zend_Config $options
     * @return void
     */
    public function __construct($options = [])
    {
        if (is_object($options)) {
            if (method_exists($options, 'toArray')) {
                $options= $options->toArray();
            } elseif ($options instanceof Traversable) {
                $options = iterator_to_array($options);
            }
        }

        if (empty($options) || !is_array($options)) {
            require_once 'Zend/Cloud/Infrastructure/Exception.php';
            throw new Zend_Cloud_Infrastructure_Exception('Invalid options provided');
        }

        if (!isset($options[self::RACKSPACE_USER])) {
            require_once 'Zend/Cloud/Infrastructure/Exception.php';
            throw new Zend_Cloud_Infrastructure_Exception('Rackspace access user not specified!');
        }

        if (!isset($options[self::RACKSPACE_KEY])) {
            require_once 'Zend/Cloud/Infrastructure/Exception.php';
            throw new Zend_Cloud_Infrastructure_Exception('Rackspace access key not specified!');
        }

        $this->accessUser = $options[self::RACKSPACE_USER];
        $this->accessKey  = $options[self::RACKSPACE_KEY];

        if (isset($options[self::RACKSPACE_REGION])) {
            switch ($options[self::RACKSPACE_REGION]) {
                case self::RACKSPACE_ZONE_UK:
                    $this->region= Zend_Service_Rackspace_Servers::UK_AUTH_URL;
                    break;
                case self::RACKSPACE_ZONE_USA:
                    $this->region = Zend_Service_Rackspace_Servers::US_AUTH_URL;
                    break;
                default:
                    require_once 'Zend/Cloud/Infrastructure/Exception.php';
                    throw new Zend_Cloud_Infrastructure_Exception('The region is not valid');
            }
        } else {
            $this->region = Zend_Service_Rackspace_Servers::US_AUTH_URL;
        }

        try {
            $this->rackspace = new Zend_Service_Rackspace_Servers($this->accessUser,$this->accessKey, $this->region);
        } catch (Exception  $e) {
            require_once 'Zend/Cloud/Infrastructure/Exception.php';
            throw new Zend_Cloud_Infrastructure_Exception('Error on create: ' . $e->getMessage(), $e->getCode(), $e);
        }

        if (isset($options[self::HTTP_ADAPTER])) {
            $this->rackspace->getHttpClient()->setAdapter($options[self::HTTP_ADAPTER]);
        }

    }
    /**
     * Convert the attributes of Rackspace server into attributes of Infrastructure
     *
     * @param  array $attr
     * @return array
     */
    protected function convertAttributes($attr)
    {
        $result = [];
        if (!empty($attr) && is_array($attr)) {
            $result[Zend_Cloud_Infrastructure_Instance::INSTANCE_ID]      = $attr['id'];
            $result[Zend_Cloud_Infrastructure_Instance::INSTANCE_NAME]    = $attr['name'];
            $result[Zend_Cloud_Infrastructure_Instance::INSTANCE_STATUS]  = $this->mapStatus[$attr['status']];
            $result[Zend_Cloud_Infrastructure_Instance::INSTANCE_IMAGEID] = $attr['imageId'];
            if ($this->region==Zend_Service_Rackspace_Servers::US_AUTH_URL) {
                $result[Zend_Cloud_Infrastructure_Instance::INSTANCE_ZONE] = self::RACKSPACE_ZONE_USA;
            } else {
                $result[Zend_Cloud_Infrastructure_Instance::INSTANCE_ZONE] = self::RACKSPACE_ZONE_UK;
            }
            $result[Zend_Cloud_Infrastructure_Instance::INSTANCE_RAM]     = $this->flavors[$attr['flavorId']]['ram'];
            $result[Zend_Cloud_Infrastructure_Instance::INSTANCE_STORAGE] = $this->flavors[$attr['flavorId']]['disk'];
        }
        return $result;
    }
    /**
     * Return a list of the available instancies
     *
     * @return false|Zend_Cloud_Infrastructure_InstanceList
     */
    public function listInstances()
    {
        $this->adapterResult = $this->rackspace->listServers(true);
        if ($this->adapterResult===false) {
            return false;
        }
        $array= $this->adapterResult->toArray();
        $result = [];
        foreach ($array as $instance) {
            $result[]= $this->convertAttributes($instance);
        }
        return new Zend_Cloud_Infrastructure_InstanceList($this, $result);
    }
    /**
     * Return the status of an instance
     *
     * @param  string $id
     * @return string|boolean
     */
    public function statusInstance($id)
    {
        $this->adapterResult = $this->rackspace->getServer($id);
        if ($this->adapterResult===false) {
            return false;
        }
        $array= $this->adapterResult->toArray();
        return $this->mapStatus[$array['status']];
    }
    /**
     * Return the public DNS name/Ip address of the instance
     *
     * @param  string $id
     * @return string|boolean
     */
    public function publicDnsInstance($id)
    {
        $this->adapterResult = $this->rackspace->getServerPublicIp($id);
        if (empty($this->adapterResult)) {
            return false;
        }
        return $this->adapterResult[0];
    }
    /**
     * Reboot an instance
     *
     * @param string $id
     * @return boolean
     */
    public function rebootInstance($id)
    {
        return $this->rackspace->rebootServer($id,true);
    }
    /**
     * Create a new instance
     *
     * @param string $name
     * @param array $options
     * @return false|Zend_Cloud_Infrastructure_Instance
     */
    public function createInstance($name, $options)
    {
        if (empty($name)) {
            require_once 'Zend/Cloud/Infrastructure/Exception.php';
            throw new Zend_Cloud_Infrastructure_Exception('You must specify the name of the instance');
        }
        if (empty($options) || !is_array($options)) {
            require_once 'Zend/Cloud/Infrastructure/Exception.php';
            throw new Zend_Cloud_Infrastructure_Exception('The options must be an array');
        }
        // @todo create an generic abstract definition for an instance?
        $metadata= [];
        if (isset($options['metadata'])) {
            $metadata= $options['metadata'];
            unset($options['metadata']);
        }
        $files= [];
        if (isset($options['files'])) {
            $files= $options['files'];
            unset($options['files']);
        }
        $options['name']= $name;
        $this->adapterResult = $this->rackspace->createServer($options,$metadata,$files);
        if ($this->adapterResult===false) {
            return false;
        }
        return new Zend_Cloud_Infrastructure_Instance($this, $this->convertAttributes($this->adapterResult->toArray()));
    }
    /**
     * Stop an instance
     *
     * @param  string $id
     * @return boolean
     */
    public function stopInstance($id)
    {
        require_once 'Zend/Cloud/Infrastructure/Exception.php';
        throw new Zend_Cloud_Infrastructure_Exception('The stopInstance method is not implemented in the adapter');
    }

    /**
     * Start an instance
     *
     * @param  string $id
     * @return boolean
     */
    public function startInstance($id)
    {
        require_once 'Zend/Cloud/Infrastructure/Exception.php';
        throw new Zend_Cloud_Infrastructure_Exception('The startInstance method is not implemented in the adapter');
    }

    /**
     * Destroy an instance
     *
     * @param  string $id
     * @return boolean
     */
    public function destroyInstance($id)
    {
        $this->adapterResult= $this->rackspace->deleteServer($id);
        return $this->adapterResult;
    }
    /**
     * Return a list of all the available instance images
     *
     * @return false|Zend_Cloud_Infrastructure_ImageList
     */
    public function imagesInstance()
    {
        $this->adapterResult = $this->rackspace->listImages(true);
        if ($this->adapterResult===false) {
            return false;
        }

        $images= $this->adapterResult->toArray();
        $result= [];

        foreach ($images as $image) {
            if (strtolower($image['status'])==='active') {
                if (strpos($image['name'],'Windows')!==false) {
                    $platform = Zend_Cloud_Infrastructure_Image::IMAGE_WINDOWS;
                } else {
                    $platform = Zend_Cloud_Infrastructure_Image::IMAGE_LINUX;
                }
                if (strpos($image['name'],'x64')!==false) {
                    $arch = Zend_Cloud_Infrastructure_Image::ARCH_64BIT;
                } else {
                    $arch = Zend_Cloud_Infrastructure_Image::ARCH_32BIT;
                }
                $result[]= [
                    Zend_Cloud_Infrastructure_Image::IMAGE_ID           => $image['id'],
                    Zend_Cloud_Infrastructure_Image::IMAGE_NAME         => $image['name'],
                    Zend_Cloud_Infrastructure_Image::IMAGE_DESCRIPTION  => $image['name'],
                    Zend_Cloud_Infrastructure_Image::IMAGE_ARCHITECTURE => $arch,
                    Zend_Cloud_Infrastructure_Image::IMAGE_PLATFORM     => $platform,
                ];
            }
        }
        return new Zend_Cloud_Infrastructure_ImageList($result,$this->adapterResult);
    }
    /**
     * Return all the available zones
     *
     * @return array
     */
    public function zonesInstance()
    {
        return [self::RACKSPACE_ZONE_USA,self::RACKSPACE_ZONE_UK];
    }
    /**
     * Return the system information about the $metric of an instance
     * NOTE: it works only for Linux servers
     *
     * @param  string $id
     * @param  string $metric
     * @param  null|array $options
     * @return array|boolean
     */
    public function monitorInstance($id, $metric, $options = null)
    {
        if (!function_exists("ssh2_connect")) {
            require_once 'Zend/Cloud/Infrastructure/Exception.php';
            throw new Zend_Cloud_Infrastructure_Exception('Monitor requires the PHP "SSH" extension (ext/ssh2)');
        }
        if (empty($id)) {
            require_once 'Zend/Cloud/Infrastructure/Exception.php';
            throw new Zend_Cloud_Infrastructure_Exception('You must specify the id of the instance to monitor');
        }
        if (empty($metric)) {
            require_once 'Zend/Cloud/Infrastructure/Exception.php';
            throw new Zend_Cloud_Infrastructure_Exception('You must specify the metric to monitor');
        }
        if (!in_array($metric,$this->validMetrics)) {
            require_once 'Zend/Cloud/Infrastructure/Exception.php';
            throw new Zend_Cloud_Infrastructure_Exception(sprintf('The metric "%s" is not valid', $metric));
        }
        if (!empty($options) && !is_array($options)) {
            require_once 'Zend/Cloud/Infrastructure/Exception.php';
            throw new Zend_Cloud_Infrastructure_Exception('The options must be an array');
        }

        switch ($metric) {
            case Zend_Cloud_Infrastructure_Instance::MONITOR_CPU:
                $cmd= 'top -b -n '.self::MONITOR_CPU_SAMPLES.' | grep \'Cpu\'';
                break;
            case Zend_Cloud_Infrastructure_Instance::MONITOR_RAM:
                $cmd= 'top -b -n 1 | grep \'Mem\'';
                break;
            case Zend_Cloud_Infrastructure_Instance::MONITOR_DISK:
                $cmd= 'df --total | grep total';
                break;
        }
        if (empty($cmd)) {
            require_once 'Zend/Cloud/Infrastructure/Exception.php';
            throw new Zend_Cloud_Infrastructure_Exception('The metric specified is not supported by the adapter');
        }

        $params= [
            Zend_Cloud_Infrastructure_Instance::SSH_USERNAME => $options['username'],
            Zend_Cloud_Infrastructure_Instance::SSH_PASSWORD => $options['password']
        ];
        $exec_time= time();
        $result= $this->deployInstance($id,$params,$cmd);

        if (empty($result)) {
            return false;
        }

        $monitor = [];
        $num     = 0;
        $average = 0;

        $outputs= explode("\n",$result);
        foreach ($outputs as $output) {
            if (!empty($output)) {
                switch ($metric) {
                    case Zend_Cloud_Infrastructure_Instance::MONITOR_CPU:
                        if (preg_match('/(\d+\.\d)%us/', $output,$match)) {
                            $usage = (float) $match[1];
                        }
                        break;
                    case Zend_Cloud_Infrastructure_Instance::MONITOR_RAM:
                        if (preg_match('/(\d+)k total/', $output,$match)) {
                            $total = (integer) $match[1];
                        }
                        if (preg_match('/(\d+)k used/', $output,$match)) {
                            $used = (integer) $match[1];
                        }
                        if ($total>0) {
                            $usage= (float) $used/$total;
                        }
                        break;
                    case Zend_Cloud_Infrastructure_Instance::MONITOR_DISK:
                        if (preg_match('/(\d+)%/', $output,$match)) {
                            $usage = (float) $match[1];
                        }
                        break;
                }

                $monitor['series'][] = [
                    'timestamp' => $exec_time,
                    'value'     => number_format($usage,2).'%'
                ];

                $average += $usage;
                $exec_time+= 60; // seconds
                $num++;
            }
        }

        if ($num>0) {
            $monitor['average'] = number_format($average/$num,2).'%';
        }
        return $monitor;
    }
    /**
     * Get the adapter
     *
     * @return Zend_Service_Rackspace_Servers
     */
    public function getAdapter()
    {
        return $this->rackspace;
    }
    /**
     * Get last HTTP request
     *
     * @return string
     */
    public function getLastHttpRequest()
    {
        return $this->rackspace->getHttpClient()->getLastRequest();
    }
    /**
     * Get the last HTTP response
     *
     * @return Zend_Http_Response
     */
    public function getLastHttpResponse()
    {
        return $this->rackspace->getHttpClient()->getLastResponse();
    }
}
