Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

initqs command #570

Merged
merged 16 commits into from
Aug 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions cid/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,5 +225,11 @@ def share(ctx, dashboard_id, **kwargs):

ctx.obj.share(dashboard_id)

@cid_command
def initqs(ctx, **kwargs):
"""Initialize QuickSight resources for deployment"""

ctx.obj.initqs(**kwargs)

if __name__ == '__main__':
main()
4 changes: 4 additions & 0 deletions cid/commands/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# flake8: noqa: E401
from cid.commands.init_qs import InitQsCommand


13 changes: 13 additions & 0 deletions cid/commands/command_base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from abc import ABC, abstractmethod


class Command(ABC): # pylint: disable=too-few-public-methods
"""Abstract base class for commands"""

@abstractmethod
def __init__(self, cid, logger=None, **kwargs):
"""Initialize the class"""

@abstractmethod
def execute(self, *args, **kwargs):
"""Run the command"""
114 changes: 114 additions & 0 deletions cid/commands/init_qs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
""" Command Init QuickSight
"""
import time
import logging

from cid.commands.command_base import Command
from cid.exceptions import CidCritical
from cid.utils import (
get_parameter,
get_yesno_parameter,
unset_parameter,
cid_print,
)

logger = logging.getLogger(__name__)

MAX_ITERATIONS = 5


class InitQsCommand(Command): # pylint: disable=too-few-public-methods
"""Init Command for CLI"""

def __init__(self, cid, **kwargs):
self.cid = cid

def execute(self, *args, **kwargs):
"""Execute the initilization"""
self._create_quicksight_enterprise_subscription() # No tagging available

def _create_quicksight_enterprise_subscription(self):
"""Enable QuickSight Enterprise if not enabled already"""
cid_print('Analysing QuickSight Status')
if self.cid.qs.edition(fresh=True) in ('ENTERPRISE', 'ENTERPRISE_AND_Q'):
cid_print(f'QuickSight Edition is {self.cid.qs.edition()}')
return

cid_print(
'<BOLD><RED>IMPORTANT<END>: <BOLD>Amazion QuickSight Enterprise Edition is required for Cost Intelligence Dashboard. '
'This will lead to costs in your AWS account (https://aws.amazon.com/quicksight/pricing/).<END>'
)

if not self.cid.all_yes and not get_yesno_parameter(
param_name='enable-quicksight-enterprise',
message='Please, confirm enabling of Amazion QuickSight Enterprise',
default='no'
):
cid_print('\tInitalization cancelled')
return

for counter in range(MAX_ITERATIONS):
email = self._get_email_for_quicksight()
account_name = self._get_account_name_for_quicksight()
params = self._get_quicksight_params(email, account_name)
try:
response = self.cid.qs.client.create_account_subscription(**params)
logger.debug(f'create_account_subscription resp: {response}')
if response.get('Status') != 200:
raise CidCritical(f'Subscription answer is not 200: {response}')
break
except Exception as exc: #pylint: disable=broad-exception-caught
cid_print(f'\tQuickSight Edition...\tError ({exc}). Please, try again or press CTRL + C to interrupt.')
unset_parameter('qs-account-name')
unset_parameter('qs-notification-email')
if counter == MAX_ITERATIONS - 1:
raise CidCritical('Quicksight setup failed') from exc
while self.cid.qs.edition(fresh=True) not in ('ENTERPRISE', 'ENTERPRISE_AND_Q'):
time.sleep(5)
cid_print(f'\tQuickSight Edition is {self.cid.qs.edition()}.')

def _get_quicksight_params(self, email, account_name):
"""Create dictionary of quicksight subscription initialization parameters"""
return {
'Edition': 'ENTERPRISE',
'AuthenticationMethod': 'IAM_AND_QUICKSIGHT',
'AwsAccountId': self.cid.base.account_id,
'AccountName': account_name, # Should be a parameter with a reasonable default
'NotificationEmail': email, # Read the value from account parameters as a default
}

def _get_account_name_for_quicksight(self):
"""Get the account name for quicksight"""
for _ in range(MAX_ITERATIONS):
account_name = get_parameter(
'qs-account-name',
message=(
'\n\tPlease, choose a descriptive name for your QuickSight account. '
'This will be used later to share it with your users. This can NOT be changed later.'
),
default=self.cid.organizations.get_account_name()
)
if account_name:
return account_name
print('\t The account name must not be empty. Please, try again.')
unset_parameter('qs-account-name')
else: #pylint: disable=W0120:useless-else-on-loop
raise CidCritical('Failed to read QuickSight Account Name')

def _get_email_for_quicksight(self):
"""Get email for quicksight"""
for _ in range(MAX_ITERATIONS):
email = get_parameter(
'qs-notification-email',
message=(
'Amazon QuickSight needs your email address to send notifications '
'regarding your Amazon QuickSight account.'
),
default=self.cid.organizations.get_account_email()
)
if '@' in email and '.' in email:
return email
cid_print(f'\t{email} does not seem to be a valid email. Please, try again.')
unset_parameter('qs-notification-email')
else: #pylint: disable=W0120:useless-else-on-loop
raise CidCritical('Failed to read email')
15 changes: 14 additions & 1 deletion cid/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,13 @@
from cid.plugin import Plugin
from cid.utils import get_parameter, get_parameters, set_parameters, unset_parameter, get_yesno_parameter, cid_print, isatty
from cid.helpers.account_map import AccountMap
from cid.helpers import Athena, CUR, Glue, QuickSight, Dashboard, Dataset, Datasource, csv2view
from cid.helpers import Athena, CUR, Glue, QuickSight, Dashboard, Dataset, Datasource, csv2view, Organizations
from cid.helpers.quicksight.template import Template as CidQsTemplate
from cid._version import __version__
from cid.export import export_analysis
from cid.logger import set_cid_logger
from cid.exceptions import CidError, CidCritical
from cid.commands import InitQsCommand

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -107,6 +108,14 @@ def glue(self) -> Glue:
'glue': Glue(self.base.session)
})
return self._clients.get('glue')

@property
def organizations(self) -> Organizations:
if not self._clients.get('organizations'):
self._clients.update({
'organizations': Organizations(self.base.session)
})
return self._clients.get('organizations')

@property
def cur(self) -> CUR:
Expand Down Expand Up @@ -1565,3 +1574,7 @@ def map(self, **kwargs):
for v in ['account_map', 'aws_accounts']:
self.accountMap.create(v)

@command
def initqs(self, **kwargs):
""" Initialize QuickSight resources for deployment """
return InitQsCommand(cid=self, **kwargs).execute()
2 changes: 2 additions & 0 deletions cid/helpers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from cid.helpers.diff import diff
from cid.helpers.quicksight import QuickSight, Dashboard, Dataset, Datasource, Template
from cid.helpers.csv2view import csv2view
from cid.helpers.organizations import Organizations

__all__ = [
"Athena",
Expand All @@ -16,4 +17,5 @@
"Template",
"diff",
"csv2view",
"Organizations"
]
31 changes: 31 additions & 0 deletions cid/helpers/organizations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import logging

from cid.base import CidBase

logger = logging.getLogger(__name__)


class Organizations(CidBase):
"""Organizations helper class"""

def __init__(self, session):
super().__init__(session)
self.client = self.session.client("organizations", region_name=self.region)

def get_account_email(self):
"""Try to extract the account's email address for Organizations"""
try:
result = self.client.describe_account(AccountId=self.account_id).get("Email", "")
except Exception: # pylint: disable=broad-except
result = None

return result

def get_account_name(self):
"""Try to extract the account name from Organizations"""
try:
result = self.client.describe_account(AccountId=self.account_id).get("Name", "")
except Exception: # pylint: disable=broad-except
result = None

return result
32 changes: 15 additions & 17 deletions cid/helpers/quicksight/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ class QuickSight(CidBase):
_user: dict = None
_principal_arn: dict = None
_group: dict = None
_subscription_info: dict = None
client = None

def __init__(self, session, resources=None) -> None:
Expand Down Expand Up @@ -108,9 +109,11 @@ def identityRegion(self) -> str:
logger.info(f'Using QuickSight identity region: {self._identityRegion}')
return self._identityRegion

@property
def edition(self) -> Union[str, None]:
if not hasattr(self, '_subscription_info'):
def edition(self, fresh: bool=False) -> str:
""" get QuickSight Edition
:fresh: set to True if you want it fresh (not cached)
"""
if fresh or not hasattr(self, '_subscription_info'):
self._subscription_info = self.describe_account_subscription()
return self._subscription_info.get('Edition')

Expand Down Expand Up @@ -154,29 +157,24 @@ def datasources(self) -> Dict[str, Datasource]:

return self._datasources


def ensure_subscription(self) -> None:
"""Ensure that the QuickSight subscription is active"""

if not self.edition:
raise CidCritical('QuickSight is not activated')
elif self.edition != 'STANDARD':
logger.info(f'QuickSight subscription: {self._subscription_info}')
else:
raise CidCritical(f'QuickSight Enterprise edition is required, you have {self.edition}')
if not self.edition(fresh=True):
raise CidCritical('QuickSight is not activated. Plase run `cid-cmd initqs` command, or activate QuickSight from the console.')
if self.edition() == 'STANDARD':
raise CidCritical(f'QuickSight Enterprise edition is required, you have {self.edition}.')
logger.info(f'QuickSight subscription: {self._subscription_info}')

def describe_account_subscription(self) -> dict:
"""Returns the account subscription details"""
result = dict()

try:
result = self.client.describe_account_subscription(AwsAccountId=self.account_id).get('AccountInfo')
except self.client.exceptions.AccessDeniedException as e:
"""
In case we lack privileges to DescribeAccountSubscription API
we use ListDashboards API call that throws UnsupportedUserEditionException
in case the account doesn't have Enterprise edition
"""
except self.client.exceptions.AccessDeniedException:
# In case we lack privileges to DescribeAccountSubscription API
# we use ListDashboards API call that throws UnsupportedUserEditionException
# in case the account doesn't have Enterprise edition
logger.info('Insufficient privileges to describe account subscription, working around')
try:
self.client.list_dashboards(AwsAccountId=self.account_id).get('AccountInfo')
Expand Down
1 change: 1 addition & 0 deletions cid/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import inspect
import logging
import platform
from typing import Any, Dict
import requests
from functools import lru_cache as cache
from collections.abc import Iterable
Expand Down
Loading