Nodes now have a `created_at` attribute

* Base Node object has `created_at` which indicates the `datetime` the
  node was launched/started/created.
* EC2, Digital Ocean, OpenStack fill this attribute.
* Nodes at drivers that do not (yet) support it have `NoneType` as date.
* Document changes.

closes #698

[GITHUB-698]

Signed-off-by: Allard Hoeve <allardhoeve@gmail.com>
diff --git a/CHANGES.rst b/CHANGES.rst
index a5d882e..3168cce 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -51,6 +51,11 @@
   (GITHUB-697)
   [Rick van de Loo]
 
+- Added `Node.create_at` which, on supported drivers, contains the datetime the
+  node was first started.
+  (GITHUB-698)
+  [Allard Hoeve]
+
 Storage
 ~~~~~~~
 
diff --git a/libcloud/compute/base.py b/libcloud/compute/base.py
index 2b9b18a..b64dd31 100644
--- a/libcloud/compute/base.py
+++ b/libcloud/compute/base.py
@@ -167,7 +167,7 @@
     """
 
     def __init__(self, id, name, state, public_ips, private_ips,
-                 driver, size=None, image=None, extra=None):
+                 driver, size=None, image=None, extra=None, created_at=None):
         """
         :param id: Node ID.
         :type id: ``str``
@@ -193,6 +193,9 @@
         :param image: Image of this node. (optional)
         :type size: :class:`.NodeImage`
 
+        :param created_at: The datetime this node was created (optional)
+        :type created_at: :class: `datetime.datetime`
+
         :param extra: Optional provider specific attributes associated with
                       this node.
         :type extra: ``dict``
@@ -205,6 +208,7 @@
         self.private_ips = private_ips if private_ips else []
         self.driver = driver
         self.size = size
+        self.created_at = created_at
         self.image = image
         self.extra = extra or {}
         UuidMixin.__init__(self)
diff --git a/libcloud/compute/drivers/digitalocean.py b/libcloud/compute/drivers/digitalocean.py
index d0b3d7b..a390352 100644
--- a/libcloud/compute/drivers/digitalocean.py
+++ b/libcloud/compute/drivers/digitalocean.py
@@ -18,6 +18,7 @@
 import json
 import warnings
 
+from libcloud.utils.iso8601 import parse_date
 from libcloud.utils.py3 import httplib
 
 from libcloud.common.digitalocean import DigitalOcean_v1_BaseDriver
@@ -551,6 +552,7 @@
         else:
             state = NodeState.UNKNOWN
 
+        created = parse_date(data['created_at'])
         networks = data['networks']
         private_ips = []
         public_ips = []
@@ -568,7 +570,7 @@
 
         node = Node(id=data['id'], name=data['name'], state=state,
                     public_ips=public_ips, private_ips=private_ips,
-                    driver=self, extra=extra)
+                    created_at=created, driver=self, extra=extra)
         return node
 
     def _to_image(self, data):
diff --git a/libcloud/compute/drivers/ec2.py b/libcloud/compute/drivers/ec2.py
index 1df3efd..8367a14 100644
--- a/libcloud/compute/drivers/ec2.py
+++ b/libcloud/compute/drivers/ec2.py
@@ -5390,6 +5390,8 @@
         except KeyError:
             state = NodeState.UNKNOWN
 
+        created = parse_date(findtext(element=element, xpath='launchTime',
+                             namespace=NAMESPACE))
         instance_id = findtext(element=element, xpath='instanceId',
                                namespace=NAMESPACE)
         public_ip = findtext(element=element, xpath='ipAddress',
@@ -5421,7 +5423,8 @@
 
         return Node(id=instance_id, name=name, state=state,
                     public_ips=public_ips, private_ips=private_ips,
-                    driver=self.connection.driver, extra=extra)
+                    driver=self.connection.driver, created_at=created,
+                    extra=extra)
 
     def _to_images(self, object):
         return [self._to_image(el) for el in object.findall(
diff --git a/libcloud/compute/drivers/openstack.py b/libcloud/compute/drivers/openstack.py
index 0fe5722..199a7af 100644
--- a/libcloud/compute/drivers/openstack.py
+++ b/libcloud/compute/drivers/openstack.py
@@ -2096,6 +2096,7 @@
         image_id = image.get('id', None) if image else None
         config_drive = api_node.get("config_drive", False)
         volumes_attached = api_node.get('os-extended-volumes:volumes_attached')
+        created = parse_date(api_node["created"])
 
         return Node(
             id=api_node['id'],
@@ -2104,6 +2105,7 @@
                                           NodeState.UNKNOWN),
             public_ips=public_ips,
             private_ips=private_ips,
+            created_at=created,
             driver=self,
             extra=dict(
                 hostId=api_node['hostId'],
diff --git a/libcloud/test/compute/test_digitalocean_v1.py b/libcloud/test/compute/test_digitalocean_v1.py
index 5719500..a09b1a8 100644
--- a/libcloud/test/compute/test_digitalocean_v1.py
+++ b/libcloud/test/compute/test_digitalocean_v1.py
@@ -88,6 +88,10 @@
         self.assertEqual(nodes[0].extra['image_id'], 1601)
         self.assertEqual(nodes[0].extra['size_id'], 66)
 
+    def test_list_nodes_does_not_support_created_datetime(self):
+        nodes = self.driver.list_nodes()
+        self.assertIsNone(nodes[0].created_at)
+
     def test_create_node_invalid_size(self):
         image = NodeImage(id='invalid', name=None, driver=self.driver)
         size = self.driver.list_sizes()[0]
diff --git a/libcloud/test/compute/test_digitalocean_v2.py b/libcloud/test/compute/test_digitalocean_v2.py
index a4a1b01..5f14b7c 100644
--- a/libcloud/test/compute/test_digitalocean_v2.py
+++ b/libcloud/test/compute/test_digitalocean_v2.py
@@ -15,6 +15,9 @@
 import sys
 import unittest
 
+from datetime import datetime
+from libcloud.utils.iso8601 import UTC
+
 try:
     import simplejson as json
 except ImportError:
@@ -87,6 +90,10 @@
         self.assertEqual(nodes[0].extra['image']['id'], 6918990)
         self.assertEqual(nodes[0].extra['size_slug'], '512mb')
 
+    def test_list_nodes_fills_created_datetime(self):
+        nodes = self.driver.list_nodes()
+        self.assertEqual(nodes[0].created_at, datetime(2014, 11, 14, 16, 29, 21, tzinfo=UTC))
+
     def test_create_node_invalid_size(self):
         image = NodeImage(id='invalid', name=None, driver=self.driver)
         size = self.driver.list_sizes()[0]
diff --git a/libcloud/test/compute/test_ec2.py b/libcloud/test/compute/test_ec2.py
index 243425c..4f1bbe1 100644
--- a/libcloud/test/compute/test_ec2.py
+++ b/libcloud/test/compute/test_ec2.py
@@ -217,8 +217,10 @@
         self.assertEqual(node.id, 'i-4382922a')
         self.assertEqual(node.name, node.id)
         self.assertEqual(len(node.public_ips), 2)
-        self.assertEqual(node.extra['launch_time'],
-                         '2013-12-02T11:58:11.000Z')
+
+        self.assertEqual(node.extra['launch_time'], '2013-12-02T11:58:11.000Z')
+        self.assertEqual(node.created_at, datetime(2013, 12, 2, 11, 58, 11, tzinfo=UTC))
+
         self.assertTrue('instance_type' in node.extra)
         self.assertEqual(node.extra['availability'], 'us-east-1d')
         self.assertEqual(node.extra['key_name'], 'fauxkey')
@@ -243,12 +245,14 @@
         self.assertEqual(ret_node2.extra['subnet_id'], 'subnet-5fd9d412')
         self.assertEqual(ret_node2.extra['vpc_id'], 'vpc-61dcd30e')
         self.assertEqual(ret_node2.extra['tags']['Group'], 'VPC Test')
-        self.assertEqual(ret_node1.extra['launch_time'],
-                         '2013-12-02T11:58:11.000Z')
-        self.assertTrue('instance_type' in ret_node1.extra)
-        self.assertEqual(ret_node2.extra['launch_time'],
-                         '2013-12-02T15:58:29.000Z')
-        self.assertTrue('instance_type' in ret_node2.extra)
+
+        self.assertEqual(ret_node1.extra['launch_time'], '2013-12-02T11:58:11.000Z')
+        self.assertEqual(ret_node1.created_at, datetime(2013, 12, 2, 11, 58, 11, tzinfo=UTC))
+        self.assertEqual(ret_node2.extra['launch_time'], '2013-12-02T15:58:29.000Z')
+        self.assertEqual(ret_node2.created_at, datetime(2013, 12, 2, 15, 58, 29, tzinfo=UTC))
+
+        self.assertIn('instance_type', ret_node1.extra)
+        self.assertIn('instance_type', ret_node2.extra)
 
     def test_ex_list_reserved_nodes(self):
         node = self.driver.ex_list_reserved_nodes()[0]
diff --git a/libcloud/test/compute/test_openstack.py b/libcloud/test/compute/test_openstack.py
index ef8d341..ee2469d 100644
--- a/libcloud/test/compute/test_openstack.py
+++ b/libcloud/test/compute/test_openstack.py
@@ -775,6 +775,9 @@
         self.assertTrue(
             'fec0:4801:7808:52:16:3eff:fe60:187d' in node.private_ips)
 
+        # test creation date
+        self.assertEqual(node.created_at, datetime.datetime(2011, 10, 11, 0, 51, 39, tzinfo=UTC))
+
         self.assertEqual(node.extra.get('flavorId'), '2')
         self.assertEqual(node.extra.get('imageId'), '7')
         self.assertEqual(node.extra.get('metadata'), {})