      1: <?php
      2: /**
      3:  * Copyright 2012-2014 Rackspace US, Inc.
      4:  *
      5:  * Licensed under the Apache License, Version 2.0 (the "License");
      6:  * you may not use this file except in compliance with the License.
      7:  * You may obtain a copy of the License at
      8:  *
      9:  * http://www.apache.org/licenses/LICENSE-2.0
     10:  *
     11:  * Unless required by applicable law or agreed to in writing, software
     12:  * distributed under the License is distributed on an "AS IS" BASIS,
     13:  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     14:  * See the License for the specific language governing permissions and
     15:  * limitations under the License.
     16:  */
     17:
     18: namespace OpenCloud\LoadBalancer\Resource;
     19:
     20: use OpenCloud\Common\Exceptions;
     21: use OpenCloud\Common\Resource\PersistentResource;
     22: use OpenCloud\DNS\Resource\HasPtrRecordsInterface;
     23: use OpenCloud\LoadBalancer\Enum\NodeCondition;
     24: use OpenCloud\LoadBalancer\Enum\IpType;
     25: use OpenCloud\LoadBalancer\Enum\NodeType;
     26:
     27: /**
     28:  * A load balancer is a logical device which belongs to a cloud account. It is
     29:  * used to distribute workloads between multiple back-end systems or services,
     30:  * based on the criteria defined as part of its configuration.
     31:  */
     32: class LoadBalancer extends PersistentResource implements HasPtrRecordsInterface
     33: {
     34:     public $id;
     35:
     36:     /**
     37:      * Name of the load balancer to create. The name must be 128 characters or
     38:      * less in length, and all UTF-8 characters are valid.
     39:      *
     40:      * @var string
     41:      */
     42:     public $name;
     43:
     44:     /**
     45:      * Port of the service which is being load balanced.
     46:      *
     47:      * @var string
     48:      */
     49:     public $port;
     50:
     51:     /**
     52:      * Protocol of the service which is being load balanced.
     53:      *
     54:      * @var string
     55:      */
     56:     public $protocol;
     57:
     58:     /**
     59:      * Type of virtual IP to add along with the creation of a load balancer.
     60:      *
     61:      * @var array|Collection
     62:      */
     63:     public $virtualIps = array();
     64:
     65:     /**
     66:      * Nodes to be added to the load balancer.
     67:      *
     68:      * @var array|Collection
     69:      */
     70:     public $nodes = array();
     71:
     72:     /**
     73:      * The access list management feature allows fine-grained network access
     74:      * controls to be applied to the load balancer's virtual IP address.
     75:      *
     76:      * @var Collection
     77:      */
     78:     public $accessList;
     79:
     80:     /**
     81:      * Algorithm that defines how traffic should be directed between back-end nodes.
     82:      *
     83:      * @var Algorithm
     84:      */
     85:     public $algorithm;
     86:
     87:     /**
     88:      * Current connection logging configuration.
     89:      *
     90:      * @var ConnectionLogging
     91:      */
     92:     public $connectionLogging;
     93:
     94:     /**
     95:      * Specifies limits on the number of connections per IP address to help
     96:      * mitigate malicious or abusive traffic to your applications.
     97:      *
     98:      * @var ConnectionThrottle
     99:      */
    100:     public $connectionThrottle;
    101:
    102:     /**
    103:      * The type of health monitor check to perform to ensure that the service is
    104:      * performing properly.
    105:      *
    106:      * @var HealthMonitor
    107:      */
    108:     public $healthMonitor;
    109:
    110:     /**
    111:      * Forces multiple requests, of the same protocol, from clients to be
    112:      * directed to the same node.
    113:      *
    114:      * @var SessionPersistance
    115:      */
    116:     public $sessionPersistence;
    117:
    118:     /**
    119:      * Information (metadata) that can be associated with each load balancer for
    120:      * the client's personal use.
    121:      *
    122:      * @var array|Metadata
    123:      */
    124:     public $metadata = array();
    125:
    126:     /**
    127:      * The timeout value for the load balancer and communications with its nodes.
    128:      * Defaults to 30 seconds with a maximum of 120 seconds.
    129:      *
    130:      * @var int
    131:      */
    132:     public $timeout;
    133:
    134:     public $created;
    135:     public $updated;
    136:     public $status;
    137:     public $nodeCount;
    138:     public $sourceAddresses;
    139:     public $cluster;
    140:
    141:     protected static $json_name = 'loadBalancer';
    142:     protected static $url_resource = 'loadbalancers';
    143:
    144:     protected $associatedResources = array(
    145:         'node'               => 'Node',
    146:         'virtualIp'          => 'VirtualIp',
    147:         'connectionLogging'  => 'ConnectionLogging',
    148:         'healthMonitor'      => 'HealthMonitor',
    149:         'sessionPersistance' => 'SessionPersistance'
    150:     );
    151:
    152:     protected $associatedCollections = array(
    153:         'nodes'      => 'Node',
    154:         'virtualIps' => 'VirtualIp',
    155:         'accessList' => 'Access'
    156:     );
    157:
    158:     protected $createKeys = array(
    159:         'name',
    160:         'port',
    161:         'protocol',
    162:         'virtualIps',
    163:         'nodes',
    164:         'accessList',
    165:         'algorithm',
    166:         'connectionLogging',
    167:         'connectionThrottle',
    168:         'healthMonitor',
    169:         'sessionPersistence'
    170:     );
    171:
    172:     /**
    173:      * This method creates a Node object and adds it to a list of Nodes
    174:      * to be added to the LoadBalancer. This method will not add the nodes
    175:      * directly to the load balancer itself; it stores them in an array and
    176:      * the nodes are added later, in one of two ways:
    177:      *
    178:      * * for a new load balancer, the nodes are added as part of the create() method call
    179:      * * for an existing load balancer, you must call the addNodes() method
    180:      *
    181:      * @param string  $address   the IP address of the node
    182:      * @param integer $port      the port # of the node
    183:      * @param boolean $condition the initial condition of the node
    184:      * @param string  $type      either PRIMARY or SECONDARY
    185:      * @param integer $weight    the node weight (for round-robin)
    186:      *
    187:      * @throws \InvalidArgumentException
    188:      * @return void
    189:      */
    190:     public function addNode(
    191:         $address,
    192:         $port,
    193:         $condition = NodeCondition::ENABLED,
    194:         $type = null,
    195:         $weight = null
    196:     ) {
    197:         $allowedConditions = array(
    198:             NodeCondition::ENABLED,
    199:             NodeCondition::DISABLED,
    200:             NodeCondition::DRAINING
    201:         );
    202:
    203:         if (!in_array($condition, $allowedConditions)) {
    204:             throw new \InvalidArgumentException(sprintf(
    205:                 "Invalid condition. It must one of the following: %s",
    206:                 implode(', ', $allowedConditions)
    207:             ));
    208:         }
    209:
    210:         $allowedTypes = array(NodeType::PRIMARY, NodeType::SECONDARY);
    211:         if ($type && !in_array($type, $allowedTypes)) {
    212:             throw new \InvalidArgumentException(sprintf(
    213:                 "Invalid type. It must one of the following: %s",
    214:                 implode(', ', $allowedTypes)
    215:             ));
    216:         }
    217:
    218:         if ($weight && !is_numeric($weight)) {
    219:             throw new \InvalidArgumentException('Invalid weight. You must supply a numeric type');
    220:         }
    221:
    222:         // queue it
    223:         $this->nodes[] = $this->node(array(
    224:             'address'   => $address,
    225:             'port'      => $port,
    226:             'condition' => $condition,
    227:             'type'      => $type,
    228:             'weight'    => $weight
    229:         ));
    230:     }
    231:
    232:     /**
    233:      * Creates currently added nodes by sending them to the API
    234:      *
    235:      * @return array of {@see \Guzzle\Http\Message\Response} objects
    236:      * @throws \OpenCloud\Common\Exceptions\MissingValueError
    237:      */
    238:     public function addNodes()
    239:     {
    240:         if (empty($this->nodes)) {
    241:             throw new Exceptions\MissingValueError(
    242:                 'Cannot add nodes; no nodes are defined'
    243:             );
    244:         }
    245:
    246:         $requests = array();
    247:
    248:         foreach ($this->nodes as $node) {
    249:             $requests[] = $this->getClient()->post($node->getUrl(), self::getJsonHeader(), $node->createJson());
    250:         }
    251:
    252:         return $this->getClient()->send($requests);
    253:     }
    254:
    255:     /**
    256:      * Remove a node from this load-balancer
    257:      *
    258:      * @param int $id id of the node
    259:      * @return \Guzzle\Http\Message\Response
    260:      */
    261:     public function removeNode($nodeId)
    262:     {
    263:         return $this->node($nodeId)->delete();
    264:     }
    265:
    266:     /**
    267:      * Adds a virtual IP to the load balancer. You can use the strings 'PUBLIC'
    268:      * or 'SERVICENET' to indicate the public or internal networks, or you can
    269:      * pass the `Id` of an existing IP address.
    270:      *
    271:      * @param string  $id        either 'public' or 'servicenet' or an ID of an
    272:      *                           existing IP address
    273:      * @param integer $ipVersion either null, 4, or 6 (both, IPv4, or IPv6)
    274:      * @return void
    275:      */
    276:     public function addVirtualIp($type = IpType::PUBLIC_TYPE, $ipVersion = null)
    277:     {
    278:         $object = new \stdClass();
    279:
    280:         switch (strtoupper($type)) {
    281:             case IpType::PUBLIC_TYPE:
    282:             case IpType::SERVICENET_TYPE:
    283:                 $object->type = strtoupper($type);
    284:                 break;
    285:             default:
    286:                 $object->id = $type;
    287:                 break;
    288:         }
    289:
    290:         if ($ipVersion) {
    291:             switch ($ipVersion) {
    292:                 case 4:
    293:                     $object->ipVersion = IpType::IPv4;
    294:                     break;
    295:                 case 6:
    296:                     $object->ipVersion = IpType::IPv6;
    297:                     break;
    298:                 default:
    299:                     throw new Exceptions\DomainError(sprintf(
    300:                         'Value [%s] for ipVersion is not valid',
    301:                         $ipVersion
    302:                     ));
    303:             }
    304:         }
    305:
    306:         /**
    307:          * If the load balancer exists, we want to add it immediately.
    308:          * If not, we add it to the virtualIps list and add it when the load
    309:          * balancer is created.
    310:          */
    311:         if ($this->Id()) {
    312:             $virtualIp = $this->virtualIp();
    313:             $virtualIp->type = $type;
    314:             $virtualIp->ipVersion = $object->ipVersion;
    315:             return $virtualIp->create();
    316:         } else {
    317:             // queue it
    318:             $this->virtualIps[] = $object;
    319:         }
    320:
    321:         return true;
    322:     }
    323:
    324:     /**
    325:      * Returns a Node
    326:      *
    327:      * @return \OpenCloud\LoadBalancer\Resource\Node
    328:      */
    329:     public function node($id = null)
    330:     {
    331:         return $this->getService()->resource('Node', $id, $this);
    332:     }
    333:
    334:     /**
    335:      * returns a Collection of Nodes
    336:      *
    337:      * @return \OpenCloud\Common\Collection\PaginatedIterator
    338:      */
    339:     public function nodeList()
    340:     {
    341:         return $this->getService()->resourceList('Node', null, $this);
    342:     }
    343:
    344:     /**
    345:      * Returns a NodeEvent object
    346:      *
    347:      * @return \OpenCloud\LoadBalancer\Resource\NodeEvent
    348:      */
    349:     public function nodeEvent()
    350:     {
    351:         return $this->getService()->resource('NodeEvent', null, $this);
    352:     }
    353:
    354:     /**
    355:      * Returns a Collection of NodeEvents
    356:      *
    357:      * @return \OpenCloud\Common\Collection\PaginatedIterator
    358:      */
    359:     public function nodeEventList()
    360:     {
    361:         return $this->getService()->resourceList('NodeEvent', null, $this);
    362:     }
    363:
    364:     /**
    365:      * Returns a single Virtual IP (not called publicly)
    366:      *
    367:      * @return \OpenCloud\LoadBalancer\Resource\VirtualIp
    368:      */
    369:     public function virtualIp($data = null)
    370:     {
    371:         return $this->getService()->resource('VirtualIp', $data, $this);
    372:     }
    373:
    374:     /**
    375:      * @return \OpenCloud\Common\Collection\PaginatedIterator
    376:      */
    377:     public function virtualIpList()
    378:     {
    379:         return $this->getService()->resourceList('VirtualIp', null, $this);
    380:     }
    381:
    382:     /**
    383:      * Return the session persistence resource
    384:      *
    385:      * @return \OpenCloud\LoadBalancer\Resource\SessionPersistence
    386:      */
    387:     public function sessionPersistence()
    388:     {
    389:         return $this->getService()->resource('SessionPersistence', null, $this);
    390:     }
    391:
    392:     /**
    393:      * Returns the load balancer's error page object
    394:      *
    395:      * @return \OpenCloud\LoadBalancer\Resource\ErrorPage
    396:      */
    397:     public function errorPage()
    398:     {
    399:         return $this->getService()->resource('ErrorPage', null, $this);
    400:     }
    401:
    402:     /**
    403:      * Returns the load balancer's health monitor object
    404:      *
    405:      * @return \OpenCloud\LoadBalancer\Resource\HealthMonitor
    406:      */
    407:     public function healthMonitor()
    408:     {
    409:         return $this->getService()->resource('HealthMonitor', null, $this);
    410:     }
    411:
    412:     /**
    413:      * Returns statistics on the load balancer operation
    414:      *
    415:      * @return \OpenCloud\LoadBalancer\Resource\Stats
    416:      */
    417:     public function stats()
    418:     {
    419:         return $this->getService()->resource('Stats', null, $this);
    420:     }
    421:
    422:     /**
    423:      * @return \OpenCloud\Common\Collection\PaginatedIterator
    424:      */
    425:     public function usage()
    426:     {
    427:         return $this->getService()->resourceList('UsageRecord', null, $this);
    428:     }
    429:
    430:     /**
    431:      * Return an access resource
    432:      *
    433:      * @return \OpenCloud\LoadBalancer\Resource\Access
    434:      */
    435:     public function access($data = null)
    436:     {
    437:         return $this->getService()->resource('Access', $data, $this);
    438:     }
    439:
    440:     /**
    441:      * Creates an access list. You must provide an array of \stdClass objects,
    442:      * each of which contains `type' and `address' properties. Valid types for
    443:      * the former are: "DENY" or "ALLOW". The address must be a valid IP
    444:      * address, either v4 or v6.
    445:      *
    446:      * @param stdClass[] $list
    447:      *
    448:      * @return \Guzzle\Http\Message\Response
    449:      */
    450:     public function createAccessList(array $list)
    451:     {
    452:         $url = $this->getUrl();
    453:         $url->addPath('accesslist');
    454:
    455:         $json = json_encode($list);
    456:         $this->checkJsonError();
    457:
    458:         return $this->getClient()->post($url, self::getJsonHeader(), $json)->send();
    459:     }
    460:
    461:     /**
    462:      * @return \OpenCloud\Common\Collection\PaginatedIterator
    463:      */
    464:     public function accessList()
    465:     {
    466:         return $this->getService()->resourceList('Access', null, $this);
    467:     }
    468:
    469:     /**
    470:      * Return a connection throttle resource
    471:      *
    472:      * @return \OpenCloud\LoadBalancer\Resource\ConnectionThrottle
    473:      */
    474:     public function connectionThrottle()
    475:     {
    476:         return $this->getService()->resource('ConnectionThrottle', null, $this);
    477:     }
    478:
    479:     /**
    480:      * Find out whether connection logging is enabled for this load balancer
    481:      *
    482:      * @return bool Returns TRUE if enabled, FALSE if not
    483:      */
    484:     public function hasConnectionLogging()
    485:     {
    486:         $url = clone $this->getUrl();
    487:         $url->addPath('connectionlogging');
    488:
    489:         $response = $this->getClient()->get($url)->send()->json();
    490:
    491:         return isset($response['connectionLogging']['enabled'])
    492:             && $response['connectionLogging']['enabled'] === true;
    493:     }
    494:
    495:     /**
    496:      * Set the connection logging setting for this load balancer
    497:      *
    498:      * @param $bool  Set to TRUE to enable, FALSE to disable
    499:      * @return \Guzzle\Http\Message\Response
    500:      */
    501:     public function enableConnectionLogging($bool)
    502:     {
    503:         $url = clone $this->getUrl();
    504:         $url->addPath('connectionlogging');
    505:
    506:         $body = array('connectionLogging' => (bool) $bool);
    507:
    508:         return $this->getClient()->put($url, self::getJsonHeader(), $body)->send();
    509:     }
    510:
    511:     /**
    512:      * @deprecated
    513:      */
    514:     public function connectionLogging()
    515:     {
    516:         $this->getLogger()->deprecated(__METHOD__, 'hasConnectionLogging or enableConnectionLogging');
    517:     }
    518:
    519:     /**
    520:      * Find out whether content caching is enabled for this load balancer
    521:      *
    522:      * @return bool Returns TRUE if enabled, FALSE if not
    523:      */
    524:     public function hasContentCaching()
    525:     {
    526:         $url = clone $this->getUrl();
    527:         $url->addPath('contentcaching');
    528:
    529:         $response = $this->getClient()->get($url)->send()->json();
    530:
    531:         return isset($response['contentCaching']['enabled'])
    532:             && $response['contentCaching']['enabled'] === true;
    533:     }
    534:
    535:     /**
    536:      * Set the content caching setting for this load balancer
    537:      *
    538:      * @param $bool  Set to TRUE to enable, FALSE to disable
    539:      * @return \Guzzle\Http\Message\Response
    540:      */
    541:     public function enableContentCaching($bool)
    542:     {
    543:         $url = clone $this->getUrl();
    544:         $url->addPath('contentcaching');
    545:
    546:         $body = array('contentCaching' => array('enabled' => (bool) $bool));
    547:         $body = json_encode($body);
    548:         $this->checkJsonError();
    549:
    550:         return $this->getClient()->put($url, self::getJsonHeader(), $body)->send();
    551:     }
    552:
    553:     /**
    554:      * @deprecated
    555:      */
    556:     public function contentCaching()
    557:     {
    558:         $this->getLogger()->deprecated(__METHOD__, 'hasContentCaching or setContentCaching');
    559:     }
    560:
    561:     /**
    562:      * Return a SSL Termination resource
    563:      *
    564:      * @return \OpenCloud\LoadBalancer\Resource\SSLTermination
    565:      */
    566:     public function SSLTermination()
    567:     {
    568:         return $this->getService()->resource('SSLTermination', null, $this);
    569:     }
    570:
    571:     /**
    572:      * Return a metadata item
    573:      *
    574:      * @return \OpenCloud\LoadBalancer\Resource\Metadata
    575:      */
    576:     public function metadata($data = null)
    577:     {
    578:         return $this->getService()->resource('Metadata', $data, $this);
    579:     }
    580:
    581:     /**
    582:      * Return a collection of metadata items
    583:      *
    584:      * @return \OpenCloud\Common\Collection\PaginatedIterator
    585:      */
    586:     public function metadataList()
    587:     {
    588:         return $this->getService()->resourceList('Metadata', null, $this);
    589:     }
    590:
    591:     protected function createJson()
    592:     {
    593:         $element = (object) array();
    594:
    595:         foreach ($this->createKeys as $key) {
    596:             if ($key == 'nodes') {
    597:                 foreach ($this->nodes as $node) {
    598:                     $nodeObject = (object) array();
    599:                     foreach ($node->createKeys as $key) {
    600:                         if (!empty($node->$key)) {
    601:                             $nodeObject->$key = $node->$key;
    602:                         }
    603:                     }
    604:                     $element->nodes[] = (object) $nodeObject;
    605:                 }
    606:             } elseif ($key == 'virtualIps') {
    607:                 foreach ($this->virtualIps as $virtualIp) {
    608:                     $element->virtualIps[] = $virtualIp;
    609:                 }
    610:             } elseif (isset($this->$key)) {
    611:                 $element->$key = $this->$key;
    612:             }
    613:         }
    614:
    615:         $object = (object) array($this->jsonName() => $element);
    616:
    617:         return $object;
    618:     }
    619:
    620:     protected function updateJson($params = array())
    621:     {
    622:         $updatableFields = array('name', 'algorithm', 'protocol', 'port', 'timeout', 'halfClosed');
    623:
    624:         $fields = array_keys($params);
    625:         foreach ($fields as $field) {
    626:             if (!in_array($field, $updatableFields)) {
    627:                 throw new Exceptions\InvalidArgumentError("You cannot update $field.");
    628:             }
    629:         }
    630:
    631:         $object = new \stdClass();
    632:         $object->loadBalancer = new \stdClass();
    633:         foreach ($params as $name => $value) {
    634:             $object->loadBalancer->$name = $this->$name;
    635:         }
    636:
    637:         return $object;
    638:     }
    639: }
    640: