def list_apps(self, con_moniker, perf=False): if not isinstance(con_moniker, EAAItem): raise TypeError("con_moniker") if perf: perf_by_apphost = self.perf_apps(con_moniker.uuid) line_fmt = "{app_id},{app_name},{perf_upd},{active}" cli.header("#app_id,app_name,perf_upd,active") else: line_fmt = "{app_id},{app_name}" cli.header("#app_id,app_name") for c, (app_id, app_name, app_host) in enumerate(self.findappbyconnector(con_moniker), start=1): perf_data = {} if perf: perf_data = perf_by_apphost.get(app_host, {}) cli.print( line_fmt.format(app_id=app_id, app_name=app_name, perf_upd=perf_data.get('timestamp', ConnectorAPI.NODATA), active=perf_data.get('active', ConnectorAPI.NODATA))) cli.footer("%s application(s) attached to connector %s" % (c, con_moniker.uuid))
def delgroup(self, appgroup_moniker): cli.print("Delete App-Group association %s..." % appgroup_moniker) deletion = self.post('mgmt-pop/appgroups', params={'method': 'DELETE'}, json={'deleted_objects': [appgroup_moniker.uuid]}) if deletion.status_code == 200: cli.print("Association %s deleted." % appgroup_moniker)
def loadgroups(self, app_moniker): """Directory+Groups allowed to access this application.""" url_params = {'limit': 0, 'expand': 'true', 'expand_sdk': 'true'} url = 'mgmt-pop/apps/{applicationId}/groups'.format(applicationId=app_moniker.uuid) result = self.get(url, url_params) count = 0 allowed_groups = result.json().get('objects') if not self._config.batch: cli.header("# Allowed Groups to access app %s" % app_moniker) cli.header("# appgroup_id,group_id,group_name,dir_name,mfa") for count, group in enumerate(allowed_groups, start=1): association = group.get('resource_uri', {}).get('href') cli.print("{prefix1}{appgroup_id},{prefix2}{group_id},{name},{dir_name},{mfa}".format( prefix1=EAAItem.Type.ApplicationGroupAssociation.scheme, appgroup_id=association.split('/')[-1], prefix2=EAAItem.Type.Group.scheme, group_id=group.get('group').get('group_uuid_url'), name=group.get('group').get('name'), dir_name=group.get('group').get('dir_name'), mfa=group.get('enable_mfa') )) if not self._config.batch: cli.print("# %s groups configured to access application %s" % (count, app_moniker)) return allowed_groups
def create(self, raw_app_config): """ Create a new EAA application configuration. :param app_config: configuration as JSON string Note: the portal use the POST to create a new app with a minimal payload: {"app_profile":1,"app_type":1,"client_app_mode":1,"app_profile_id":"Fp3RYok1EeSE6AIy9YR0Dw", "name":"tes","description":"test"} We should do the same here """ app_config = json.loads(self.parse_template(raw_app_config)) logging.debug("Post Jinja parsing:\n%s" % json.dumps(app_config)) app_config_create = { "app_profile": app_config.get('app_profile'), "app_type": app_config.get('app_type', ApplicationAPI.Type.Hosted.value), "name": app_config.get('name'), "description": app_config.get('description') } newapp = self.post('mgmt-pop/apps', json=app_config_create) logging.info("Create app core: %s %s" % (newapp.status_code, newapp.text)) if newapp.status_code != 200: cli.exit(2) newapp_config = newapp.json() logging.info("New app JSON:\n%s" % newapp.text) app_moniker = EAAItem("app://{}".format(newapp_config.get('uuid_url'))) logging.info("UUID of the newapp: %s" % app_moniker) # Now we push everything else as a PUT self.put('mgmt-pop/apps/{applicationId}'.format( applicationId=app_moniker.uuid), json=app_config) # Sub-components of the application configuration definition # --- Connectors if app_config.get('agents'): self.attach_connectors(app_moniker, app_config.get('agents', [])) # IdP, Directories, Groups self.create_auth(app_moniker, app_config) # --- Access Control rules self.create_acl(app_moniker, app_config) # --- Other services # TODO: implement # URL based policies self.create_urlbasedpolicies(app_moniker, app_config) # At the end we reload the app entirely cli.print(json.dumps(self.load(app_moniker)))
def list(self, perf=False): """ Display the list of EAA connectors as comma separated CSV """ url_params = {'expand': 'true', 'limit': ConnectorAPI.LIMIT_SOFT} data = self.get('mgmt-pop/agents', params=url_params) connectors = data.json() total_con = 0 header = '#Connector-id,name,reachable,status,version,privateip,publicip,debug' format_line = "{scheme}{con_id},{name},{reachable},{status},{version},{privateip},{publicip},{debugchan}" if perf: header += ",CPU%,Mem%,Disk%,NetworkMbps,do_total,do_idle,do_active" format_line += ",{ts},{cpu},{mem},{disk},{network},{dialout_total},{dialout_idle},{dialout_active}" if perf: # Add performance metrics in the report perf_res_list = None with Pool(ConnectorAPI.POOL_SIZE) as p: perf_res_list = p.map( self.perf_system, [c.get('uuid_url') for c in connectors.get('objects', [])]) perf_res = dict(perf_res_list) cli.header(header) perf_latest = {} for total_con, c in enumerate(connectors.get('objects', []), start=1): if perf: perf_latest = perf_res.get(c.get('uuid_url'), {}) cli.print( format_line.format( scheme=EAAItem.Type.Connector.scheme, con_id=c.get('uuid_url'), name=c.get('name'), reachable=c.get('reach'), status=c.get('status'), version=(c.get('agent_version') or '').replace('AGENT-', '').strip(), privateip=c.get('private_ip'), publicip=c.get('public_ip'), debugchan='Y' if c.get('debug_channel_permitted') else 'N', ts=perf_latest.get('timestamp') or ConnectorAPI.NODATA, cpu=perf_latest.get('cpu_pct') or ConnectorAPI.NODATA, disk=perf_latest.get('disk_pct') or ConnectorAPI.NODATA, mem=perf_latest.get('mem_pct') or ConnectorAPI.NODATA, network=perf_latest.get('network_traffic_mbps') or ConnectorAPI.NODATA, dialout_total=perf_latest.get('dialout_total') or ConnectorAPI.NODATA, dialout_idle=perf_latest.get('dialout_idle') or ConnectorAPI.NODATA, dialout_active=perf_latest.get('dialout_active') or ConnectorAPI.NODATA)) cli.footer("Total %s connector(s)" % total_con)
def list_users(self, search=None): logging.info("SEARCH %s" % search) url_params = {'limit': 0} url = 'mgmt-pop/users' if search: url_params.update({'q': search}) resp = self.get(url, params=url_params) resj = resp.json() for u in resj.get('objects'): cli.print("{scheme}{uuid},{fn},{ln}".format( scheme=EAAItem.Type.User.scheme, uuid=u.get('uuid_url'), fn=u.get('first_name'), ln=u.get('last_name')))
def list(self): url_params = {'limit': MAX_RESULT} search_idp = self.get('mgmt-pop/idp', params=url_params) cli.header("#IdP-id,name,status,certificate,client,dp") idps = search_idp.json() for i in idps.get('objects', []): cli.print( "idp://{idp_id},{name},{status},{cert},{client},{dp}".format( idp_id=i.get('uuid_url'), name=i.get('name'), status=ApplicationAPI.Status(i.get('idp_status')).name, cert=(("crt://%s" % i.get('cert')) if i.get('cert') else '-'), client=('Y' if i.get('enable_access_client') else 'N'), dp=('Y' if i.get('enable_device_posture') else 'N')))
def rotate(self): """ Update an existing certificate. """ cert_moniker = EAAItem(self._config.certificate_id) cli.print("Rotating certificate %s..." % cert_moniker.uuid) api_url = 'mgmt-pop/certificates/{certificate_id}'.format( certificate_id=cert_moniker.uuid) get_resp = self.get(api_url) current_cert = get_resp.json() # cli.print(json.dumps(current_cert, sort_keys=True, indent=4)) payload = {} payload['name'] = current_cert.get('name') cli.print("Certificate CN: %s (%s)" % (current_cert.get('cn'), payload['name'])) payload['cert_type'] = current_cert.get('cert_type') with self._config.cert as f: payload['cert'] = f.read() with self._config.key as f: payload['private_key'] = f.read() if self._config.passphrase: payload['password'] = self._config.passphrase put_resp = self.put(api_url, json=payload, params={ 'expand': 'true', 'limit': 0 }) if put_resp.status_code == 200: new_cert = put_resp.json() cli.footer(("Certificate %s updated, %s application/IdP(s) " "have been marked ready for deployment.") % (cert_moniker.uuid, new_cert.get('app_count'))) if self._config.deployafter: self.deployafter(cert_moniker.uuid) else: cli.footer("Please deploy at your convience.") else: cli.print_error("Error rotating certificate, see response below:") cli.print_error(put_resp.text) cli.exit(2)
def list(self): url_params = {'expand': 'true', 'limit': 0} data = self.get('mgmt-pop/certificates', params=url_params) certificates = data.json() total_cert = 0 logging.info(json.dumps(data.json(), indent=4)) cli.print('#Certificate-ID,cn,type,expiration,days left') format_line = "{scheme}{uuid},{cn},{cert_type},{expiration},{days_left}" for total_cert, c in enumerate(certificates.get('objects', {}), start=1): cli.print( format_line.format(scheme=EAAItem.Type.Certificate.scheme, uuid=c.get('uuid_url'), cn=c.get('cn'), cert_type=CertificateAPI.Type( c.get('cert_type')).name, expiration=c.get('expired_at'), days_left=c.get('days_left'))) cli.footer("Total %s certificate(s)" % total_cert)
def status(self): """ Display status for a particular certificate. """ cert_moniker = EAAItem(self._config.certificate_id) cli.header("#App/IdP ID,name,status") app_api = ApplicationAPI(config) for app_id, app_name in self.findappsbycert(cert_moniker.uuid): # We don't need much info so expand=False to keep it quick app_config = app_api.load(app_id, expand=False) # cli.print(app_config) app_status = ApplicationAPI.Status(app_config.get('app_status')) cli.print("%s,%s,%s" % (app_id, app_name, app_status.name)) idp_api = IdentityProviderAPI(config) for idp_id, idp_name in self.findidpbycert(cert_moniker.uuid): idp_config = idp_api.load(idp_id) idp_status = ApplicationAPI.Status(idp_config.get('idp_status')) cli.print("%s,%s,%s" % (idp_id, idp_name, idp_status.name))
def swap(self, old_con_id, new_con_id, dryrun=False): """ Replace an EAA connector with another in: - application - directory Args: old_con_id (EAAItem): Existing connector to be replaced new_con_id (EAAItem): New connector to attach on the applications and directories dryrun (bool, optional): Enable dry run. Defaults to False. """ infos_by_conid = {} old_con = EAAItem(old_con_id) new_con = EAAItem(new_con_id) for c in [old_con, new_con]: connector_info = self.load(c) if not connector_info: cli.print_error("EAA connector %s not found." % c) cli.print_error("Please check with command 'akamai eaa connector'.") cli.exit(2) # Save the details for better infos_by_conid[c] = connector_info app_api = ApplicationAPI(self._config) app_processed = 0 cli.header("#Operation,connector-id,connector-name,app-id,app-name") for app_using_old_con, app_name, app_host in self.findappbyconnector(old_con): if dryrun: cli.print("DRYRUN +,%s,%s,%s,%s" % ( new_con, infos_by_conid[new_con].get('name'), app_using_old_con, app_name)) cli.print("DRYRUN -,%s,%s,%s,%s" % ( old_con, infos_by_conid[old_con].get('name'), app_using_old_con, app_name)) else: app_api.attach_connectors(app_using_old_con, [{'uuid_url': new_con.uuid}]) cli.print("+,%s,%s,%s,%s" % ( new_con, infos_by_conid[new_con].get('name'), app_using_old_con, app_name)) app_api.detach_connectors(app_using_old_con, [{'uuid_url': old_con.uuid}]) cli.print("-,%s,%s,%s,%s" % ( old_con, infos_by_conid[old_con].get('name'), app_using_old_con, app_name)) app_processed += 1 if app_processed == 0: cli.footer("Connector %s is not used by any application." % old_con_id) cli.footer("Check with command 'akamai eaa connector %s apps'" % old_con_id) else: cli.footer("Connector swapped in %s application(s)." % app_processed) cli.footer("Updated application(s) is/are marked as ready to deploy")
def list_directories(self): if self._directory_id: if self._config.users: if self._config.search_pattern and not self._config.batch: cli.header( "# list users matching %s in %s" % (self._config.search_pattern, self._directory_id)) self.list_users(self._config.search_pattern) elif self._config.groups: if self._config.search_pattern and not self._config.batch: cli.header("# list groups matching %s" % self._config.search_pattern) self.list_groups() else: resp = self.get("mgmt-pop/directories") if resp.status_code != 200: logging.error("Error retrieve directories (%s)" % resp.status_code) resj = resp.json() # print(resj) if not self._config.batch: cli.header("#dir_id,dir_name,status,user_count") total_dir = 0 for total_dir, d in enumerate(resj.get("objects"), start=1): cli.print( "{scheme}{dirid},{name},{status},{user_count}".format( scheme=EAAItem.Type.Directory.scheme, dirid=d.get("uuid_url"), name=d.get("name"), status=d.get("directory_status"), user_count=d.get("user_count"))) if total_dir == 0: cli.footer("No EAA Directory configuration found.") elif total_dir == 1: cli.footer("One EAA Directory configuration found.") else: cli.footer("%d EAA Directory configurations found." % total_dir)
def deployafter(self, certid): """ Trigger deployment request of all Apps and IdP using the certificate. """ app_api = ApplicationAPI(config) for app_id, app_name in self.findappsbycert(certid): cli.print("Deploying application %s (%s)..." % (app_name, app_id)) app_api.deploy(app_id) idp_api = IdentityProviderAPI(config) for idp_id, idp_name in self.findidpbycert(certid): cli.print("Deploying IdP %s (%s)..." % (idp_name, idp_id)) idp_api.deploy(idp_id) cli.print( "Deployment(s) in progress, it typically take 3 to 5 minutes") cli.print( "Use 'akamai eaa cert crt://%s status' to monitor the progress." % certid)
def synchronize_group(self, group_uuid): """ Synchronize a group within the directory Args: group_uuid (EAAItem): Group UUID e.g. grp://abcdef """ """ API call POST https://control.akamai.com/crux/v1/mgmt-pop/groups/16nUm7dCSyiihfCvMiHrMg/sync Payload: {} Response: {"response": "Syncing Group Sales Department"} """ group = EAAItem(group_uuid) retry_remaining = self._config.retry + 1 while retry_remaining > 0: retry_remaining -= 1 cli.print("Synchronizing %s [retry=%s]..." % (group_uuid, retry_remaining)) resp = self.get( 'mgmt-pop/directories/{dir_uuid}/groups/{group_uuid}'.format( dir_uuid=self._directory_id, group_uuid=group.uuid)) if resp.status_code != 200: logging.error("Error retrieve group info (%s)" % resp.status_code) cli.exit(2) group_info = resp.json() if group_info.get('last_sync_time'): last_sync = datetime.datetime.fromisoformat( group_info.get('last_sync_time')) delta = datetime.datetime.utcnow() - last_sync cli.print( "Last sync of group %s was @ %s UTC (%d seconds ago)" % (group_info.get('name'), last_sync, delta.total_seconds())) if delta.total_seconds() > self._config.mininterval: sync_resp = self.post( 'mgmt-pop/groups/{group_uuid}/sync'.format( group_uuid=group.uuid)) if sync_resp.status_code != 200: cli.print_error( "Fail to synchronize group (API response code %s)" % sync_resp.status_code) cli.exit(3) else: cli.print( "Synchronization of group %s (%s) successfully requested." % (group_info.get('name'), group)) break else: cli.print_error( "Last group sync is too recent, sync aborted. %s seconds interval is required." % self._config.mininterval) if retry_remaining == 0: cli.exit(2) else: sleep_time = last_sync + datetime.timedelta(seconds=self._config.mininterval) - \ datetime.datetime.utcnow() cli.print( "Sleeping for %s, press Control-Break to interrupt" % sleep_time) time.sleep(sleep_time.total_seconds())
def fetch_logs(self, exit_fn, stop_event): """ Fetch all logs until stop_event is set :param exit_fn: function to call upon SIGTERM and SIGINT :param stop_event: thread event, the fetch will operate in a loop until the event is set """ log_type = self.EventType(config.log_type) logging.info(log_type) signal.signal(signal.SIGTERM, exit_fn) signal.signal(signal.SIGINT, exit_fn) logging.info("PID: %s" % os.getpid()) logging.info("Poll interval: %s seconds" % EventLogAPI.PULL_INTERVAL_SEC) out = None try: if isinstance(self._output, str): logging.info("Output file: %s" % self._output) out = open(self._output, 'w+', encoding='utf-8') elif hasattr(self._output, 'write'): out = self._output if hasattr(out, 'reconfigure'): out.reconfigure(encoding='utf-8') if out.seekable(): start_position = out.tell() else: start_position = 0 while True: ets, sts = EventLogAPI.date_boundaries() s = time.time() logging.info("Fetching log[%s] from %s to %s..." % (log_type, sts, ets)) if log_type == log_type.ADMIN: if not config.batch and out.seekable(): cli.print( "#DatetimeUTC,AdminID,ResourceType,Resource,Event,EventType" ) out.write scroll_id = None while (True): drpc_args = { 'sts': str(sts), 'ets': str(ets), 'metrics': 'logs', 'es_fields': 'flog', 'limit': '1000', 'sub_metrics': 'scroll', 'source': SOURCE, } if scroll_id is not None: drpc_args.update({'scroll_id': str(scroll_id)}) scroll_id = self.get_logs(drpc_args, log_type, out) out.flush() if scroll_id is None: break if not config.tail: if not config.batch and out.seekable(): total_bytes = out.tell() - start_position cli.print("# Start: %s (EPOCH %d)" % (time.strftime( '%m/%d/%Y %H:%M:%S UTC', time.gmtime( sts / 1000.)), sts / 1000.)) cli.print("# End: %s (EPOCH %d)" % (time.strftime( '%m/%d/%Y %H:%M:%S UTC', time.gmtime( ets / 1000.)), ets / 1000.)) cli.print( "# Total: %s event(s), %s error(s), %s bytes written" % (self.line_count, self.error_count, total_bytes)) break else: elapsed = time.time() - s logging.debug("Now waiting %s seconds..." % (EventLogAPI.PULL_INTERVAL_SEC - elapsed)) stop_event.wait(EventLogAPI.PULL_INTERVAL_SEC - elapsed) if stop_event.is_set(): break except Exception: logging.exception("General exception while fetching EAA logs") finally: if out and self._output != sys.stdout: logging.debug("Closing output file...") out.close() logging.info("%s log lines were fetched." % self.line_count)
def process_command(self): """ Process command passed from the CLI. """ applications = list() appgroups = list() if self._config.application_id == '-': # nested if below because we don't do anything on create in this section if self._config.action != 'create': for line in sys.stdin: scanned_items = line.split(',') if len(scanned_items) == 0: logging.warning("Cannot parse line: %s" % line) continue try: scanned_obj = EAAItem(scanned_items[0]) if scanned_obj.objtype == EAAItem.Type.Application: applications.append(scanned_obj) elif scanned_obj.objtype == EAAItem.Type.ApplicationGroupAssociation: appgroups.append(scanned_obj) except EAAInvalidMoniker: logging.warning("Invalid application moniker: %s" % scanned_items[0]) else: logging.info("Single app %s" % config.application_id) applications.append(EAAItem(config.application_id)) logging.info("%s" % EAAItem(config.application_id)) if config.action == "deploy": for a in applications: self.deploy(a) cli.print("Application %s deployment requested, it may take a few minutes before it gets live." % a) elif config.action == "create": # new_config = json.load(sys.stdin) new_config = sys.stdin.read() self.create(new_config) elif config.action == "update": if len(applications) > 1: raise Exception("Batch operation not supported") app = applications[0] new_config = json.load(sys.stdin) self.update(app, new_config) cli.print("Configuration for application %s has been updated." % app) elif config.action == "delete": for a in applications: self.delete_app(a) elif config.action == "add_dnsexception": for a in applications: self.add_dnsexception(a) elif config.action == "del_dnsexception": for a in applications: self.del_dnsexception(a) elif config.action == 'viewgroups': for a in applications: self.loadgroups(a) elif config.action == 'delgroup': for ag in appgroups: self.delgroup(ag) elif config.action in ('attach', 'detach'): for a in applications: connectors = [] for c in set(config.connector_id): con_moniker = EAAItem(c) if con_moniker.objtype is not EAAItem.Type.Connector: raise TypeError("Invalid type of connector: %s" % c) connectors.append({"uuid_url": con_moniker.uuid}) if config.action == 'attach': self.attach_connectors(a, connectors) elif config.action == 'detach': self.detach_connectors(a, connectors) else: # view by default for a in applications: app_config = self.load(a) print(json.dumps(app_config))
def delete_app(self, app_moniker): deletion = self.delete('mgmt-pop/apps/{applicationId}'.format(applicationId=app_moniker.uuid)) if deletion.status_code == 200: cli.print("Application %s deleted." % app_moniker.uuid)