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