Skip to content

Commit

Permalink
Merge pull request #47 from akamai/rc0.6.9
Browse files Browse the repository at this point in the history
Version 0.6.9
  • Loading branch information
bitonio authored Jun 3, 2024
2 parents ad1cc13 + 459be13 commit 9c18a1b
Show file tree
Hide file tree
Showing 13 changed files with 393 additions and 150 deletions.
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.6.8
0.6.9
128 changes: 4 additions & 124 deletions bin/akamai-eaa
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ from application import ApplicationAPI
from idp import IdentityProviderAPI
from connector import ConnectorAPI
from directory import DirectoryAPI
from reporting import ReportingAPI

verbose = getattr(config, 'verbose', False)

Expand Down Expand Up @@ -98,129 +99,6 @@ class SearchAPI(BaseAPI):
cli.footer("%s app(s)" % app_count)


class ReportingAPI(BaseAPI):
def __init__(self, config):
super(ReportingAPI, self).__init__(config, api=BaseAPI.API_Version.OpenAPI)

def clients(self):
now = time.time()
params = {
'limit': 0,
'start': int((now - 30 * 24 * 60 * 60) * 1000),
'end': int(now * 1000)
}
resp = self.get('mgmt-pop/clients', params=params)
if resp.status_code != 200:
logging.error(resp.text)
data = resp.json()
cli.header("#device_id,version,idp_user,idp_host,lastseen")
for count, c in enumerate(data.get('objects', {})):
cli.print("{device_id},{version},{idp_user},{idp_host},{lastseen}".format(
device_id=c.get("device_id"),
version=c.get("device_info", {}).get("version"),
idp_user=c.get("idp_user"),
idp_host=c.get("idp_host"),
lastseen=c.get("timestamp")
))
cli.footer("%s unique EAA Clients checked-in in the last 30 days" % count)


@staticmethod
def facility_from_popname(pop_name):
"""
Crudly derive the facility based on the POP name.
"""
facility = "AWS"
if "-LIN-" in pop_name:
facility = "Akamai Cloud Compute (formerly Linode)"
return facility

def tenant_info(self):
"""
Display tenant info/stats.
"""
info = {"cloudzones": []}
resp = self.get('mgmt-pop/pops?shared=true')

if self._config.show_usage:
app_apiv3 = ApplicationAPI(self._config, BaseAPI.API_Version.OpenAPIv3)
app_api = ApplicationAPI(self._config, BaseAPI.API_Version.OpenAPI)
idp_api = IdentityProviderAPI(self._config)
app_by_cz = app_apiv3.stats_by_cloudzone()
entdns_by_cz = app_api.entdns_stats_by_cloudzone()
idp_by_pop = idp_api.stats_by_pop()

scanned_cz = []
for eaa_cloudzone in resp.json().get('objects'):
cz_info = {
"name": eaa_cloudzone.get('region'),
"facility": ReportingAPI.facility_from_popname(eaa_cloudzone.get('name')),
}
if self._config.show_usage:
cz_info["count_idp"] = idp_by_pop.get(eaa_cloudzone.get('uuid_url'), 0)
cz_info["count_app"] = app_by_cz.get(eaa_cloudzone.get('region'), 0)
cz_info["count_entdns"] = entdns_by_cz.get(eaa_cloudzone.get('region'), 0)

scanned_cz.append(cz_info)

# sort by Cloud Zone name
info['cloudzones'] = sorted(scanned_cz, key=lambda x: x['name'])

print(dumps(info, indent=4))

def deviceposture_inventory(self, follow=False, interval=300):
"""
Fetch Device Posture inventory once or until a SIG_TERM is received or Control-C is pressed
Args:
follow (bool, optional): Will not terminate and keep pulling every `interval` sec. Defaults to False.
interval (int, optional): Interval in seconds to pull the inventory. Defaults to 300.
"""
while not cli.stop_event.is_set():
start = time.time()
offset = 0
limit = 3000
devices = []

while not cli.stop_event.is_set():
page_start = time.time()
resp = self.get('device-posture/inventory/list', params={'offset': offset, 'limit': limit})
if resp.status_code != 200:
cli.print_error("Non HTTP 200 response fromt the API")
cli.exit(2)
doc = resp.json()
meta = doc.get('meta', {})
next_page = meta.get('next')
limit = meta.get('limit')
objects_in_page = doc.get('objects', [])
offset = dict(parse_qsl(next_page)).get('offset')
logging.debug("--- DP Inventory page with {c} devices fetched in {elapsed:.2f} seconds".format(
c=len(objects_in_page), elapsed=time.time()-page_start))
devices += objects_in_page
if not next_page:
break

for d in devices:
cli.print(dumps(d))

logging.debug("DP Inventory Total {device_count} devices fetched in {elapsed:.2f} seconds".format(
device_count=len(devices), elapsed=time.time()-start))
if follow is False:
break
else:
wait_time = interval - (time.time() - start) # interval - time elapsed
if wait_time > 0:
logging.debug("Sleeping for {:.2f} seconds".format(wait_time))
time.sleep(wait_time)
else:
logging.warn("Fetching data takes more time than the interval, "
"consider increase the --interval parameter")

def deviceposture_devicehistory(self, device_id):
resp = self.get('device-posture/inventory/device-history/{deviceId}'.format(deviceId=device_id))
print(dumps(resp.json(), indent=4))


def setup_logging():

logging.basicConfig(filename=config.logfile, level=cli.log_level(), format=LOG_FMT)
Expand Down Expand Up @@ -310,10 +188,12 @@ if __name__ == "__main__":
elif config.command in ("idp", "i"):
i = IdentityProviderAPI(config)
i.list()
elif config.command in ("report", "r"):
elif config.command in ("report", "reports", "r"):
r = ReportingAPI(config)
if config.report_name == "clients":
r.clients()
elif config.report_name == "last_access":
r.last_access(config.start, config.end, config.app)
elif config.command in ("info", ):
r = ReportingAPI(config)
r.tenant_info()
Expand Down
12 changes: 11 additions & 1 deletion bin/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import os
import argparse
import time

from configparser import ConfigParser

Expand Down Expand Up @@ -159,7 +160,16 @@ def __init__(self, config_values, configuration, flags=None):
help="Deploy impacted apps/idp after the rotation")

report_parser = subparsers.add_parser('report', aliases=["r"], help='EAA reports')
report_parser.add_argument(dest='report_name', choices=['clients'], help="Report name")
# Pre 0.6.9
# report_parser.add_argument(dest='report_name', choices=['clients', 'last_access'], help="Report name")
subsub = report_parser.add_subparsers(dest="report_name", help='Report name')
subsub.add_parser("clients")
access_parser = subsub.add_parser("last_access")
access_parser.add_argument('--app', '-a', type=str, help="Application/IdP UUID")
access_parser.add_argument('--start', '-s', type=int, default=int(time.time() - 7 * 24 * 60 * 60),
help="Start datetime (EPOCH), default is 1 week ago")
access_parser.add_argument('--end', '-e', type=int, default=int(time.time()),
help="End datetime (EPOCH), default is now")

con_parser = subparsers.add_parser('connector', aliases=["c", "con"], help='Manage EAA connectors')
con_parser.add_argument(dest='connector_id', nargs="?", default=None,
Expand Down
2 changes: 1 addition & 1 deletion cli.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"commands": [
{
"name": "eaa",
"version": "0.6.8",
"version": "0.6.9",
"description": "Akamai CLI for Enterprise Application Access (EAA)"
}
]
Expand Down
44 changes: 44 additions & 0 deletions docs/commands/akamai-eaa-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
[< cli-eaa documentation](../../README.md)

# akamai eaa report<!-- omit in toc -->

Manage EAA application configurations

Alias: `r`, `reports`

## Table of contents<!-- omit in toc -->

- [Users who connected successfully to EAA and their last access time](#users-who-connected-successfully-to-eaa-and-their-last-access-time)


## Users who connected successfully to EAA and their last access time

This command may take several minutes depending on the time range, and activity on the IdP.
Tenant with Client-based application will typically have more events and longer time to process.

It is a good idea to grab the *idp hostname* with the command:

```
$ akamai eaa idp | column -s, -t
#IdP-id name idp_hostname status certificate client dp
idp://yb_XndD-RjmCFTrTx1QGhw AMEAADEMO welcome.akamaidemo.net Deployed crt://XtH1dUwqS0qAztk0Zl_M6w N N
idp://_unBFxtJRbS4gZ8pz2ZLDA OKTA login-okta.akamaidemo.net Deployed crt://XtH1dUwqS0qAztk0Zl_M6w Y N
idp://NMmW2cwqQJanYn3VTXoS9Q Global IdP login.akamaidemo.net Deployed crt://XtH1dUwqS0qAztk0Zl_M6w Y N
idp://TRmwtYFHRqi9BDXlkvzceg ADFS login-adfs.akamaidemo.net Deployed crt://XtH1dUwqS0qAztk0Zl_M6w N N
```

From there, generate the report with the `-a` parameter:

```
$ akamai eaa report last_access -a login.akamaidemo.net > report_unique_users.csv
Range start: 1716850539 2024-05-27 15:55:39
Range end: 1717455339 2024-06-03 15:55:39
11 users accessed the application for this time range, 193 records processed
1 API calls issued.
$ head report_unique_users.csv
userid,last_access_epoch_ms,last_access_iso8601
engineering,1716993722339,2024-05-29T14:42:02.339000+00:00Z
it,1717141602759,2024-05-31T07:46:42.759000+00:00Z
sales,1716883434605,2024-05-28T08:03:54.605000+00:00Z
```
2 changes: 1 addition & 1 deletion libeaa/cert.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2022 Akamai Technologies, Inc. All Rights Reserved
# Copyright 2024 Akamai Technologies, Inc. All Rights Reserved
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand Down
34 changes: 19 additions & 15 deletions libeaa/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"""

#: cli-eaa version [PEP 8]
__version__ = '0.6.8'
__version__ = '0.6.9'

import sys
from threading import Event
Expand All @@ -38,6 +38,7 @@
# If all parameters are set already, use them. Otherwise
# use the config
config = EdgeGridConfig({'verbose': False}, 'default')
logger = logging.getLogger(__name__)

#: HTTP Request Timeout in seconds
HTTP_REQ_TIMEOUT = 300
Expand Down Expand Up @@ -83,12 +84,12 @@ def log_level():

@staticmethod
def exit(code):
logging.info("Exit cli-eaa with code %s" % code)
logger.info("Exit cli-eaa with code %s" % code)
exit(code)

@staticmethod
def exit_gracefully(signum, frame):
logging.info(f"Stop due to SIGTERM(15) or SIGINT(2) signal received: {signum} ")
logger.info(f"Stop due to SIGTERM(15) or SIGINT(2) signal received: {signum} ")
cli.stop_event.set()


Expand Down Expand Up @@ -231,10 +232,10 @@ def __init__(self, config=None, api=API_Version.Legacy):
if self._session:
self._session.headers.update({'User-Agent': self.user_agent()})
if config.proxy:
logging.info("Set proxy to %s" % config.proxy)
logger.info("Set proxy to %s" % config.proxy)
self._session.proxies['https'] = 'http://%s' % config.proxy

logging.info("Initialized with base_url %s" % self._baseurl)
logger.info("Initialized with base_url %s" % self._baseurl)

def user_agent(self):
return f"{self._config.ua_prefix} cli-eaa/{__version__}"
Expand All @@ -253,14 +254,17 @@ def build_params(self, params=None):
final_params.update({'accountSwitchKey': self._config.accountkey})
if isinstance(params, dict):
final_params.update(params)
if params.get('_remove-extras_'):
for p in ('_remove-extras_', 'contract_id', 'accountkey', 'accountSwitchKey', 'ua'):
if final_params.get(p): del(final_params[p])
return final_params

@staticmethod
def log_response_summary(response):
"""
Log information about the API request/response to help with troubleshooting.
"""
logging.info(f"BaseAPI: {response.url} {response.request.method} response is HTTP/{response.status_code}, "
logger.info(f"BaseAPI: {response.url} {response.request.method} response is HTTP/{response.status_code}, "
f"x-trace-id: {response.headers.get('x-trace-id')}, "
f"x-ids-session-id: {response.headers.get('x-ids-session-id')}")

Expand All @@ -272,39 +276,39 @@ def get(self, url_path, params=None):
response = self._session.get(url, params=self.build_params(params), timeout=HTTP_REQ_TIMEOUT)
BaseAPI.log_response_summary(response)
if response.status_code == 401:
logging.fatal(f"API returned HTTP/401, check your API credentials\n{response.text}")
logging.fatal(f"EdgeRC Section: {config.section}")
logger.fatal(f"API returned HTTP/401, check your API credentials\n{response.text}")
logger.fatal(f"EdgeRC Section: {config.section}")
cli.exit(401)
if response.status_code != requests.status_codes.codes.ok:
logging.info("BaseAPI: GET response body: %s" % response.text)
logger.info("BaseAPI: GET response body: %s" % response.text)
return response

def post(self, url_path, json=None, params=None, allow_redirects=True):
url = urljoin(self._baseurl, url_path)
logging.info("API URL: %s" % url)
logger.info("API URL: %s" % url)
response = self._session.post(url, json=json, params=self.build_params(params),
timeout=HTTP_REQ_TIMEOUT, allow_redirects=allow_redirects)
BaseAPI.log_response_summary(response)
if response.status_code != 200:
logging.info("BaseAPI: POST response body: %s" % response.text)
logger.info("BaseAPI: POST response body: %s" % response.text)
return response

def put(self, url_path, json=None, params=None):
url = urljoin(self._baseurl, url_path)
logging.info("[PUT] API URL: %s" % url)
logger.info("[PUT] API URL: %s" % url)
response = self._session.put(url, json=json, params=self.build_params(params), timeout=HTTP_REQ_TIMEOUT)
BaseAPI.log_response_summary(response)
if response.status_code != 200:
logging.info("BaseAPI: PUT response body: %s" % response.text)
logger.info("BaseAPI: PUT response body: %s" % response.text)
return response

def delete(self, url_path, json=None, params=None):
url = urljoin(self._baseurl, url_path)
logging.info("API URL: %s" % url)
logger.info("API URL: %s" % url)
response = self._session.delete(url, json=json, params=self.build_params(params), timeout=HTTP_REQ_TIMEOUT)
BaseAPI.log_response_summary(response)
if response.status_code != 200:
logging.info("BaseAPI: DELETE response body: %s" % response.text)
logger.info("BaseAPI: DELETE response body: %s" % response.text)
return response


Expand Down
2 changes: 1 addition & 1 deletion libeaa/directory.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2022 Akamai Technologies, Inc. All Rights Reserved
# Copyright 2024 Akamai Technologies, Inc. All Rights Reserved
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand Down
2 changes: 1 addition & 1 deletion libeaa/error.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2022 Akamai Technologies, Inc. All Rights Reserved
# Copyright 2024 Akamai Technologies, Inc. All Rights Reserved
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand Down
2 changes: 1 addition & 1 deletion libeaa/eventlog.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2022 Akamai Technologies, Inc. All Rights Reserved
# Copyright 2024 Akamai Technologies, Inc. All Rights Reserved
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand Down
Loading

0 comments on commit 9c18a1b

Please sign in to comment.