def get_element_by_id(element_id: int, platform_access: PlatformAccess, board: 'Board') -> Optional['Element']: """ Returns an Element by the given board and ID :param element_id: Element's ID to retrieve :param platform_access: PlatformAccess of a user with access rights :param board: Element's board :return: Element if found, otherwise None """ # Compose the request, execute it, and retrieve its data http_method = 'GET' detail = 'elements' url = compose_path(platform_access.get_platform_url(), 'boards', board.get_id(), 'elements', element_id) values = {} headers = platform_access.get_headers(http_method, url, values, detail) response = request(method=http_method, url=url, headers=headers) response.raise_for_status() response_data = response.json() href = response_data['element']['_links'][0]['href'] name = response_data['element']['caption'] element = Element(platform_access, name, href, board) return element
def update_agent_task(self, form: Dict) -> None: """ Upload an agent task to backend :param: form: an agent task to be uploaded :return None """ if not form: return None agent_task = form.get('agentTask', None) if not agent_task: return None agent_task_id = agent_task.get('id', None) agent_name = form['filename'].split('/')[0].split('_service')[0] agent_data_id, credentials = self.get_agent_data_id_by_agent_name(agent_name) if not agent_data_id: print(f'Agent id of {agent_name} is not found') return None backend_agent_task = get_agent_task_by_id(PlatformAccess(credentials[0], credentials[1]), agent_data_id=agent_data_id, agent_task_id=agent_task_id) if not backend_agent_task: backend_agent_task = "Wasn't found" # Get confirmation to do the operation if not self._get_confirmation(f'Current configuration: \n {json.dumps(backend_agent_task, indent=2)}\n' f'New configuration: \n {json.dumps(agent_task, indent=2)}\n' f'Update AgentTask configuration on the back-end?.. [y/N]\n'): return None # Update task update_agent_task_by_id(PlatformAccess(credentials[0], credentials[1]), agent_data_id=agent_data_id, task_file_name=form[FILENAME_KEY], agent_task_id=agent_task_id)
def _get_remote_agent_tasks(self) -> [Dict, None]: """ Returns all the AgentTasks a user has access to :return: None or a dict where a key is an agent_data_id and a value is a list of tasks """ agent_tasks = dict() local_configs = self._get_agents_configs_local() platform_access = PlatformAccess(LOGIN, PASSWORD) user_id = platform_access.get_user_id() # Execute GET-request to get AgentData response = make_request(platform_access=platform_access, http_method='GET', url_postfix='agentdata', detail='agentsData') if not response: return None data = response.json() if 'agentsData' in data: for agent in data['agentsData']: if user_id != str(agent['agentsUser']['id']): continue for config in local_configs.values(): agent_name = config['agentData']['name'] credentials = self.find_agent_credentials(agent_name) if not credentials[0]: continue # Add AgentTasks to the dictionary agent_tasks[f'{agent["id"]}'] = agent['agentTasks'], credentials, agent_name return agent_tasks
def get_agent_data_by_user_id(platform_access: PlatformAccess, user_id: int = None) -> Optional[Dict]: """ Returns agent's data :param platform_access: User's PlatformAccess, generated with login/password tuple :param user_id: Not to confuse with AgentData ID!.. :return: AgentData configuration if a request was successful, otherwise None """ if not user_id: user_id = platform_access.get_user_id() try: http_method = 'GET' detail = 'agentData' postfix = f'user/{user_id}/agentdata' response = make_request(platform_access=platform_access, http_method=http_method, url_postfix=postfix, detail=detail) response.raise_for_status() except (HTTPError, ConnectionError) as error: logger.error(OWN_ADAPTER_NAME, f'Error while updating database: {error}', response) return None else: logger.debug(OWN_ADAPTER_NAME, f'Successfully updated the database: {response}', response) if response: return response.json() return None
def put_agent_data(platform_access: PlatformAccess, data: Union[Dict, str], agent_data_id: int) -> Optional[Dict]: """ Updates the instance of AgentDataConfiguration on the back-end Reference: https://github.com/own-dev/own-agent-open/blob/master/docs/APIDescription.md#put-agentdataagentdataid :param platform_access: PlatformAccess for this Agent :param data: AgentData configuration in dictionary or JSON (str) format :param agent_data_id: ID of AgentData for the associated Agent on the back-end :return: Updated AgentData if request was successful, or None otherwise """ http_method = 'PUT' detail = 'agentData' postfix = f'agentdata/{agent_data_id}' data['agentData']['agentsUserId'] = platform_access.get_user_id() if isinstance(data, str): data = json.loads(data) try: response = make_request(platform_access=platform_access, http_method=http_method, detail=detail, url_postfix=postfix, data=data) response.raise_for_status() except (HTTPError, ConnectionError) as excpt: logger.error(OWN_ADAPTER_NAME, f'Error while updating database: {excpt}', response) return None else: logger.debug(OWN_ADAPTER_NAME, f'Successfully updated the database: {response}', response) if response: return response.json() return None
def _add_new_config(self, kwargs: Dict) -> None: """ Adds new AgentData to the back-end from the given config_data and config_path; later then updates the local configs with newly assigned IDs :param kwargs: {'config_data': Dict, 'config_path': str} config_path -- absolute path to the config to add config_data -- settings.conf as a dictionary :return: Nothing """ # Check if data exists config_data: Dict = kwargs.get('config_data', None) config_path: str = kwargs.get('config_path', None) if not (config_data and config_path): return # Get new credentials from env. vars agent_name = config_data['agentData']['name'] if not agent_name: return credentials = self.find_agent_credentials(agent_name) # Execute POST-request response = post_agent_data(platform_access=PlatformAccess(credentials[0], credentials[1]), data=config_data) if not response: return # Update local machine's config self._json_to_config(json_data=json.dumps(response), conf_filepath=config_path)
def post_agent_data(platform_access: PlatformAccess, data: Dict) -> Optional[Dict]: """ Creates new instance of AgentDataConfiguration on the back-end :param platform_access: :param data: agentData-like dict TODO: Check if it is dict or str :return: New JSON-data (with IDs) if the request was successful, otherwise None """ try: http_method = 'POST' detail = 'agentData' postfix = 'agentdata' data['agentData']['agentsUserId'] = platform_access.get_user_id() response = make_request(platform_access=platform_access, http_method=http_method, url_postfix=postfix, detail=detail, data=data) response.raise_for_status() except (HTTPError, ConnectionError) as excpt: logger.error(OWN_ADAPTER_NAME, f'Error while updating database: {excpt}', response) else: logger.debug(OWN_ADAPTER_NAME, f'Successfully updated the database: {response}', response) if response: return response.json() return None
def refresh_configs_menu_create(self) -> None: """ Refreshes AgentData::Create menu :return: None """ local_configs = self._get_agents_configs_local() # Generate menu-items (options) new_options = list() for config_filepath, config in local_configs.items(): agent_name = config['agentData']['name'] credentials = self.find_agent_credentials(agent_name) if not (credentials[0] and credentials[1]): continue agent_data = get_agent_data_by_user_id(PlatformAccess(credentials[0], credentials[1])) if agent_data: continue # Create a new menu-item config_data = json.loads(self.config_to_json(config_filepath)) new_options.append((f'{agent_name} agent', self._add_new_config, {'config_path': config_filepath, 'config_data': config_data})) new_options.append(BACK) # Set options for creation menu self.configs_menu_create.set_options(new_options)
def find_local_agent_name_by_id(self, agent_data_id: int, local_configs: Dict = None) -> Tuple[str, Tuple[str, str], Dict]: """ Find an agent name by id from local configs :param agent_data_id: an agent data id :param local_configs: a list containing local configs :return: the agent name, credentials and a local config of that agent """ if not local_configs: local_configs = self._get_agents_configs_local() # Different agents should have different credentials for config_filepath, config in local_configs.items(): # Read the config local_agent_name = config['agentData']['name'] credentials = self.find_agent_credentials(local_agent_name) if not credentials[0]: continue agent_data = get_agent_data_by_user_id(PlatformAccess(credentials[0], credentials[1])) if not agent_data: continue if agent_data['agentData']['id'] == agent_data_id: config['file_path'] = config_filepath return local_agent_name, credentials, config return '', ('', ''), {}
def _remove_agent_task(self, data: Dict) -> None: """ Removes the AgentTask by the given AgentDataID and AgentTaskID :param data: {'data_id': int, 'credentials': Tuple[str, str], 'task_id': int} data_id -- agent_data_id credentials -- a login and a password of an agent of the task task_id -- agent_task_id agent_name -- a name of the agent name :return None """ agent_data_id = data.get('data_id', None) agent_task_id = data.get('task_id', None) credentials = data.get('credentials', None) agent_name = data.get('agent_name', None) if not agent_task_id or not agent_data_id or not credentials: return # Get confirmation to do the operation if not self._get_confirmation(): return # Execute DELETE-request response = remove_agent_task_by_id(platform_access=PlatformAccess(credentials[0], credentials[1]), agent_data_id=agent_data_id, agent_task_id=agent_task_id) if not response: return # TODO: Remove IDs for the local file, if it persist on the machine def remove_local_form_of_agent_task(): """Removes AgentTask file from the local machine""" # Read the configs service_abs_path = os.path.join(self.services_abs_path, f'{agent_name}_service') agent_tasks_abs_path = os.path.join(service_abs_path, 'agent_tasks') for form in os.listdir(agent_tasks_abs_path): if not form.endswith('.json'): continue # Read AgentTask form_abs_path = os.path.join(agent_tasks_abs_path, form) with open(form_abs_path, 'r') as file: form_data: Dict = json.load(file).get('agentTask', None) # Check if it has ID current_form_id = form_data.get('id', None) if current_form_id and current_form_id == agent_task_id: form_data.pop('id') with open(form_abs_path, 'w') as file: json.dump({'agentTask': form_data}, file, indent=2) return remove_local_form_of_agent_task()
def put_comment(self, platform_access: PlatformAccess, comment: str = '', link: str = '') -> Optional[int]: """ Puts a comment to file :param platform_access: :param comment: comment text :param link: url of link, where to put comment :return: response status or none """ try: http_method = 'POST' detail = 'commentCreationResponse' url = link + '/comments' additional_headers = { 'Content-Type': 'application/json; charset=UTF-8' } payload = json.dumps({'fileComment': { 'message': comment }}, separators=(',', ':'), ensure_ascii=False) values = {} headers = platform_access.get_headers( http_method, url, values, detail, payload=payload, additional_headers=additional_headers) response = requests.post(url=url, headers=headers, data=payload.encode()) response_status = response.status_code response.raise_for_status() return response_status except requests.HTTPError as e: logger.exception( OWN_ADAPTER_NAME, f'Error: put comment {self._title} to {self.get_title()} failed. Error type: {e}', response) return None except Exception as e: logger.exception( OWN_ADAPTER_NAME, f'Error: put comment {self._title} to {self.get_title()} failed. Error type: {e}' ) return None
def refresh_configs_menu_update_or_remove(self, remove_flag: bool = False) -> None: """ Refreshes either AgentDataConfigurations::Update or AgentDataConfigurations::Remove menu :param remove_flag: False - find configs to be updated, True - find configs to be removed :return: None """ # Get the AgentData from the back-end local_configs = self._get_agents_configs_local() # To check if AgentData ID is unique; fool proof agent_data_ids = {} new_options = [] for config_filepath, config in local_configs.items(): local_agent_name = config['agentData']['name'] credentials = self.find_agent_credentials(local_agent_name) if not (credentials[0] and credentials[1]): continue agent_data = get_agent_data_by_user_id(PlatformAccess(credentials[0], credentials[1])) if not agent_data: continue # Create a new menu-item if not remove_flag: # Create a new menu-item for Update new_options.append((local_agent_name, self._update_config, {'agent_data': agent_data, 'agent_name': local_agent_name, 'credentials': credentials, 'config': config})) else: # Check if this AgentData ID is unique if agent_data['agentData']['id'] not in agent_data_ids: # TODO: Log if it wasn't the unique ID agent_data_ids[agent_data['agentData']['id']] = True # Create a new menu-item for Remove new_options.append((f'Id: {agent_data["agentData"]["id"]} :: Desc: ' f'{agent_data["agentData"]["description"]}', self._remove_config, {'agent_data': agent_data, 'credentials': credentials})) new_options.append(BACK) # Add options to either menu if not remove_flag: self.configs_menu_update.set_options(new_options) else: self.configs_menu_remove.set_options(new_options)
def get_agent_data_id_by_agent_name(self, agent_name: str) -> [Tuple[int, Tuple[str, str]], Tuple[None, Tuple[str, str]]]: """ Get an agent data id from a config file of an agent :param agent_name: a name of the config file :return: the agent data id and credentials of the agent """ credentials = self.find_agent_credentials(agent_name) if not credentials[0]: return None, credentials agent_data = get_agent_data_by_user_id(PlatformAccess(credentials[0], credentials[1])) if not agent_data: return None, credentials return agent_data['agentData']['id'], credentials
def get_board_by_id(board_id: str, platform_access: PlatformAccess, need_name: bool = True) -> Optional['Board']: """ Returns a board by its ID :param board_id: Board's ID on the platform/back-end :param platform_access: PlatformAccess object of a(n) user/agent which has access to this board?.. :param need_name: Either to force-get (via request) board's name or not :return: Board instance for the given ID, or None if a board can't be created """ name = '' if need_name: http_method = 'GET' detail = 'boards' url = f'{platform_access.get_platform_url()}/boards/{board_id}' values = {} headers = platform_access.get_headers(http_method, url, values, detail) response = None try: response = request(method=http_method, url=url, headers=headers) response.raise_for_status() except HTTPError as http_error: logger.exception( OWN_ADAPTER_NAME, f'Could not perform name extraction: {http_error}', response) except ConnectionError as connect_error: logger.exception( OWN_ADAPTER_NAME, f'Could not perform name extraction, connection-error: {connect_error}', response) else: response_data = response.json() name = response_data['board']['name'] # TODO: Check if the board exists?.. I.e., ID is correct, and the PlatformAccess-object can get it board = Board(platform_access, name, board_id) return board
def __init__(self, platform_access: PlatformAccess, name: str = '', identifier: str = '', board: 'Board' = None, pos_x: int = None, pos_y: int = None, size_x: int = None, size_y: int = None, last_processing_time: datetime = DEFAULT_PROCESSING_DATETIME, agent_task_id: int = None, agent_data_id: int = None): """ :param platform_access: PlatformAccess of a user who has access rights to it [board, i.e., boards] :param name: Element's caption :param identifier: Element's ID :param board: The board on which the element is on :param pos_x: Element's horizontal position, starting with 1 :param pos_y: Element's vertical position, starting with 1 :param size_x: Element's width :param size_y: Element's height :param last_processing_time: The last time this element was processed :param agent_task_id: AgentTask's ID that is assigned to the element (if some) :param agent_data_id: AgentData's ID of an agent that is assigned to the element (if there is some) """ self.__platform_access = platform_access self.__name = name if PREFIX and PREFIX in identifier: identifier = identifier[len(PREFIX) + 1:] self.__url = platform_access.get_platform_url() + identifier self.__id = identifier self.__board = board self.__last_processing_time = last_processing_time self.__pos_x = pos_x self.__pos_y = pos_y self.__size_x = size_x self.__size_y = size_y self.__agent_task_id = agent_task_id self.__agent_data_id = agent_data_id
def __get_platform_access(self) -> [PlatformAccess, None]: """Returns PlatformAccess with valid agent's credentials (environ), None otherwise""" if not self.platform_access: try: login = os.environ[f'OWN_{self.name.upper()}_AGENT_LOGIN'] password = os.environ[ f'OWN_{self.name.upper()}_AGENT_PASSWORD'] self.platform_access = PlatformAccess(login, password) except KeyError as key_error: exception( self.name, f'Failed get credentials for {self.name}-agent. Error message: {str(key_error)}' ) except Exception as err: error( self.name, f'Some error occurred while establishing connection to the platform: {err}' ) return self.platform_access
def _remove_config(self, dict_with_data: Dict) -> None: """ Removes AgentData from the back-end :param dict_with_data: {'agent_data': Dict, 'credentials': Tuple[str, str]} agent_data -- agent_data stored on the backend credentials -- a login and a password of that agent :return: None """ agent_data = dict_with_data['agent_data']['agentData'] credentials = dict_with_data['credentials'] agent_data_id = agent_data['id'] backend_config_readable = self.config_to_readable(agent_data) if not self._get_confirmation(f'Agent config to be removed: \n {backend_config_readable}' f' \nAre you sure you want to ' f'remove Agent with ID = {agent_data_id}? [y/N]'): return res = delete_agent_data(platform_access=PlatformAccess(credentials[0], credentials[1]), agent_data_id=agent_data_id) if not res: print('Agent was not deleted')
def _change_config_status(self, kwargs: Dict) -> None: """ Changes config's status on the back-end :param kwargs: {'agent_name': str, 'new_status': str, 'agent_data': Dict} agent_name -- a name of an agent credentials -- a login and a password of that agent agent_data -- agent data of the agent :return: Nothing """ agent_name = kwargs.get('agent_name', None) new_status = kwargs.get('new_status', None) agent_data = kwargs.get('agent_data', None) if not (agent_name and new_status and agent_data): return agent_data_id = agent_data['id'] # Fix the status to its real value # Input parameter can be 1, or 2, or 3 # to escape "if not new_status:" when it's 0 (and not None) new_status -= 1 agent_name, credentials, config = self.find_local_agent_name_by_id(agent_data_id) if not agent_name: print(f'You don\'t have access to that agent') return # Firstly, get confirmation to do te operation if not self._get_confirmation(): return agent_data['status'] = new_status # Update the back-end response = put_agent_data(platform_access=PlatformAccess(credentials[0], credentials[1]), data={'agentData': agent_data}, agent_data_id=agent_data_id) if response: print(f'{agent_name}\'s status was set to {STATUS[str(new_status)]}.') else: print(f'{agent_name}\'s status was not updated.')
def refresh_configs_menu_list(self) -> None: """ Lists all the agent-services on the local machine Allows to Run or Stop them :return: Nothing """ # Get AgentData configs from the back-end agent_datas = get_all_agents_data(PlatformAccess(LOGIN, PASSWORD)) # Generate all the menu-items menu_items = list() for agent in agent_datas: # Create a new menu-item agent_name = f"{agent['agentsUser']['firstName']}" new_entry_name = f"{agent_name:<16} (id={agent['id']:<4})\t{agent['status']}" backend_config_readable = self.config_to_readable(agent) if 'agentTasks' in backend_config_readable: backend_config_readable.pop('agentTasks', None) new_entry = Menu(title=new_entry_name, options=[ ('Set ACTIVE', self._change_config_status, {'agent_name': agent_name.lower(), 'agent_data': agent, 'new_status': 1}), ('Set BEING_TESTED', self._change_config_status, {'agent_name': agent_name.lower(), 'agent_data': agent, 'new_status': 2}), ('Set INACTIVE', self._change_config_status, {'agent_name': agent_name.lower(), 'agent_data': agent, 'new_status': 3}), (f'AGENT CONFIG: \n{json.dumps(backend_config_readable, indent=2)}', lambda: None, None), BACK ]) menu_items.append(new_entry) # Generate options for AgentTasks::List new_options = [(config_item.title, config_item.open, None) for config_item in menu_items] new_options.append(BACK) self.configs_menu_list.set_options(new_options)
def _create_new_agent_task(self, task_filepath: str) -> None: """ Creates a new AgentTask on the back-end :param task_filepath: a path to a file with a task :return: None """ # Check if data persists if not task_filepath: return # Get confirmation to do the operation if not self._get_confirmation(): return agent_name = task_filepath.split('/')[0].split('_service')[0] agent_data_id, credentials = self.get_agent_data_id_by_agent_name(agent_name) if not agent_data_id: print(f'Agent id of {agent_name} is not found') return # Execute POST-request upload_or_update_agent_task_from_file(platform_access=PlatformAccess(credentials[0], credentials[1]), agent_data_id=agent_data_id, file_name=task_filepath, force_update=True)
def make_request(platform_access: PlatformAccess, http_method: str, url_postfix: Optional[str], detail: str, data: Dict = None, values: Dict = None, logger_name: str = None) -> Optional[Response]: """ Makes request according the given parameters :param platform_access: :param http_method: {GET|POST|PUT|DELETE} :param url_postfix: :param detail: :param data: Actual payload :param values: # TODO: Check PlatformAccess::get_headers() :param logger_name: Name for logging, default is OWN_ADAPTER_NAME :return: requests.Response """ # Set default values if values is None: values = dict() if not logger_name: logger_name = OWN_ADAPTER_NAME try: # Generate the payload payload = json.dumps( data, separators=(',', ':'), sort_keys=True, ensure_ascii=False).encode('utf-8') if data else '' # Generate URL url = f'{platform_access.get_platform_url()}' if url_postfix: url += f'/{url_postfix}' debug(logger_name, f'URL: ({http_method}) {url}') # Generate headers if http_method != 'DELETE': headers = platform_access.get_headers(method=http_method, url=url, values=values, detail=detail, payload=payload, additional_headers={}) else: headers = platform_access.get_headers(method=http_method, url=url, values=values, detail=detail) except ConnectionError as con_error: errno, strerror = con_error.args exception(logger_name, f'Connection error ({errno}): {strerror}') return None except Exception: exception( logger_name, f'Unexpected error while getting headers: {sys.exc_info()[0]}') return None else: debug(logger_name, f'Successfully generated headers:\n{headers}') try: # Make a request if http_method != 'DELETE': response = request(url=url, method=http_method, headers=headers, data=payload) else: response = request(url=url, method=http_method, headers=headers) except HTTPError as http_error: exception( logger_name, f'HTTP error ({http_error.errno}) while making request: {http_error}' ) return None except ConnectionError as con_error: exception( logger_name, f'HTTP error ({con_error.errno}) while making request: {con_error}' ) return None except URLError as url_error: exception(logger_name, f'URL error, failed to reach server: {url_error.reason}') return None else: debug( logger_name, f'Successfully made a request, code {response.status_code}, {response}' ) return response
def _update_config(self, data: Dict) -> None: """ Updates AgentData on the back-end :param data: {'agent_data': Dict, 'agent_name': str, 'credentials': Tuple[str, str], 'config': Dict} agent_data -- agent_data stored on the backend agent_name -- a name of an agent to be updated credentials -- a login and a password of that agent config -- a local config to be uploaded :return: None """ backend_config = data['agent_data']['agentData'] agent_name = data['agent_name'] credentials = data['credentials'] data = data['config'] local_config_data = data['agentData'] # Update the local config with IDs from the back-end new_config_data_with_ids = local_config_data new_config_data_with_ids[ID_KEY] = backend_config[ID_KEY] def find_subs_plan_by_period_name(subscription_plans: List[Dict], period_val: PeriodicalPlan) -> Optional[Dict]: """ Searches for the needed subscription plan in the given ones by name Reference: See Agent Data section in docs/APIDescription.md :param subscription_plans: Subscription plans to search in :param period_val: "DAY", "WEEK", or "MONTH" :return: If found, returns a subscription plan data for the given period, otherwise, None """ if not (subscription_plans and period_val): return None required_subscription_plan = None for plan in subscription_plans: if plan[SUBSCRIPTION_INTERVAL_KEY] == period_val: required_subscription_plan = plan break return required_subscription_plan # Adds IDs for the new config parsed from the one form back-end # TODO: After production server's updated for Organization payments, add its handling backend_subscriptions = backend_config[SUBSCRIPTION_PLANS_KEY] for idx, period in enumerate(PERIODS): back_subs_plan = find_subs_plan_by_period_name(backend_subscriptions, period) if not back_subs_plan: continue # Update subscription interval ID back_interval_id = back_subs_plan[ID_KEY] new_config_data_with_ids[SUBSCRIPTION_PLANS_KEY][idx][ID_KEY] = \ back_interval_id # Update subscription plan pricing ID back_plan_pricing_id = back_subs_plan[PLAN_PRICINGS_KEY][0][ID_KEY] # For euro-currency new_config_data_with_ids[SUBSCRIPTION_PLANS_KEY][idx][PLAN_PRICINGS_KEY][0][ID_KEY] = \ back_plan_pricing_id # Get confirmation to do the operation backend_config_readable = self.config_to_readable(backend_config) old_config = json.dumps(backend_config_readable, indent=2) new_config = json.dumps(local_config_data, indent=2) if not self._get_confirmation(f'Current configuration: \n {old_config}\n' f'New configuration: \n {new_config}\n' f'Update {agent_name}\'s configuration on the back-end?.. [y/N]\n'): return # Actually update it res = put_agent_data(platform_access=PlatformAccess(credentials[0], credentials[1]), data=data, agent_data_id=backend_config['id']) if not res: print('Agent was not updated')