├── .gitignore ├── LICENSE ├── README.md ├── agent ├── Dockerfile ├── README.md └── agent.py └── server ├── Dockerfile ├── db.py ├── ecs_api.py ├── new_relic_url_generator.py ├── run_server.py ├── server.py └── settings.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | 61 | #Ipython Notebook 62 | .ipynb_checkpoints 63 | 64 | # Pycharm 65 | .idea/* 66 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ECS ID Mapper 2 | 3 | *Please note, this is still in active development and is of beta quality* 4 | 5 | ### Overview 6 | 7 | ECS ID Mapper is a tool for tracking the relationship between an ECS task ID and a Docker container ID. The tool 8 | also records relevant information about a docker container which can be useful for other tools to query. It has two 9 | components, an agent that runs on the EC2 host in the ECS cluster and a server. 10 | 11 | 12 | ### Server Methods 13 | The server has a basic REST interface that behaves as follows: 14 | 15 | 16 | `/query/container_id/` 17 | Get the corresponding task id for a given container id. Returns a text string if found, 404 if not found. 18 | 19 | `/query/container_id//_all` 20 | Get all known attributes of a container based on container id. Returns JSON if found, 404 if not found. 21 | 22 | `/query/container_id//cadvisor` 23 | Get the cadvisor URL for a given container by container_id. Append `?redir=true` to vault_get a 302 redirect to the cAdvisor URL. 24 | 25 | `/query/task_id/` 26 | Get the corresponding container id for a given task id. Returns a text string if found, 404 if not found. 27 | 28 | `/query/container_id//_all` 29 | Get all known attributes of a container based on task id. Returns JSON if found, 404 if not found. 30 | 31 | `/query/task_id//cadvisor` 32 | Get the cadvisor URL for a given container by task_id. Append `?redir=true` to vault_get a 302 redirect to the URL. 33 | 34 | `/query/task_id//graylog` 35 | Get the graylog URL for a given container by task_id. Append `?redir=true` to vault_get a 302 redirect to the URL. 36 | 37 | `/query/task_id//newrelic` 38 | Get the new relic URL for the application instance of the corresponding new relic application. 39 | Append `?redir=true` to vault_get a 302 redirect to the URL. 40 | 41 | `/query/service///newrelic` 42 | Get the new relic URL for the application represented by the ECS service. 43 | Append `?redir=true` to vault_get a 302 redirect to the URL. 44 | 45 | `/query/service///_all` 46 | Get attributes for all tasks in a serivce. 47 | 48 | `/health` 49 | Internal health check of the server, returns 200 if the server is healthy. 50 | 51 | 52 | ### Architecture 53 | The agent runs on the EC2 instances that run containers (aka ECS instances) periodically polls the local ECS agent 54 | on port 51678 for information about the containers it is managing on that host. Also polls the EC2 instance metadata 55 | service for information about the instance itself. It then compares the containers reported by ECS agent with its internal 56 | state of known containers and reports any new containers to the server. The agent also reports the changes in state of 57 | containers as events regardless of the state being known. 58 | 59 | The server stores information about containers it receives in [AWS SDB](https://aws.amazon.com/simpledb/), and also does 60 | some processing to generate URLs for monitoring tools for each container it gets reports of. 61 | 62 | The server provides REST APIs to users who want to query information in the database. 63 | 64 | The server runs in a Docker container. Each container is essentially stateless so multiple 65 | containers can be run behind a load balancer for increased availability and throughput. 66 | 67 | ### Requirements 68 | * Docker compatible runtime 69 | * [Hashicorp Vault](https://www.vaultproject.io) (for storing secrets) 70 | * [AWS SimpleDB](https://aws.amazon.com/simpledb/) 71 | * [AWS EC2 Container Service](https://aws.amazon.com/ecs/) 72 | * python 2.7 and [python requests](http://docs.python-requests.org/en/master/) (for the agent) 73 | 74 | -------------------------------------------------------------------------------- /agent/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.3 2 | RUN apk add --update python 3 | RUN apk add --update --virtual build-dependencies \ 4 | python-dev \ 5 | py-pip \ 6 | && pip install requests==2.9.1 \ 7 | && pip install docker-py==1.8.0 \ 8 | && apk del build-dependencies 9 | ADD . /src/ 10 | ENV log_level INFO 11 | CMD ["python", "/src/agent.py"] 12 | -------------------------------------------------------------------------------- /agent/README.md: -------------------------------------------------------------------------------- 1 | ## ECS ID Mapper Agent 2 | 3 | The agent runs on the ECS Container Instance and queries the ECS agent and the EC2 Metadata services for information. 4 | The container needs to run in host networking mode so it can access the loopback interface of the Container Instance 5 | (which is where the ECS Agent listens). 6 | 7 | ### Usage 8 | The agent is designed to be run in a container using host network mode. It will also need access to a volume 9 | that maps to the docker socket (`/var/run/docker.sock`). See this docker run command for example: 10 | 11 | 12 | ``` 13 | docker run -d -v /var/run/docker.sock:/var/run/docker.sock --name="ecs_id_mapper_agent" --restart="always" --memory="64m" --net=host -e "endpoint=http://" 14 | ``` 15 | -------------------------------------------------------------------------------- /agent/agent.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import logging 3 | import time 4 | import hashlib 5 | import copy 6 | from os import path, getenv 7 | import json 8 | from socket import gethostname 9 | from sys import exit 10 | import subprocess 11 | from docker import Client 12 | 13 | 14 | class ECSIDMapAgent(): 15 | def __init__(self, server_endpoint, log_level): 16 | self.id_map = {} 17 | self.new_id_map = {} 18 | self.server_endpoint = server_endpoint 19 | self.logger = self._setup_logger(log_level) 20 | self.backoff_time = 2 21 | self.current_backoff_time = self.backoff_time 22 | self.current_retry = 0 23 | self.max_retries = 2 24 | self.hostname = gethostname() 25 | self.instance_ip = self.get_instance_metadata('local-ipv4') # set these at object constr. as they don't change 26 | self.instance_id = self.get_instance_metadata('instance-id') 27 | self.instance_type = self.get_instance_metadata('instance-type') 28 | self.instance_az = self.get_instance_metadata('placement/availability-zone') 29 | self.docker_client = Client(base_url='unix://var/run/docker.sock', version='1.21') 30 | 31 | @staticmethod 32 | def _setup_logger(log_level): 33 | logger = logging.getLogger('ecs_id_mapper_agent') 34 | logger.setLevel(log_level.upper()) 35 | logger.propagate = False 36 | stderr_logs = logging.StreamHandler() 37 | stderr_logs.setLevel(getattr(logging, log_level)) 38 | stderr_logs.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')) 39 | logger.addHandler(stderr_logs) 40 | return logger 41 | 42 | def _retry(self): 43 | self.logger.debug('backoff: {} retry: {} max_retry {}'.format(self.current_backoff_time, 44 | self.current_retry, self.max_retries)) 45 | if self.current_retry >= self.max_retries: 46 | self.logger.info('Max _retry reached. Aborting') 47 | self.current_retry = 0 48 | self.current_backoff_time = self.backoff_time 49 | return False 50 | else: 51 | self.current_retry += 1 52 | self.logger.info('Sleeping for {} seconds'.format(str(self.current_backoff_time))) 53 | time.sleep(self.current_backoff_time) 54 | self.current_backoff_time **= 2 55 | return True 56 | 57 | def _http_connect(self, url, timeout=1): 58 | self.logger.debug('Making connection to: {}'.format(url)) 59 | while True: 60 | try: 61 | r = requests.get(url, timeout=timeout) 62 | return r 63 | except requests.exceptions.ConnectionError: 64 | self.logger.error('Connection error accessing URL {}'.format(str(url))) 65 | if not self._retry(): 66 | return None 67 | except requests.exceptions.Timeout: 68 | self.logger.error( 69 | 'Connection timeout accessing URL {}. Current timeout value {}'.format(url, str(timeout))) 70 | if not self._retry(): 71 | return None 72 | 73 | def get_instance_metadata(self, path): 74 | self.logger.info('Checking instance metadata for {}'.format(path)) 75 | metadata = self._http_connect('http://169.254.169.254/latest/meta-data/{}'.format(path)) 76 | if metadata: 77 | return metadata.text 78 | else: 79 | return "" 80 | 81 | @staticmethod 82 | def get_container_ports(container_id): 83 | try: 84 | cmd = ["/usr/bin/docker", "port", container_id[:12]] 85 | p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 86 | output, errors = p.communicate() 87 | if errors or len(output) < 1: 88 | return "0", "0" 89 | cport, hport = output.split("-") 90 | cport = cport.split('/')[0] 91 | hport = hport.split(':')[1].strip() 92 | return cport, hport 93 | except (OSError, ValueError): 94 | return "0", "0" 95 | 96 | def get_ecs_agent_tasks(self): 97 | self.logger.info('Requesting data from ECS agent') 98 | ecs_agent_tasks_response = self._http_connect('http://127.0.0.1:51678/v1/tasks') 99 | ecs_agent_metadata_response = self._http_connect('http://127.0.0.1:51678/v1/metadata') 100 | 101 | if ecs_agent_tasks_response and ecs_agent_metadata_response: 102 | ecs_agent_tasks = ecs_agent_tasks_response.json() 103 | ecs_agent_metadata = ecs_agent_metadata_response.json() 104 | else: 105 | ecs_agent_tasks = None 106 | ecs_agent_metadata = None 107 | return False 108 | id_map = {} 109 | cluster_name = ecs_agent_metadata['Cluster'] 110 | ecs_agent_version = ecs_agent_metadata['Version'] 111 | for task in ecs_agent_tasks['Tasks']: 112 | task_id = str(task['Arn'].split(":")[-1][5:]) 113 | desired_status = str(task['DesiredStatus']) 114 | known_status = str(task['KnownStatus']) 115 | task_name = str(task['Family']) 116 | task_version = str(task['Version']) 117 | for container in task['Containers']: 118 | docker_id = str(container['DockerId']) 119 | if desired_status == "RUNNING": 120 | container_port, instance_port = self.get_container_ports(docker_id) 121 | else: 122 | container_port, instance_port = "0", "0" 123 | container_name = str(container['Name']) 124 | pkey = hashlib.sha256() 125 | pkey.update(docker_id) 126 | pkey.update(task_id) 127 | pkey.update(desired_status) 128 | id_map[pkey.hexdigest()] = {'container_id': docker_id, 129 | 'container_name': container_name, 130 | 'container_port': container_port, 131 | 'task_id': task_id, 132 | 'task_name': task_name, 133 | 'task_version': task_version, 134 | 'instance_port': instance_port, 135 | 'instance_ip': self.instance_ip, 136 | 'instance_id': self.instance_id, 137 | 'instance_type': self.instance_type, 138 | 'instance_az': self.instance_az, 139 | 'desired_status': desired_status, 140 | 'known_status': known_status, 141 | 'host_name': self.hostname, 142 | 'cluster_name': cluster_name, 143 | 'ecs_agent_version': ecs_agent_version, 144 | 'sample_time': time.time()} 145 | # Update internal state 146 | self.new_id_map = copy.deepcopy(id_map) 147 | 148 | def report_event(self, event_id, action): 149 | for id in event_id: 150 | self.logger.info('Reporting new container event. Action {}. Event id: {}'.format(action, id)) 151 | headers = {'Content-type': 'application/json', 'Accept': 'text/plain'} 152 | while True: 153 | try: 154 | r = requests.post(path.join(self.server_endpoint, 'report/event'), 155 | headers=headers, 156 | data=json.dumps({'event_id': id, 'event': action, 'timestamp': time.time()})) 157 | self.logger.debug("HTTP response: " + str(r.status_code)) 158 | break 159 | except requests.exceptions.ConnectionError: 160 | self.logger.info('Unable to connect to server endpoint. Sleeping for {} seconds'.format( 161 | str(self.current_backoff_time))) 162 | if not self._retry(): 163 | break 164 | 165 | def report_map(self): 166 | self.logger.info('Reporting current id map') 167 | headers = {'Content-type': 'application/json', 'Accept': 'text/plain'} 168 | while True: 169 | try: 170 | r = requests.post(path.join(self.server_endpoint, 'report/map'), 171 | headers=headers, data=json.dumps(self.id_map)) 172 | self.logger.debug("HTTP response: " + str(r.status_code)) 173 | break 174 | except requests.exceptions.ConnectionError: 175 | self.logger.info('Unable to connect to server endpoint. Sleeping for {} seconds'.format( 176 | str(self.current_backoff_time))) 177 | if not self._retry(): 178 | break 179 | 180 | def compare_hash(self): 181 | self.logger.info('Comparing known state to current state') 182 | containers_added = set(self.new_id_map.keys()) - set(self.id_map.keys()) 183 | containers_removed = set(self.id_map.keys()) - set(self.new_id_map.keys()) 184 | if len(containers_added) > 0: 185 | self.logger.info('Containers added {}'.format(containers_added)) 186 | self.report_event(containers_added, 'added') 187 | if len(containers_removed) > 0: 188 | self.logger.info('Containers removed {}'.format(containers_removed)) 189 | self.report_event(containers_removed, 'removed') 190 | if len(containers_removed) > 0 or len(containers_added) > 0: 191 | self.id_map = copy.deepcopy(self.new_id_map) 192 | self.report_map() 193 | else: 194 | self.logger.info('No container actions to report') 195 | 196 | def run(self): 197 | """ 198 | Blocking method to run agent 199 | :return: 200 | """ 201 | self.logger.info('Starting agent') 202 | for event in self.docker_client.events(decode=True): 203 | self.logger.debug(str(event)) 204 | if event['status'] == 'start' or event['status'] == 'die': 205 | agent.get_ecs_agent_tasks() 206 | agent.compare_hash() 207 | 208 | 209 | if __name__ == '__main__': 210 | server_endpoint = getenv('endpoint', None) 211 | log_level = getenv('log_level', 'INFO') 212 | 213 | if not server_endpoint: 214 | print "Error: you must specify server endpoint as EVAR 'endpoint'" 215 | exit(1) 216 | 217 | agent = ECSIDMapAgent(server_endpoint, log_level) 218 | 219 | # Reduce verbosity of requests logging 220 | logging.getLogger("requests").setLevel(logging.WARNING) 221 | logging.getLogger("urllib3").setLevel(logging.WARNING) 222 | 223 | # Start the agent 224 | agent.run() 225 | -------------------------------------------------------------------------------- /server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:14.04 2 | RUN apt-get update && apt-get -y install python-setuptools build-essential python-dev libffi-dev libssl-dev 3 | RUN easy_install pip 4 | RUN pip install urllib3[secure]==1.15.1 5 | RUN pip install Flask==0.10.1 6 | RUN pip install boto==2.39.0 7 | RUN pip install hvac==0.2.10 8 | RUN pip install tornado==4.3 9 | RUN pip install requests==2.9.1 10 | RUN pip install boto3==1.3.0 11 | EXPOSE 5001 12 | ADD . /src/ 13 | CMD ["python", "/src/run_server.py"] 14 | -------------------------------------------------------------------------------- /server/db.py: -------------------------------------------------------------------------------- 1 | import boto.sdb 2 | from boto.exception import SDBResponseError 3 | from itertools import islice 4 | import settings 5 | import logging 6 | 7 | logger = logging.getLogger('ecs_id_mapper') 8 | 9 | conn = boto.sdb.connect_to_region(settings.simpledb_aws_region, 10 | aws_access_key_id=settings.aws_id, 11 | aws_secret_access_key=settings.aws_secret_key) 12 | 13 | # maintain state of existing domain objects 14 | domains = {} 15 | 16 | 17 | def _get_domain(domain): 18 | """ 19 | Check if I have the domain object in local state, if not create one, and if the domain does 20 | not exist, make a create_domain call and return the domain obj. 21 | :param domain: str. name of the domain to get 22 | :return: domain obj. 23 | """ 24 | assert type(domain) == str 25 | try: 26 | _dom = domains[domain] 27 | except KeyError: 28 | try: 29 | _dom = conn.get_domain(domain) 30 | except SDBResponseError as e: 31 | if str(e.error_code) == 'NoSuchDomain': 32 | logger.info('Domain {dom} does not exist, creating...'.format(dom=domain)) 33 | # The domain doesn't exist 34 | # create the domain 35 | conn.create_domain(domain) 36 | # get the domain object 37 | _dom = conn.get_domain(domain) 38 | # store domain obj for later use 39 | domains[domain] = _dom 40 | else: 41 | # something else happened that we don't know how to handle 42 | raise 43 | # finally, return domain 44 | return _dom 45 | 46 | 47 | def _batch_items(items, increment=25): 48 | """ 49 | generator that returns a dictionary of a specified size of keys 50 | :param items: dict. dictionary to batch 51 | :param increment: int. qty of keys per batch 52 | :return: dict. batched results 53 | """ 54 | assert type(items) == dict 55 | assert type(increment) == int 56 | start = 0 57 | end = increment 58 | incr = increment 59 | r = {} 60 | while True: 61 | for k,v in islice(items.iteritems(), start, end): 62 | r[k] = v 63 | if len(r) > 0: 64 | yield r 65 | start = end+1 66 | end += incr 67 | r = {} 68 | else: 69 | break 70 | 71 | 72 | def put(key, value, domain, replace=False): 73 | dom = _get_domain(domain) 74 | return dom.put_attributes(key, value, replace=replace) 75 | 76 | 77 | def batch_put(items, domain): 78 | dom = _get_domain(domain) 79 | for items in _batch_items(items): 80 | dom.batch_put_attributes(items) 81 | return True 82 | 83 | 84 | def get(key, domain, consistent_read=True): 85 | dom = _get_domain(domain) 86 | return dom.get_item(key, consistent_read=consistent_read) 87 | 88 | 89 | def get_all_dom(domain): 90 | dom = _get_domain(domain) 91 | return dom.select('select * from `{dom}`'.format(dom=domain)) 92 | 93 | 94 | def search_domain(query, domain): 95 | dom =_get_domain(domain) 96 | return dom.select(query) 97 | 98 | 99 | def del_key(key, domain): 100 | dom = _get_domain(domain) 101 | return dom.delete_item(get(key, domain)) 102 | 103 | 104 | def list_domains(): 105 | return conn.get_all_domains() 106 | 107 | 108 | def create_domain(domain): 109 | return conn.create_domain(domain) 110 | 111 | -------------------------------------------------------------------------------- /server/ecs_api.py: -------------------------------------------------------------------------------- 1 | import settings 2 | import boto3 3 | import botocore.exceptions 4 | import logging 5 | 6 | logger = logging.getLogger('ecs_id_mapper') 7 | 8 | client = boto3.client('ecs', 9 | aws_access_key_id=settings.aws_id, 10 | aws_secret_access_key=settings.aws_secret_key, 11 | region_name=settings.simpledb_aws_region 12 | ) 13 | 14 | 15 | def get_task_ids_from_service(service_name, cluster_name): 16 | logger.info('Making call to AWS API for service {} and cluster {}'.format(service_name, cluster_name)) 17 | try: 18 | tasks = client.list_tasks(cluster=cluster_name, serviceName=service_name)['taskArns'] 19 | except botocore.exceptions.ClientError: 20 | logger.info('ECS service {} not found'.format(service_name)) 21 | raise Exception('ECS service not found') 22 | logger.debug(tasks) 23 | tlist = [] 24 | if len(tasks) >= 1: 25 | for task in tasks: 26 | tlist.append(task.split('/')[1]) 27 | return tlist 28 | 29 | -------------------------------------------------------------------------------- /server/new_relic_url_generator.py: -------------------------------------------------------------------------------- 1 | import time 2 | import db 3 | import requests 4 | import logging 5 | import settings 6 | import ecs_api 7 | 8 | logger = logging.getLogger('ecs_id_mapper') 9 | 10 | # Reduce verbosity of requests logging 11 | logging.getLogger("requests").setLevel(logging.WARNING) 12 | logging.getLogger("urllib3").setLevel(logging.WARNING) 13 | 14 | nr_api_key = settings.nr_api_key 15 | 16 | 17 | class NewRelicAPIException(Exception): 18 | ''' 19 | Unable to get an acceptable response from New Relic API 20 | ''' 21 | 22 | 23 | def get_map_entries(timerange=30): 24 | query = "select * from `ecs_id_mapper_hash` where `newrelic_url` is null and `sample_time` between '{}' and '{}'".\ 25 | format(time.time() - timerange, time.time()) 26 | result = db.search_domain(query, 'ecs_id_mapper_hash') 27 | return result 28 | 29 | 30 | def update_nr_URL(): 31 | for result in get_map_entries(): 32 | nr_url = get_new_relic_app_instance_url(result['container_id']) 33 | _result = result 34 | _result['newrelic_url'] = nr_url 35 | if len(nr_url) > 0 and nr_url is not 'unknown': 36 | logger.info('Updating record with New Relic URL') 37 | print(result.name, _result, 'ecs_id_mapper_hash') 38 | db.put(result.name, _result, 'ecs_id_mapper_hash') 39 | else: 40 | logger.info('New Relic URL not found') 41 | 42 | 43 | def get_new_relic_app_instance_url(container_id): 44 | """ 45 | Query New Relic API for list of applications filtered by host id (docker short id). If the application was found 46 | key 'applications' will contain a list with details about the New Relic application. 47 | :param container_id: 48 | :return: 49 | """ 50 | container_id = container_id[:12] # Slice container id to shortened form 51 | # NR hostnames will always be in this format 52 | logger.info('Making request to New Relic API for container id {}'.format(container_id)) 53 | try: 54 | r = requests.get('https://api.newrelic.com/v2/applications.json?filter[host]={}'.format(container_id), 55 | headers={"X-Api-Key": nr_api_key, 56 | "content-type": "application/x-www-form-urlencoded"}, 57 | timeout=1) 58 | logger.debug(r.text) 59 | if len(r.json()['applications']) > 1: 60 | # we got more applications back than we expected 61 | logger.info('found more than one new relic application for that container') 62 | raise NewRelicAPIException 63 | 64 | application_id = r.json()['applications'][0]['id'] 65 | r = requests.get('https://api.newrelic.com/v2/applications/{}/hosts.json?filter[hostname]={}'.format( 66 | application_id, container_id), 67 | headers={"X-Api-Key": nr_api_key, 68 | "content-type": "application/x-www-form-urlencoded"}, 69 | timeout=1) 70 | logger.debug(r.text) 71 | application_instance_id = r.json()['application_hosts'][0]['links']['application_instances'][0] 72 | return settings.new_relic_app_instance_url.format(account_id=settings.new_relic_account_id, 73 | application_id=application_id, 74 | application_instance_id=application_instance_id) 75 | except requests.exceptions.Timeout: 76 | logger.info("New Relic didn't respond in time") 77 | raise NewRelicAPIException 78 | except (IndexError, KeyError): 79 | logger.info("Received an invalid response from New Relic. Likely this container ID wasn't found") 80 | if r: 81 | logger.debug(r.json()) 82 | raise NewRelicAPIException 83 | except requests.exceptions.SSLError as e: 84 | logger.error("SSL error connecting to New Relic {}".format(e)) 85 | raise NewRelicAPIException 86 | 87 | 88 | def get_new_relic_service_url(service_name, cluster_name): 89 | """ 90 | Get the new relic application URL for a given service. Assumes any one of the tasks in service report to the same 91 | new relic 'app'. 92 | :param service_name: str. Name of the service 93 | :param cluster_name: str. Name of cluster service is in 94 | :return: str. URL of new relic page for app 95 | """ 96 | try: 97 | task_id = ecs_api.get_task_ids_from_service(service_name,cluster_name) 98 | except: 99 | raise NewRelicAPIException 100 | else: 101 | task_id = task_id[0] # take the first task ID we found since we only need one 102 | logger.info('Found task_id {} for service {}'.format(task_id, service_name)) 103 | resultset = db.search_domain( 104 | 'select * from `ecs_id_mapper_hash` where task_id="{task_id}" and desired_status="RUNNING"'. 105 | format(task_id=task_id), 'ecs_id_mapper_hash') 106 | try: 107 | r = resultset.next() 108 | new_relic_url = r['new_relic_url'] 109 | except StopIteration: 110 | logger.info('Unable to find task {} details in our database'.format(task_id)) 111 | raise NewRelicAPIException 112 | except KeyError: 113 | new_relic_url = get_new_relic_app_instance_url(r['container_id']) 114 | db.put(r.name, {"new_relic_url": new_relic_url}, settings.hash_schema, replace=True) 115 | return new_relic_url.split('_')[0] 116 | -------------------------------------------------------------------------------- /server/run_server.py: -------------------------------------------------------------------------------- 1 | from tornado.wsgi import WSGIContainer 2 | from tornado.httpserver import HTTPServer 3 | from tornado.ioloop import IOLoop 4 | from server import ecs_id_mapper 5 | import logging 6 | import settings 7 | 8 | logger = logging.getLogger('ecs_id_mapper') 9 | logger.setLevel(settings.log_level) 10 | logger.propagate = False 11 | stderr_logs = logging.StreamHandler() 12 | stderr_logs.setLevel(getattr(logging, settings.log_level)) 13 | stderr_logs.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')) 14 | logger.addHandler(stderr_logs) 15 | 16 | # Reduce verbosity of requests logging 17 | logging.getLogger("requests").setLevel(logging.WARNING) 18 | logging.getLogger("urllib3").setLevel(logging.WARNING) 19 | 20 | # Format tornado's logs 21 | logging.getLogger('tornado').addHandler(stderr_logs) 22 | 23 | logger.info('Starting server on port {}'.format(str(settings.server_port))) 24 | http_server = HTTPServer(WSGIContainer(ecs_id_mapper)) 25 | http_server.listen(settings.server_port) 26 | IOLoop.instance().start() 27 | -------------------------------------------------------------------------------- /server/server.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, redirect, jsonify, abort 2 | from werkzeug.exceptions import NotFound 3 | import db 4 | import logging 5 | import copy 6 | import settings 7 | import new_relic_url_generator 8 | import ecs_api 9 | 10 | ecs_id_mapper = Flask(__name__) 11 | logger = logging.getLogger('ecs_id_mapper') 12 | 13 | 14 | 15 | @ecs_id_mapper.route('/report/event', methods=['POST']) 16 | def report_event(): 17 | """ 18 | update DB with new container task state change event 19 | :return: str. 'true' if successful 20 | """ 21 | if not request.json: 22 | logger.error('received non-json data') 23 | abort(400) 24 | logger.info('Received event from {}'.format(request.remote_addr)) 25 | logger.debug('Event payload {}'.format(request.json)) 26 | event_id = request.json['event_id'] 27 | event = request.json['event'] 28 | timestamp = request.json['timestamp'] 29 | db.put(str(timestamp)+"_"+str(event_id), 30 | {'container_id': event_id, 'event_action': event, 'timestamp': timestamp}, 31 | 'ecs_id_mapper_events') 32 | return 'true' 33 | 34 | 35 | @ecs_id_mapper.route('/report/map', methods=['POST']) 36 | def report_map(): 37 | """ 38 | update DB with new version of a container instance's id map 39 | :return: str. 'true' if successful 40 | """ 41 | if not request.json: 42 | logger.error('received non-json data') 43 | abort(400) 44 | logger.info('Received map update from {}'.format(request.remote_addr)) 45 | logger.debug('Map update {}'.format(request.json)) 46 | _map = request.json 47 | 48 | for k,v in _map.iteritems(): 49 | container_attributes = copy.deepcopy(v) 50 | try: 51 | container_attributes['cadvisor_url'] = \ 52 | "http://{}:9070/docker/{}".format( 53 | container_attributes['instance_ip'], 54 | container_attributes['container_id']) 55 | container_attributes['graylog_url'] = \ 56 | settings.graylog_url.format(graylog_fqdn=settings.graylog_fqdn, 57 | container_id=container_attributes['container_id'][:12]) 58 | except KeyError as e: 59 | logger.error('Unable to find keys in response: {}'.format(e)) 60 | _map[k] = container_attributes 61 | db.batch_put(_map, 'ecs_id_mapper_hash') 62 | return 'true' 63 | 64 | 65 | @ecs_id_mapper.route('/query/container_id/', methods=['GET']) 66 | def get_container_by_container_id(container_id): 67 | """ 68 | lookup task id based on matching container id 69 | :param container_id: str. container_id is a unique string generated by the docker daemon 70 | for each instance of a container 71 | :return: str. task id 72 | """ 73 | if len(container_id) <= 12: 74 | query = 'select * from `{schema}` where container_id like "{short_container_id}%"'.\ 75 | format(schema=settings.hash_schema, short_container_id=container_id) 76 | else: 77 | query = 'select * from `{schema}` where container_id="{container_id}"'.\ 78 | format(schema=settings.hash_schema, container_id=container_id) 79 | resultset = db.search_domain(query, settings.hash_schema) 80 | try: 81 | return resultset.next()['task_id'] 82 | except StopIteration: 83 | abort(404) 84 | 85 | 86 | @ecs_id_mapper.route('/query/container_id//_all', methods=['GET']) 87 | def get_all_container_attributes_by_container_id(container_id): 88 | """ 89 | lookup all attributes a container has by its container id 90 | :param container_id: str. container_id is a unique string generated by the docker daemon 91 | for each instance of a container 92 | :return: str. json encoded 93 | """ 94 | if len(container_id) <= 12: 95 | query = 'select * from `{schema}` where container_id like "{short_container_id}%"'.\ 96 | format(schema=settings.hash_schema, short_container_id=container_id) 97 | else: 98 | query = 'select * from `{schema}` where container_id="{container_id}"'.\ 99 | format(schema=settings.hash_schema, container_id=container_id) 100 | resultset = db.search_domain(query, settings.hash_schema) 101 | json_results = {} 102 | logger.debug(resultset) 103 | for result in resultset: 104 | for k,v in result.iteritems(): 105 | json_results[k] = v 106 | if len(json_results) == 0: 107 | abort(404) 108 | return jsonify(json_results) 109 | 110 | 111 | @ecs_id_mapper.route('/query/container_id//cadvisor', methods=['GET']) 112 | def get_cadvisor_url_by_container_id(container_id): 113 | """ 114 | Get the cadvisor URL for a given container 115 | :param container_id: str. container_id is a unique string generated by the docker 116 | daemon for each instance of a container 117 | :return: str. 118 | """ 119 | if len(container_id) <= 12: 120 | query = 'select * from `{schema}` where container_id like "{short_container_id}%"'.\ 121 | format(schema=settings.hash_schema, short_container_id=container_id) 122 | else: 123 | query = 'select * from `{schema}` where container_id="{container_id}"'.\ 124 | format(schema=settings.hash_schema, container_id=container_id) 125 | resultset = db.search_domain(query, settings.hash_schema) 126 | try: 127 | d = resultset.next() 128 | instance_ip = d['instance_ip'] 129 | cadvisor_url = "http://{}:{}/docker/{}".format(instance_ip, settings.cadvisor_port, container_id) 130 | if request.args.get('redir') and request.args.get('redir').lower() == "true": 131 | return redirect(cadvisor_url, 302) 132 | else: 133 | return cadvisor_url 134 | except StopIteration: 135 | abort(404) 136 | 137 | 138 | @ecs_id_mapper.route('/query/container_id//urls', methods=['GET']) 139 | def get_all_container_urls(container_id): 140 | """ 141 | get a list of all URLs we have relating to a container 142 | :param container_id: str. container_id 143 | :return: json 144 | """ 145 | if len(container_id) <= 12: 146 | query = 'select * from `{schema}` where container_id like "{short_container_id}%"'.\ 147 | format(schema=settings.hash_schema, short_container_id=container_id) 148 | else: 149 | query = 'select * from `{schema}` where container_id="{container_id}"'.\ 150 | format(schema=settings.hash_schema, container_id=container_id) 151 | resultset = db.search_domain(query, settings.hash_schema) 152 | json_results = {} 153 | logger.debug(resultset) 154 | for result in resultset: 155 | for k,v in result.iteritems(): 156 | json_results[k] = v 157 | return jsonify(json_results) 158 | 159 | 160 | @ecs_id_mapper.route('/query/task_id/', methods=['GET']) 161 | def get_container_by_task_id(task_id): 162 | """ 163 | lookup container id based on matching task id 164 | :param task_id: str. Task id is a uuid like string generated by ECS for each instance of a task 165 | :return: str. container id 166 | """ 167 | resultset = db.search_domain( 168 | 'select * from `ecs_id_mapper_hash` where task_id="{task_id}"'. 169 | format(task_id=task_id), 'ecs_id_mapper_hash') 170 | try: 171 | return resultset.next()['container_id'] 172 | except StopIteration: 173 | abort(404) 174 | 175 | 176 | @ecs_id_mapper.route('/query/task_id//_all', methods=['GET']) 177 | def get_all_container_attributes_by_task_id(task_id, json=True): 178 | """ 179 | lookup all attributes a container has by its task_id 180 | :param task_id: str. Task id is a uuid like string generated by ECS for each instance of a task 181 | :param json: bool. return a serialized json response (true) or a dict (false) 182 | :return: str. json encoded 183 | """ 184 | resultset = db.search_domain( 185 | 'select * from `ecs_id_mapper_hash` where task_id="{task_id}"'. 186 | format(task_id=task_id), 'ecs_id_mapper_hash') 187 | json_results = {} 188 | logger.debug(resultset) 189 | for result in resultset: 190 | for k,v in result.iteritems(): 191 | json_results[k] = v 192 | if len(json_results) == 0: 193 | abort(404) 194 | if json: 195 | return jsonify(json_results) 196 | return json_results 197 | 198 | 199 | @ecs_id_mapper.route('/query/task_id//cadvisor', methods=['GET']) 200 | def get_cadvisor_url_by_task_id(task_id): 201 | """ 202 | Get the cadvisor URL for a given container 203 | :param task_id: Task id is a uuid like string generated by ECS for each instance of a task 204 | :return: str. 205 | """ 206 | resultset = db.search_domain( 207 | 'select * from `ecs_id_mapper_hash` where task_id="{task_id}"'. 208 | format(task_id=task_id), 'ecs_id_mapper_hash') 209 | try: 210 | d = resultset.next() 211 | instance_ip = d['instance_ip'] 212 | container_id = d['container_id'] 213 | cadvisor_url = "http://{}:{}/docker/{}".format(instance_ip, settings.cadvisor_port, container_id) 214 | if request.args.get('redir') and request.args.get('redir').lower() == "true": 215 | return redirect(cadvisor_url, 302) 216 | else: 217 | return cadvisor_url 218 | except StopIteration: 219 | abort(404) 220 | 221 | 222 | @ecs_id_mapper.route('/query/task_id//graylog', methods=['GET']) 223 | def get_graylog_url_by_task_id(task_id): 224 | """ 225 | Get the graylog URL for a given container 226 | :param task_id: Task id is a uuid like string generated by ECS for each instance of a task 227 | :return: str. 228 | """ 229 | resultset = db.search_domain( 230 | 'select * from `ecs_id_mapper_hash` where task_id="{task_id}"'. 231 | format(task_id=task_id), 'ecs_id_mapper_hash') 232 | try: 233 | d = resultset.next() 234 | graylog_url = d['graylog_url'] 235 | if request.args.get('redir') and request.args.get('redir').lower() == "true": 236 | return redirect(graylog_url, 302) 237 | else: 238 | return graylog_url 239 | except StopIteration: 240 | abort(404) 241 | 242 | 243 | @ecs_id_mapper.route('/query/task_id//newrelic', methods=['GET']) 244 | def get_newrelic_url_by_task_id(task_id): 245 | """ 246 | Get the new relic URL for a given container. This will be the app instance specific URL for new relic 247 | :param task_id: Task id is a uuid like string generated by ECS for each instance of a task 248 | :return: str. 249 | """ 250 | resultset = db.search_domain( 251 | 'select * from `ecs_id_mapper_hash` where task_id="{task_id}"'. 252 | format(task_id=task_id), 'ecs_id_mapper_hash') 253 | try: 254 | d = resultset.next() 255 | new_relic_url = d['new_relic_url'] 256 | if request.args.get('redir') and request.args.get('redir').lower() == "true": 257 | return redirect(new_relic_url, 302) 258 | else: 259 | return new_relic_url 260 | except StopIteration: 261 | abort(404) 262 | except KeyError: 263 | # We don't have the new relic url yet 264 | try: 265 | logger.info('NR URL not found in DB. Querying NR API') 266 | new_relic_url = new_relic_url_generator.get_new_relic_app_instance_url(d['container_id']) 267 | db.put(d.name, {"new_relic_url": new_relic_url}, settings.hash_schema, replace=True) 268 | if request.args.get('redir') and request.args.get('redir').lower() == "true": 269 | return redirect(new_relic_url, 302) 270 | else: 271 | return new_relic_url 272 | except KeyError: 273 | logger.error('Unable to find container id for task_id {}'.format(task_id)) 274 | abort(404, 'Unable to find container id for task_id {}'.format(task_id)) 275 | except new_relic_url_generator.NewRelicAPIException: 276 | logger.error('Invalid response from New Relic API') 277 | abort(404, 'Unable to find New Relic app for that container/service.') 278 | 279 | 280 | @ecs_id_mapper.route('/query/service///newrelic') 281 | def get_newrelic_url_by_service(cluster_name, service_name): 282 | """ 283 | Get new relic app URL for a service 284 | :param cluster_name: str. name of the cluster that the service is in 285 | :param service_name: str. name of service to look up 286 | :return: URL of new relic app, or 302 redir to URL 287 | """ 288 | try: 289 | new_relic_url = new_relic_url_generator.get_new_relic_service_url(service_name, cluster_name) 290 | except new_relic_url_generator.NewRelicAPIException: 291 | abort(404, 'Unable to find New Relic app for that container/service') 292 | if request.args.get('redir') and request.args.get('redir').lower() == "true": 293 | return redirect(new_relic_url, 302) 294 | else: 295 | return new_relic_url 296 | 297 | 298 | @ecs_id_mapper.route('/query/service///_all') 299 | def get_all_service_attributes(cluster_name, service_name): 300 | """ 301 | Get all information for a service 302 | :param cluster_name: str. name of the cluster that the service is in 303 | :param service_name: str. name of service to look up 304 | :return: json 305 | """ 306 | service = {"service_name": service_name, 307 | "cluster_name": cluster_name, 308 | "tasks": []} 309 | try: 310 | for task in ecs_api.get_task_ids_from_service(service_name, cluster_name): 311 | try: 312 | task_json = get_all_container_attributes_by_task_id(task, json=False) 313 | service['tasks'].append(task_json) 314 | except NotFound as e: 315 | logger.warn('ECS API told us about task {} but unable to find in our database'. 316 | format(task)) 317 | return jsonify(service) 318 | except: 319 | abort(404, 'ECS service not found') 320 | 321 | 322 | @ecs_id_mapper.route('/query/events/', methods=['GET']) 323 | def get_events_by_task_name(task_name): 324 | """ 325 | Get the graylog URL for a given container 326 | :param task_name: This is the name of the task as defined in the task ECS Task Definition 327 | """ 328 | 329 | resultset=db.search_domain( 330 | 'select * from `ecs_id_mapper_hash` where task_name="{task_name}"'. 331 | format(task_name=task_name), 'ecs_id_mapper_hash') 332 | try: 333 | d = resultset.next() 334 | contiainer_id = d['contianer_id'] 335 | print contiainer_id 336 | except StopIteration: 337 | abort(404) 338 | 339 | resultset = db.search_domain( 340 | 'select * from `ecs_id_mapper_events` where container_id="{contiainer_id}"'. 341 | format(contiainer_id=contiainer_id), 'ecs_id_mapper_events') 342 | try: 343 | d = resultset.next() 344 | return d 345 | except StopIteration: 346 | abort(404) 347 | 348 | 349 | @ecs_id_mapper.route('/health') 350 | def check_health(): 351 | try: 352 | db.list_domains() 353 | return "All systems go!" 354 | except: 355 | abort(500) 356 | 357 | 358 | if __name__ == '__main__': 359 | # This starts the built in flask server, not designed for production use 360 | logger.info('Starting server...') 361 | ecs_id_mapper.run(debug=False, host='0.0.0.0', port=5001) -------------------------------------------------------------------------------- /server/settings.py: -------------------------------------------------------------------------------- 1 | from os import getenv 2 | import hvac 3 | import logging 4 | 5 | vault_client = {} 6 | logger = logging.getLogger('ecs_id_mapper') 7 | 8 | vault_host = getenv('vault_host') 9 | vault_token = getenv('vault_token') 10 | vault_key_aws_id = getenv('vault_key_aws_id') 11 | vault_key_aws_secret_key = getenv('vault_key_aws_secret_key') 12 | simpledb_aws_region = getenv('simpledb_aws_region', 'us-west-2') 13 | graylog_fqdn = getenv('graylog_fqdn') 14 | cadvisor_port = getenv('cadvisor_port') 15 | new_relic_account_id = getenv('new_relic_account_id') 16 | graylog_url = getenv('graylog_url', 17 | "http://{graylog_fqdn}/search?rangetype=relative&fields=message%2Csource&width=1639&relative=86400&q=tag%3Adocker.{container_id}#fields=log") 18 | log_level = getenv('log_level', 'INFO') 19 | server_port = getenv('server_port', 5001) 20 | dev_mode = getenv('dev_mode', 'false') 21 | hash_schema = 'ecs_id_mapper_hash' 22 | events_schema = 'ecs_id_mapper_events' 23 | services_schema = 'ecs_id_mapper_services' 24 | new_relic_app_instance_url = "https://rpm.newrelic.com/accounts/{account_id}/applications/" \ 25 | "{application_id}_i{application_instance_id}" 26 | 27 | 28 | def vault_get(name): 29 | try: 30 | client = vault_client['client'] 31 | except KeyError: 32 | print("Making connection to vault host: {}".format(vault_host)) 33 | vault_client['client'] = hvac.Client(url=vault_host, token=vault_token) 34 | client = vault_client['client'] 35 | 36 | result = client.read('secret/{}'.format(name)) 37 | if result is None: 38 | raise Exception('Unable to find secret {}'.format(name)) 39 | else: 40 | try: 41 | logger.info("Retrieved secret from Vault using key {}".format(name)) 42 | return result['data']['value'] 43 | except KeyError: 44 | logger.error('Unable to find key in response data from Vault') 45 | raise Exception('Unable to find key in response data from Vault') 46 | 47 | if not dev_mode == 'true': 48 | if not vault_token or not vault_host: 49 | raise Exception('Missing required config values: vault_host, vault_token') 50 | aws_id = vault_get(vault_key_aws_id) 51 | aws_secret_key = vault_get(vault_key_aws_secret_key) 52 | nr_api_key = vault_get('new_relic_api_key') 53 | else: 54 | aws_id = getenv('aws_id') 55 | aws_secret_key = getenv('aws_secret_key') 56 | nr_api_key = getenv('nr_api_key') 57 | --------------------------------------------------------------------------------