Skip to content

Commit

Permalink
feat: ec2 describe volumes (#421)
Browse files Browse the repository at this point in the history
eg:
```
❯ ec2 describe -c Name,Volumes
                                         
  Name                 Volumes           
 ─────────────────────────────────────── 
  the-fresh-instance   ['Size=140 GiB']  
```
  • Loading branch information
tekumara authored Mar 27, 2023
1 parent f2bf29e commit ce6d8e1
Show file tree
Hide file tree
Showing 6 changed files with 108 additions and 24 deletions.
4 changes: 0 additions & 4 deletions .darglint

This file was deleted.

1 change: 1 addition & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ repos:
rev: v1.13.23
hooks:
- id: typos
stages: [push]
# vscode & the cli uses these too so they has been installed into the virtualenv
- repo: local
hooks:
Expand Down
68 changes: 64 additions & 4 deletions docs/ec2.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Run `aec ec2 -h` for help:
from aec.main import build_parser
cog.out(f"```\n{build_parser()._subparsers._actions[1].choices['ec2'].format_help()}```")
]]] -->

```
usage: aec ec2 [-h] {create-key-pair,describe,launch,logs,modify,start,stop,tag,tags,status,templates,terminate,user-data} ...
Expand All @@ -36,6 +37,7 @@ subcommands:
terminate Terminate EC2 instance.
user-data Describe user data for an instance.
```

<!-- [[[end]]] -->

Launch an instance named `food baby` from the [ec2 launch template](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-launch-templates.html) named `yummy`:
Expand Down Expand Up @@ -73,14 +75,16 @@ List all instances in the region:
<!-- [[[cog
cog.out(f"```\n{docs('aec ec2 describe', ec2.describe(config))}\n```")
]]] -->

```
aec ec2 describe
InstanceId State Name Type DnsName LaunchTime ImageId
InstanceId State Name Type DnsName LaunchTime ImageId
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
i-b5f2c2719ad4a4204 running alice t3.small ec2-54-214-90-201.compute-1.amazonaws.com 2023-03-15 23:25:13+00:00 ami-03cf127a
i-b5f2c2719ad4a4204 running alice t3.small ec2-54-214-90-201.compute-1.amazonaws.com 2023-03-15 23:25:13+00:00 ami-03cf127a
i-17227103cbf97cb86 running sam t3.small ec2-54-214-204-133.compute-1.amazonaws.com 2023-03-15 23:25:14+00:00 ami-03cf127a
```

<!-- [[[end]]] -->

List instances containing `gaga` in the name:
Expand All @@ -107,10 +111,10 @@ Show running instances sorted by date started (ie: LaunchTime), oldest first:
aec ec2 describe -r -s LaunchTime
```

Show a custom set of columns
Show a custom set of [columns](#columns)

```
aec ec2 describe -c SubnetId,Name
aec ec2 describe -c Name,SubnetId,Volumes
```

Show instances and all their tags:
Expand Down Expand Up @@ -187,3 +191,59 @@ aec ec2 describe -it i-02a840e0ca609c432 -c StateReason
─────────────────────────────────────────────────────────────────────────────────────────────
{'Code': 'Client.InternalError', 'Message': 'Client.InternalError: Client error on launch'}
```

## Columns

Columns special to aec:

- `DnsName` - PublicDnsName if available otherwise PrivateDnsName
- `Name` - Name tag
- `State` - state name
- `Type` - instance type
- `Volumes` - volumes attached to the instance

Columns returned by the EC2 API:

```
AmiLaunchIndex
Architecture
BlockDeviceMappings
CapacityReservationSpecification
ClientToken
CpuOptions
CurrentInstanceBootMode
EbsOptimized
EnaSupport
EnclaveOptions
HibernationOptions
Hypervisor
IamInstanceProfile
ImageId
InstanceId
InstanceType
KeyName
LaunchTime
MaintenanceOptions
MetadataOptions
Monitoring
NetworkInterfaces
Placement
PlatformDetails
PrivateDnsName
PrivateDnsNameOptions
PrivateIpAddress
ProductCodes
PublicDnsName
RootDeviceName
RootDeviceType
SecurityGroups
SourceDestCheck
State
StateTransitionReason
SubnetId
Tags
UsageOperation
UsageOperationUpdateTime
VirtualizationType
VpcId
```
27 changes: 21 additions & 6 deletions src/aec/command/ec2.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import base64
import os
import os.path
from collections import defaultdict
from time import sleep
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence, cast

Expand All @@ -17,6 +18,7 @@
from mypy_boto3_ec2.literals import InstanceTypeType
from mypy_boto3_ec2.type_defs import (
BlockDeviceMappingTypeDef,
DescribeVolumesResultTypeDef,
FilterTypeDef,
InstanceStatusSummaryTypeDef,
TagSpecificationTypeDef,
Expand All @@ -40,6 +42,7 @@ class Instance(TypedDict, total=False):
Type: str
DnsName: str
SubnetId: str
Volumes: List[str]


def launch(
Expand Down Expand Up @@ -222,15 +225,27 @@ def describe(

kwargs: Dict[str, Any] = {"MaxResults": 1000, "Filters": filters}

response = ec2_client.describe_instances(**kwargs)

# import json; print(json.dumps(response))
response_fut = executor.submit(ec2_client.describe_instances, **kwargs)

cols = columns.split(",")

# don't sort by cols we aren't showing
sort_cols = [sc for sc in sort_by.split(",") if sc in cols]

if "Volumes" in columns:
# fetch volume info
volumes_response: DescribeVolumesResultTypeDef = executor.submit(ec2_client.describe_volumes).result()
volumes: Dict[str, List[str]] = defaultdict(list)
for v in volumes_response["Volumes"]:
for a in v["Attachments"]:
volumes[a["InstanceId"]].append(f'Size={v["Size"]} GiB')
else:
volumes = {}

response = response_fut.result()

# import json; print(json.dumps(response))

instances: List[Instance] = []
while True:
for r in response["Reservations"]:
Expand All @@ -246,9 +261,9 @@ def describe(
elif col == "Type":
desc[col] = i["InstanceType"]
elif col == "DnsName":
desc[col] = (
i["PublicDnsName"] if i.get("PublicDnsName", None) != "" else i["PrivateDnsName"]
)
desc[col] = i["PublicDnsName"] or i["PrivateDnsName"]
elif col == "Volumes":
desc[col] = volumes.get(i["InstanceId"], [])
else:
desc[col] = i.get(col, None)

Expand Down
10 changes: 10 additions & 0 deletions tests/test_display.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,16 @@ def test_as_table_with_datetime():
]


def test_as_table_with_list():
assert as_table(
[{"a": 1, "b": ["x", "y"]}],
["a", "b"],
) == [
["a", "b"],
["1", "['x', 'y']"],
]


def test_as_table_with_none():
assert as_table([{"a": 1, "b": None}]) == [["a", "b"], ["1", None]]

Expand Down
22 changes: 12 additions & 10 deletions tests/test_ec2.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,15 +177,6 @@ def test_describe_instance_without_tags(mock_aws_config: Config):
assert len(instances) == 1


def test_tag(mock_aws_config: Config):
launch(mock_aws_config, "alice", ami_id)

instances = tag(mock_aws_config, ["Project=top secret"], "alice")

assert len(instances) == 1
assert instances[0]["Tag: Project"] == "top secret"


def test_describe_by_name(mock_aws_config: Config):
launch(mock_aws_config, "alice", ami_id)
launch(mock_aws_config, "alex", ami_id)
Expand Down Expand Up @@ -260,14 +251,16 @@ def test_describe_columns(mock_aws_config: Config):
del mock_aws_config["key_name"]
launch(mock_aws_config, "alice", ami_id)

instances = describe(config=mock_aws_config, columns="SubnetId,Name,MissingKey")
instances = describe(config=mock_aws_config, columns="SubnetId,Name,MissingKey,Volumes")
print(instances)

assert len(instances) == 2
assert instances[0]["Name"] == "alice"
assert instances[1]["Name"] == "sam"
assert "subnet" in instances[0]["SubnetId"]
assert "subnet" in instances[1]["SubnetId"]
assert instances[0]["Volumes"] == ["Size=15 GiB"]
assert instances[1]["Volumes"] == ["Size=15 GiB"]

# MissingKey will appear without values
assert instances[0]["MissingKey"] is None # type: ignore
Expand All @@ -280,6 +273,15 @@ def describe_instance0(region_name: str, instance_id: str):
return instances["Reservations"][0]["Instances"][0]


def test_tag(mock_aws_config: Config):
launch(mock_aws_config, "alice", ami_id)

instances = tag(mock_aws_config, ["Project=top secret"], "alice")

assert len(instances) == 1
assert instances[0]["Tag: Project"] == "top secret"


def test_tags(mock_aws_config: Config):
mock_aws_config["additional_tags"] = {"Owner": "alice@testlab.io", "Project": "top secret"}
launch(mock_aws_config, "alice", ami_id)
Expand Down

0 comments on commit ce6d8e1

Please sign in to comment.