Repository: ifosch/accloudtant

Issue:
Create footer for report
When I use the `report` command
I want to get a footer with totals.

Comments:

Patch:
diff --git a/accloudtant/aws/prices.py b/accloudtant/aws/prices.py
index 44435af..1cb74ef 100644
--- a/accloudtant/aws/prices.py
+++ b/accloudtant/aws/prices.py
@@ -143,7 +143,8 @@ def process_model(url, instances=None):
         instances = {}
     js_name = url.split('/')[-1]
     pricing = requests.get(url)
-    for js_line in io.StringIO(pricing.content.decode("utf-8").replace("\n", "")):
+    content = pricing.content.decode("utf-8").replace("\n", "")
+    for js_line in io.StringIO(content):
         if 'callback' in js_line:
             data = fix_lazy_json(re.sub(r".*callback\((.+)\).*",
                                         r"\1", js_line))
diff --git a/accloudtant/aws/reports.py b/accloudtant/aws/reports.py
index 65a433e..0f56ea5 100644
--- a/accloudtant/aws/reports.py
+++ b/accloudtant/aws/reports.py
@@ -24,7 +24,7 @@ import sys
 
 
 class Reports(object):
-    def __init__(self, logger = None):
+    def __init__(self, logger=None):
         if logger is None:
             self.logger = getLogger('accloudtant.report')
             self.logger.setLevel(DEBUG)
@@ -33,25 +33,36 @@ class Reports(object):
             self.logger = logger
         ec2 = boto3.resource('ec2')
         ec2_client = boto3.client('ec2')
-        instances_filters = [{
-            'Name': 'instance-state-name',
-            'Values': ['running', ],
-        }, ]
-        reserved_instances_filters = [{
-            'Name': 'state',
-            'Values': ['active', ],
-        }, ]
+        self.counters = {
+            'instances': {
+                'total': 0,
+            },
+            'reserved': {
+                'total': 0,
+            },
+        }
+        self.instances = []
+        self.reserved_instances = []
         try:
-            self.instances = [
-                Instance(i)
-                for i in ec2.instances.filter(Filters=instances_filters)
-            ]
-            self.reserved_instances = [
-                ReservedInstance(i)
-                for i in ec2_client.describe_reserved_instances(
-                    Filters=reserved_instances_filters
-                )['ReservedInstances']
-            ]
+            for i in ec2.instances.all():
+                instance = Instance(i)
+                if instance.state == "running":
+                    self.instances.append(instance)
+                if instance.state not in self.counters['instances']:
+                    self.counters['instances'][instance.state] = 0
+                self.counters['instances'][instance.state] += 1
+                self.counters['instances']['total'] += 1
+            ri_key = 'ReservedInstances'
+            reserved_ctrs = self.counters['reserved']
+            for r in ec2_client.describe_reserved_instances()[ri_key]:
+                reserved_instance = ReservedInstance(r)
+                if reserved_instance.state == "active":
+                    self.reserved_instances.append(reserved_instance)
+                    reserved_ctrs['total'] += reserved_instance.instance_count
+            reserved_ctrs['free'] = reserved_ctrs['total']
+            reserved_ctrs['not_reserved'] = reserved_ctrs['total']
+            reserved_ctrs['used'] = 0
+            reserved_ctrs['not reserved'] = 0
         except exceptions.NoCredentialsError:
             logger.error("Error: no AWS credentials found")
             sys.exit(1)
@@ -72,30 +83,60 @@ class Reports(object):
                     instance.reserved = 'Yes'
                     instance.current = reserved.usage_price
                     reserved.link(instance)
+                    self.counters['reserved']['used'] += 1
+                    self.counters['reserved']['free'] -= 1
                     break
+        reserved_counters = self.counters['reserved']
+        instances_counters = self.counters['instances']
+        reserved_counters['not reserved'] = instances_counters['running']
+        reserved_counters['not reserved'] -= reserved_counters['used']
 
     def __repr__(self):
-        headers = ['Id',
-                   'Name',
-                   'Type',
-                   'AZ',
-                   'OS',
-                   'State',
-                   'Launch time',
-                   'Reserved',
-                   'Current hourly price',
-                   'Renewed hourly price']
+        headers = [
+            'Id',
+            'Name',
+            'Type',
+            'AZ',
+            'OS',
+            'State',
+            'Launch time',
+            'Reserved',
+            'Current hourly price',
+            'Renewed hourly price',
+        ]
         table = []
         for instance in self.instances:
-            row = [instance.id,
-                   instance.name,
-                   instance.size,
-                   instance.availability_zone,
-                   instance.operating_system,
-                   instance.state,
-                   instance.launch_time.strftime('%Y-%m-%d %H:%M:%S'),
-                   instance.reserved,
-                   instance.current,
-                   instance.best]
+            row = [
+                instance.id,
+                instance.name,
+                instance.size,
+                instance.availability_zone,
+                instance.operating_system,
+                instance.state,
+                instance.launch_time.strftime('%Y-%m-%d %H:%M:%S'),
+                instance.reserved,
+                instance.current,
+                instance.best,
+            ]
             table.append(row)
-        return tabulate(table, headers)
+        footer_headers = [
+            'Running',
+            'Stopped',
+            'Total instances',
+            'Used',
+            'Free',
+            'Not reserved',
+            'Total reserved',
+        ]
+        footer_table = [[
+            self.counters['instances']['running'],
+            self.counters['instances']['stopped'],
+            self.counters['instances']['total'],
+            self.counters['reserved']['used'],
+            self.counters['reserved']['free'],
+            self.counters['reserved']['not reserved'],
+            self.counters['reserved']['total'],
+        ]]
+        inventory = tabulate(table, headers)
+        summary = tabulate(footer_table, footer_headers)
+        return "{}\n\n{}".format(inventory, summary)
diff --git a/accloudtant/utils/__init__.py b/accloudtant/utils/__init__.py
index c1b5df2..1e7211d 100644
--- a/accloudtant/utils/__init__.py
+++ b/accloudtant/utils/__init__.py
@@ -15,7 +15,6 @@
 #   limitations under the License.
 
 import io
-import codecs
 import tokenize
 import token
 


Test patch:
diff --git a/tests/aws/report_expected.txt b/tests/aws/report_expected.txt
index f2bbb8b..882ff19 100644
--- a/tests/aws/report_expected.txt
+++ b/tests/aws/report_expected.txt
@@ -7,3 +7,7 @@ i-1840273d  database1  r2.8xlarge  us-east-1c  Linux/UNIX                stopped
 i-1840273c  database2  r2.8xlarge  us-east-1c  Linux/UNIX                running  2015-10-22 14:15:10  Yes                         0.611                   0.379
 i-1840273b  database3  r2.8xlarge  us-east-1c  Linux/UNIX                running  2015-10-22 14:15:10  Yes                         0.611                   0.379
 i-912a4393  test       t1.micro    us-east-1c  Linux/UNIX                running  2015-10-22 14:15:10  No                          0.767                   0.3892
+
+  Running    Stopped    Total instances    Used    Free    Not reserved    Total reserved
+---------  ---------  -----------------  ------  ------  --------------  ----------------
+        6          1                  7       5       1               1                 6
diff --git a/tests/aws/report_running_expected.txt b/tests/aws/report_running_expected.txt
index befecd0..e7bd69c 100644
--- a/tests/aws/report_running_expected.txt
+++ b/tests/aws/report_running_expected.txt
@@ -6,3 +6,7 @@ i-9840273d  app2       r2.8xlarge  us-east-1c  SUSE Linux                running
 i-1840273c  database2  r2.8xlarge  us-east-1c  Linux/UNIX                running  2015-10-22 14:15:10  Yes                         0.611                   0.379
 i-1840273b  database3  r2.8xlarge  us-east-1c  Linux/UNIX                running  2015-10-22 14:15:10  Yes                         0.611                   0.379
 i-912a4393  test       t1.micro    us-east-1c  Linux/UNIX                running  2015-10-22 14:15:10  No                          0.767                   0.3892
+
+  Running    Stopped    Total instances    Used    Free    Not reserved    Total reserved
+---------  ---------  -----------------  ------  ------  --------------  ----------------
+        6          1                  7       5       1               1                 6
diff --git a/tests/aws/test_reports.py b/tests/aws/test_reports.py
index d0f6793..57084c3 100644
--- a/tests/aws/test_reports.py
+++ b/tests/aws/test_reports.py
@@ -320,6 +320,39 @@ def test_reports(capsys, monkeypatch, ec2_resource, ec2_client, process_ec2):
             'UsagePrice': 0.611,
             'Duration': 31536000,
             'State': 'active',
+        }, {
+            'ProductDescription': 'Linux/UNIX',
+            'InstanceTenancy': 'default',
+            'InstanceCount': 1,
+            'InstanceType': 't2.micro',
+            'Start': datetime.datetime(
+                    2011,
+                    6,
+                    5,
+                    6,
+                    20,
+                    10,
+                    494000,
+                    tzinfo=tzutc()
+                ),
+            'RecurringCharges': [],
+            'End': datetime.datetime(
+                    2011,
+                    6,
+                    5,
+                    6,
+                    20,
+                    10,
+                    tzinfo=tzutc()
+                ),
+            'CurrencyCode': 'USD',
+            'OfferingType': 'Medium Utilization',
+            'ReservedInstancesId': '46a408c7-c33d-422d-af59-28df12233320',
+            'FixedPrice': 5352.36,
+            'AvailabilityZone': 'us-east-1c',
+            'UsagePrice': 0.611,
+            'Duration': 31536000,
+            'State': 'active',
         }, {
             'ProductDescription': 'Linux/UNIX',
             'InstanceTenancy': 'default',
diff --git a/tests/aws/test_reserved_instance.py b/tests/aws/test_reserved_instance.py
index 9627ebf..50fd6fc 100644
--- a/tests/aws/test_reserved_instance.py
+++ b/tests/aws/test_reserved_instance.py
@@ -13,7 +13,6 @@
 #   limitations under the License.
 
 import datetime
-import pytest
 from dateutil.tz import tzutc
 import accloudtant.aws.reserved_instance
 from conftest import MockEC2Instance
diff --git a/tests/test_utils.py b/tests/test_utils.py
index caad5a4..c3b0877 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -19,4 +19,5 @@ import accloudtant.utils
 def test_fix_lazy_json():
     bad_json = '{ key: "value" }'.encode('utf-8')
     good_json = '{"key":"value"}'
-    assert(accloudtant.utils.fix_lazy_json(codecs.decode(bad_json)) == good_json)
+    result = accloudtant.utils.fix_lazy_json(codecs.decode(bad_json))
+    assert(result == good_json)